Add document outline support for Markdown/Quarto files
Build and Deploy Verso / deploy (push) Successful in 10m47s
Build and Deploy Verso / deploy (push) Successful in 10m47s
outline.ts: export NestingLevel so it can be used outside the file. markdown/document-outline.ts: new enterMarkdownNode function that walks the Lezer Markdown syntax tree and extracts ATXHeading1-6 and SetextHeading1-2 nodes, mapping them to the same NestingLevel enum used by the LaTeX outline (Section→SubSection→SubSubSection…). Wrapped in makeProjectionStateField for incremental updates. markdown/index.ts: register markdownDocumentOutline as a CodeMirror extension in the Markdown LanguageSupport so the StateField is active whenever a .qmd file is open. codemirror-outline.tsx: fall back to markdownDocumentOutline when the LaTeX documentOutline StateField is not present in the editor state (i.e. when the active language is Markdown, not LaTeX). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useCodeMirrorStateContext } from './codemirror-context'
|
||||
import React, { useEffect } from 'react'
|
||||
import { documentOutline } from '../languages/latex/document-outline'
|
||||
import { markdownDocumentOutline } from '../languages/markdown/document-outline'
|
||||
import { ProjectionStatus } from '../utils/tree-operations/projection'
|
||||
import useDebounce from '../../../shared/hooks/use-debounce'
|
||||
import { useOutlineContext } from '@/features/ide-react/context/outline-context'
|
||||
@@ -10,7 +11,10 @@ export const CodemirrorOutline = React.memo(function CodemirrorOutline() {
|
||||
|
||||
const state = useCodeMirrorStateContext()
|
||||
const debouncedState = useDebounce(state, 100)
|
||||
const outlineResult = debouncedState.field(documentOutline, false)
|
||||
// Use whichever outline StateField is active for the current language
|
||||
const outlineResult =
|
||||
debouncedState.field(documentOutline, false) ??
|
||||
debouncedState.field(markdownDocumentOutline, false)
|
||||
|
||||
// when the outline projection changes, calculate the flat outline
|
||||
useEffect(() => {
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { EditorState } from '@codemirror/state'
|
||||
import { SyntaxNodeRef } from '@lezer/common'
|
||||
import {
|
||||
FlatOutlineItem,
|
||||
NestingLevel,
|
||||
} from '../../utils/tree-operations/outline'
|
||||
import { NodeIntersectsChangeFn } from '../../utils/tree-operations/projection'
|
||||
import { makeProjectionStateField } from '../../utils/projection-state-field'
|
||||
|
||||
// Map Lezer Markdown node names to outline nesting levels
|
||||
const HEADING_LEVELS: Record<string, NestingLevel> = {
|
||||
ATXHeading1: NestingLevel.Section,
|
||||
ATXHeading2: NestingLevel.SubSection,
|
||||
ATXHeading3: NestingLevel.SubSubSection,
|
||||
ATXHeading4: NestingLevel.Paragraph,
|
||||
ATXHeading5: NestingLevel.SubParagraph,
|
||||
ATXHeading6: NestingLevel.SubParagraph,
|
||||
SetextHeading1: NestingLevel.Section,
|
||||
SetextHeading2: NestingLevel.SubSection,
|
||||
}
|
||||
|
||||
const enterMarkdownNode = (
|
||||
state: EditorState,
|
||||
node: SyntaxNodeRef,
|
||||
items: FlatOutlineItem[],
|
||||
nodeIntersectsChange: NodeIntersectsChangeFn
|
||||
): void => {
|
||||
const level = HEADING_LEVELS[node.name]
|
||||
if (level === undefined) return
|
||||
|
||||
if (!nodeIntersectsChange(node)) {
|
||||
// Node unchanged — already present in items from the previous projection
|
||||
return
|
||||
}
|
||||
|
||||
// Collect heading text, skipping the HeaderMark (the leading # characters)
|
||||
let title = ''
|
||||
const cursor = node.node.cursor()
|
||||
if (cursor.firstChild()) {
|
||||
do {
|
||||
if (cursor.name !== 'HeaderMark') {
|
||||
title += state.sliceDoc(cursor.from, cursor.to)
|
||||
}
|
||||
} while (cursor.nextSibling())
|
||||
}
|
||||
|
||||
items.push({
|
||||
line: state.doc.lineAt(node.from).number,
|
||||
toLine: state.doc.lineAt(node.to).number,
|
||||
title: title.trim(),
|
||||
from: node.from,
|
||||
to: node.to,
|
||||
level,
|
||||
} as FlatOutlineItem)
|
||||
}
|
||||
|
||||
export const markdownDocumentOutline =
|
||||
makeProjectionStateField<FlatOutlineItem>(enterMarkdownNode)
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
syntaxHighlighting,
|
||||
} from '@codemirror/language'
|
||||
import { tags } from '@lezer/highlight'
|
||||
import { markdownDocumentOutline } from './document-outline'
|
||||
|
||||
export const markdown = () => {
|
||||
const { language, support } = markdownLanguage({
|
||||
@@ -19,6 +20,7 @@ export const markdown = () => {
|
||||
support,
|
||||
shortcuts(),
|
||||
syntaxHighlighting(markdownHighlightStyle),
|
||||
markdownDocumentOutline,
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export class FlatOutlineItem extends ProjectionItem {
|
||||
export type FlatOutline = FlatOutlineItem[]
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
enum NestingLevel {
|
||||
export enum NestingLevel {
|
||||
Book = 1,
|
||||
Part = 2,
|
||||
Chapter = 3,
|
||||
|
||||
Reference in New Issue
Block a user