diff --git a/services/web/frontend/js/features/source-editor/components/codemirror-outline.tsx b/services/web/frontend/js/features/source-editor/components/codemirror-outline.tsx index 4a17e1b46a..1d69329963 100644 --- a/services/web/frontend/js/features/source-editor/components/codemirror-outline.tsx +++ b/services/web/frontend/js/features/source-editor/components/codemirror-outline.tsx @@ -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(() => { diff --git a/services/web/frontend/js/features/source-editor/languages/markdown/document-outline.ts b/services/web/frontend/js/features/source-editor/languages/markdown/document-outline.ts new file mode 100644 index 0000000000..7f7a5ea4d8 --- /dev/null +++ b/services/web/frontend/js/features/source-editor/languages/markdown/document-outline.ts @@ -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 = { + 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(enterMarkdownNode) diff --git a/services/web/frontend/js/features/source-editor/languages/markdown/index.ts b/services/web/frontend/js/features/source-editor/languages/markdown/index.ts index 23d3597967..3ce1280b21 100644 --- a/services/web/frontend/js/features/source-editor/languages/markdown/index.ts +++ b/services/web/frontend/js/features/source-editor/languages/markdown/index.ts @@ -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, ]) } diff --git a/services/web/frontend/js/features/source-editor/utils/tree-operations/outline.ts b/services/web/frontend/js/features/source-editor/utils/tree-operations/outline.ts index 8a3b089285..84814fa6cf 100644 --- a/services/web/frontend/js/features/source-editor/utils/tree-operations/outline.ts +++ b/services/web/frontend/js/features/source-editor/utils/tree-operations/outline.ts @@ -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,