From e02508806596579492a420b08c15ebe9d1e5c92d Mon Sep 17 00:00:00 2001 From: Alf Eaton Date: Thu, 8 Jun 2023 09:35:51 +0100 Subject: [PATCH] Merge pull request #13241 from overleaf/ae-extensions-documentation Add documentation for CodeMirror extensions GitOrigin-RevId: e5f07084173f201919272f9d46dcdaef4b817874 --- .../source-editor/extensions/annotations.ts | 16 ++++++- .../source-editor/extensions/auto-complete.ts | 15 ++++++- .../source-editor/extensions/auto-pair.ts | 3 ++ .../extensions/bracket-matching.ts | 12 +++++- .../extensions/cursor-highlights.ts | 3 ++ .../extensions/cursor-position.ts | 5 +++ .../extensions/draw-selection.ts | 9 ++-- .../source-editor/extensions/editable.ts | 5 +++ .../extensions/effect-listeners.ts | 4 ++ .../extensions/empty-line-filler.ts | 4 ++ .../extensions/exception-logger.ts | 4 ++ .../extensions/filter-characters.ts | 3 ++ .../extensions/folding-keymap.ts | 3 ++ .../source-editor/extensions/font-load.ts | 4 ++ .../source-editor/extensions/go-to-line.ts | 4 ++ .../extensions/highlight-active-line.ts | 4 ++ .../extensions/highlight-special-chars.ts | 18 ++++++++ .../extensions/indentation-markers.ts | 4 ++ .../source-editor/extensions/index.ts | 43 ++++++++++++------- .../extensions/inline-background.ts | 4 ++ .../source-editor/extensions/keybindings.ts | 5 +++ .../source-editor/extensions/keymaps.ts | 5 +++ .../source-editor/extensions/language.ts | 17 +++++++- .../source-editor/extensions/line-numbers.ts | 9 +++- .../extensions/line-wrapping-indentation.ts | 6 +++ .../source-editor/extensions/phrases.ts | 3 ++ .../source-editor/extensions/realtime.ts | 3 ++ .../extensions/scroll-one-line.ts | 4 +- .../extensions/scroll-position.ts | 6 +++ .../source-editor/extensions/search.ts | 12 ++++-- .../source-editor/extensions/shortcuts.ts | 3 ++ .../extensions/spelling/index.ts | 8 +++- .../extensions/symbol-palette.ts | 3 ++ .../source-editor/extensions/theme.ts | 15 +++++-- .../extensions/third-party-extensions.ts | 4 ++ .../extensions/toolbar/toolbar-panel.ts | 4 ++ .../source-editor/extensions/track-changes.ts | 4 ++ .../extensions/vertical-overflow.ts | 5 +++ .../extensions/visual/atomic-decorations.ts | 1 + .../extensions/visual/list-item-marker.ts | 5 ++- .../extensions/visual/mark-decorations.ts | 1 + .../extensions/visual/selection.ts | 4 ++ .../extensions/visual/skip-preamble-cursor.ts | 4 ++ .../extensions/visual/visual-keymap.ts | 4 ++ .../extensions/visual/visual-theme.ts | 3 ++ .../source-editor/extensions/visual/visual.ts | 8 ++++ .../extensions/wait-for-parser.ts | 3 ++ .../languages/latex/codemirror-dev-tools.ts | 4 ++ 48 files changed, 282 insertions(+), 38 deletions(-) create mode 100644 services/web/frontend/js/features/source-editor/extensions/highlight-special-chars.ts diff --git a/services/web/frontend/js/features/source-editor/extensions/annotations.ts b/services/web/frontend/js/features/source-editor/extensions/annotations.ts index a1611ddaa8..e9c33119d4 100644 --- a/services/web/frontend/js/features/source-editor/extensions/annotations.ts +++ b/services/web/frontend/js/features/source-editor/extensions/annotations.ts @@ -2,6 +2,7 @@ import { EditorView, ViewUpdate } from '@codemirror/view' import { Diagnostic, linter, lintGutter } from '@codemirror/lint' import { Compartment, + Extension, RangeSet, RangeValue, StateEffect, @@ -15,10 +16,15 @@ const compileLintSourceConf = new Compartment() export const annotations = () => [ compileDiagnosticsState, compileLintSourceConf.of(compileLogLintSource()), + /** + * The built-in lint gutter extension, configured with zero hover delay. + */ lintGutter({ hoverTime: 0, }), - // move the lint gutter outside the line numbers + /** + * A theme which moves the lint gutter outside the line numbers. + */ EditorView.baseTheme({ '.cm-gutter-lint': { order: -1, @@ -41,7 +47,10 @@ export const lintSourceConfig = { }, } -const compileLogLintSource = () => +/** + * A lint source using the compile log diagnostics + */ +const compileLogLintSource = (): Extension => linter(view => { const items: Diagnostic[] = [] const cursor = view.state.field(compileDiagnosticsState).iter() @@ -64,6 +73,9 @@ class DiagnosticRangeValue extends RangeValue { const setCompileDiagnosticsEffect = StateEffect.define() +/** + * A state field for the compile log diagnostics + */ export const compileDiagnosticsState = StateField.define< RangeSet >({ diff --git a/services/web/frontend/js/features/source-editor/extensions/auto-complete.ts b/services/web/frontend/js/features/source-editor/extensions/auto-complete.ts index d9e5f5d389..315faa5582 100644 --- a/services/web/frontend/js/features/source-editor/extensions/auto-complete.ts +++ b/services/web/frontend/js/features/source-editor/extensions/auto-complete.ts @@ -28,6 +28,11 @@ const createAutoComplete = (enabled: boolean) => { return [ [ autocompleteTheme, + /** + * A built-in extension which provides the autocomplete feature, + * configured with a custom render function and + * a zero interaction delay (so that keypresses are handled after the autocomplete is opened). + */ autocompletion({ icons: false, defaultKeymap: false, @@ -50,6 +55,9 @@ const createAutoComplete = (enabled: boolean) => { }, interactionDelay: 0, }), + /** + * A keymap which adds Tab for accepting a completion and Ctrl-Space for opening autocomplete. + */ Prec.highest( keymap.of([ { key: 'Escape', run: closeCompletion }, @@ -61,7 +69,9 @@ const createAutoComplete = (enabled: boolean) => { { key: 'Tab', run: acceptCompletion }, ]) ), - // NOTE: must be lower precedence than Ctrl-Space and Alt-Space in references-search + /** + * A keymap which positions Ctrl-Space and Alt-Space below the corresponding bindings for advanced reference search. + */ Prec.high( keymap.of([ { key: 'Ctrl-Space', run: startCompletion }, @@ -72,6 +82,9 @@ const createAutoComplete = (enabled: boolean) => { ] } +/** + * Styles for the autocomplete menu + */ const autocompleteTheme = EditorView.baseTheme({ '.cm-tooltip.cm-tooltip-autocomplete': { // shift the tooltip, so the completion aligns with the text diff --git a/services/web/frontend/js/features/source-editor/extensions/auto-pair.ts b/services/web/frontend/js/features/source-editor/extensions/auto-pair.ts index f3d8f0a2b2..791820a728 100644 --- a/services/web/frontend/js/features/source-editor/extensions/auto-pair.ts +++ b/services/web/frontend/js/features/source-editor/extensions/auto-pair.ts @@ -16,6 +16,9 @@ export const setAutoPair = (autoPairDelimiters: boolean): TransactionSpec => { } } +/** + * The built-in closeBrackets extension and closeBrackets keymap. + */ const extension = [ closeBrackets(), // NOTE: using Prec.highest as this needs to run before the default Backspace handler diff --git a/services/web/frontend/js/features/source-editor/extensions/bracket-matching.ts b/services/web/frontend/js/features/source-editor/extensions/bracket-matching.ts index 3ec0bd64ad..afcdf5c44e 100644 --- a/services/web/frontend/js/features/source-editor/extensions/bracket-matching.ts +++ b/services/web/frontend/js/features/source-editor/extensions/bracket-matching.ts @@ -18,6 +18,11 @@ const FORWARDS = 1 const BACKWARDS = -1 type Direction = 1 | -1 +/** + * A built-in extension which decorates matching pairs of brackets when focused, + * configured with a custom render function that combines adjacent pairs of matching markers + * into a single decoration so there’s no border between them. + */ export const bracketMatching = () => { return bracketMatchingExtension({ renderMatch: match => { @@ -59,6 +64,10 @@ const matchedAdjacent = (match: MatchResult): match is AdjacentMatchResult => (match.start.to === match.end.from || match.end.to === match.start.from) ) +/** + * A custom extension which handles double-click events on a matched bracket + * and extends the selection to cover the contents of the bracket pair. + */ export const bracketSelection = (): Extension[] => [ EditorView.domEventHandlers({ dblclick: (evt, view) => { @@ -74,11 +83,10 @@ export const bracketSelection = (): Extension[] => [ maxScanDistance: 0, }) if (match?.matched && match.end) { - const newRange = EditorSelection.range( + return EditorSelection.range( Math.min(match.start.from, match.end.from), Math.max(match.end.to, match.start.to) ) - return newRange } return false } diff --git a/services/web/frontend/js/features/source-editor/extensions/cursor-highlights.ts b/services/web/frontend/js/features/source-editor/extensions/cursor-highlights.ts index 16fc40f934..6838535aef 100644 --- a/services/web/frontend/js/features/source-editor/extensions/cursor-highlights.ts +++ b/services/web/frontend/js/features/source-editor/extensions/cursor-highlights.ts @@ -16,6 +16,9 @@ import { findValidPosition } from '../utils/position' import { Highlight } from '../../../../../types/highlight' import { fullHeightCoordsAtPos, getBase } from '../utils/layer' +/** + * A custom extension that displays collaborator cursors in a separate layer. + */ export const cursorHighlights = () => { return [ cursorHighlightsState, diff --git a/services/web/frontend/js/features/source-editor/extensions/cursor-position.ts b/services/web/frontend/js/features/source-editor/extensions/cursor-position.ts index 38dff4beed..6a61e0c5aa 100644 --- a/services/web/frontend/js/features/source-editor/extensions/cursor-position.ts +++ b/services/web/frontend/js/features/source-editor/extensions/cursor-position.ts @@ -11,6 +11,11 @@ import customLocalStorage from '../../../infrastructure/local-storage' const buildStorageKey = (docId: string) => `doc.position.${docId}` +/** + * A custom extension that: + * a) stores the cursor position in localStorage when the view is destroyed or the window is closed. + * b) dispatches the cursor position when it changes, for use with “show position in PDF”. + */ export const cursorPosition = ({ currentDoc: { doc_id: docId }, }: { diff --git a/services/web/frontend/js/features/source-editor/extensions/draw-selection.ts b/services/web/frontend/js/features/source-editor/extensions/draw-selection.ts index 046441f1d7..01278316ce 100644 --- a/services/web/frontend/js/features/source-editor/extensions/draw-selection.ts +++ b/services/web/frontend/js/features/source-editor/extensions/draw-selection.ts @@ -4,9 +4,12 @@ import { rectangleMarkerForRange } from '../utils/layer' import { updateHasMouseDownEffect } from './visual/selection' import browser from './browser' -// This is mostly a copy of CodeMirror's built-in drawSelection extension. We -// have our own copy so that we can use our own implementation of -// rectangleMarkerForRange +/** + * The built-in extension which draws the cursor and selection(s) in layers, + * copied to make use of a custom version of rectangleMarkerForRange which calls + * fullHeightCoordsAtPos when in Source mode, extending the top and bottom + * of the coords to cover the full line height. + */ export const drawSelection = () => { return [cursorLayer, selectionLayer, hideNativeSelection] } diff --git a/services/web/frontend/js/features/source-editor/extensions/editable.ts b/services/web/frontend/js/features/source-editor/extensions/editable.ts index 83b2b6c7f7..1c59248670 100644 --- a/services/web/frontend/js/features/source-editor/extensions/editable.ts +++ b/services/web/frontend/js/features/source-editor/extensions/editable.ts @@ -3,6 +3,11 @@ import { EditorView } from '@codemirror/view' const readOnlyConf = new Compartment() +/** + * A custom extension which determines whether the content is editable, by setting the value of the EditorState.readOnly and EditorView.editable facets. + * Commands and extensions read the EditorState.readOnly facet to decide whether they should be applied. + * EditorView.editable determines whether the DOM can be focused, by changing the value of the contenteditable attribute. + */ export const editable = () => { return [ readOnlyConf.of([ diff --git a/services/web/frontend/js/features/source-editor/extensions/effect-listeners.ts b/services/web/frontend/js/features/source-editor/extensions/effect-listeners.ts index 04d4d72aaa..906aeb3b15 100644 --- a/services/web/frontend/js/features/source-editor/extensions/effect-listeners.ts +++ b/services/web/frontend/js/features/source-editor/extensions/effect-listeners.ts @@ -14,6 +14,10 @@ type EffectListener = { const addEffectListenerEffect = StateEffect.define() const removeEffectListenerEffect = StateEffect.define() +/** + * A state field that stores a collection of listeners per effect, + * and runs each listener's callback function when the effect is dispatched. + */ export const effectListeners = () => [effectListenersField] const effectListenersField = StateField.define({ diff --git a/services/web/frontend/js/features/source-editor/extensions/empty-line-filler.ts b/services/web/frontend/js/features/source-editor/extensions/empty-line-filler.ts index 94b839390e..0d84b868e1 100644 --- a/services/web/frontend/js/features/source-editor/extensions/empty-line-filler.ts +++ b/services/web/frontend/js/features/source-editor/extensions/empty-line-filler.ts @@ -20,6 +20,10 @@ class EmptyLineWidget extends WidgetType { } } +/** + * A custom extension which adds a widget decoration at the start of each empty line in the viewport, + * so that the line is highlighted when part of tracked changes. + */ export const emptyLineFiller = () => { if (browser.ios) { // disable on iOS as it breaks Backspace across empty lines diff --git a/services/web/frontend/js/features/source-editor/extensions/exception-logger.ts b/services/web/frontend/js/features/source-editor/extensions/exception-logger.ts index 117e9915a3..c36d318315 100644 --- a/services/web/frontend/js/features/source-editor/extensions/exception-logger.ts +++ b/services/web/frontend/js/features/source-editor/extensions/exception-logger.ts @@ -2,6 +2,10 @@ import { Extension } from '@codemirror/state' import { EditorView } from '@codemirror/view' import { captureException } from '../../../infrastructure/error-reporter' +/** + * A custom extension which configures the EditorView.exceptionSink facet + * so that exceptions are sent to Sentry with a `cm6-exception` tag. + */ export const exceptionLogger = (): Extension => { return EditorView.exceptionSink.of(exception => { captureException(exception, { diff --git a/services/web/frontend/js/features/source-editor/extensions/filter-characters.ts b/services/web/frontend/js/features/source-editor/extensions/filter-characters.ts index d6f0e35d1b..62f59518c6 100644 --- a/services/web/frontend/js/features/source-editor/extensions/filter-characters.ts +++ b/services/web/frontend/js/features/source-editor/extensions/filter-characters.ts @@ -3,6 +3,9 @@ import { ChangeSpec, EditorState, Transaction } from '@codemirror/state' const BAD_CHARS_REGEXP = /[\0\uD800-\uDFFF]/g const BAD_CHARS_REPLACEMENT_CHAR = '\uFFFD' +/** + * A custom extension that replaces input characters in a Unicode range with a replacement character. + */ export const filterCharacters = () => { return EditorState.transactionFilter.of(tr => { if (tr.docChanged && !tr.annotation(Transaction.remote)) { diff --git a/services/web/frontend/js/features/source-editor/extensions/folding-keymap.ts b/services/web/frontend/js/features/source-editor/extensions/folding-keymap.ts index 8bb410339f..af914cb3b8 100644 --- a/services/web/frontend/js/features/source-editor/extensions/folding-keymap.ts +++ b/services/web/frontend/js/features/source-editor/extensions/folding-keymap.ts @@ -1,5 +1,8 @@ import { foldAll, toggleFold, unfoldAll } from '@codemirror/language' +/** + * A custom extension that binds keyboard shortcuts to folding actions. + */ export const foldingKeymap = [ { key: 'F2', diff --git a/services/web/frontend/js/features/source-editor/extensions/font-load.ts b/services/web/frontend/js/features/source-editor/extensions/font-load.ts index df3587b103..04c2861a4f 100644 --- a/services/web/frontend/js/features/source-editor/extensions/font-load.ts +++ b/services/web/frontend/js/features/source-editor/extensions/font-load.ts @@ -5,6 +5,10 @@ import { updateHasEffect } from '../utils/effects' const fontLoadEffect = StateEffect.define() export const hasFontLoadedEffect = updateHasEffect(fontLoadEffect) +/** + * A custom extension that listens for an event indicating that fonts have finished loading, + * then dispatches an effect which other extensions can use to recalculate values. + */ export const fontLoad = ViewPlugin.define(view => { function listener(this: FontFaceSet, event: FontFaceSetLoadEvent) { view.dispatch({ effects: fontLoadEffect.of(event.fontfaces) }) diff --git a/services/web/frontend/js/features/source-editor/extensions/go-to-line.ts b/services/web/frontend/js/features/source-editor/extensions/go-to-line.ts index 5669a74798..cd2dd90be7 100644 --- a/services/web/frontend/js/features/source-editor/extensions/go-to-line.ts +++ b/services/web/frontend/js/features/source-editor/extensions/go-to-line.ts @@ -2,6 +2,10 @@ import { Prec } from '@codemirror/state' import { EditorView, keymap } from '@codemirror/view' import { gotoLine } from '@codemirror/search' +/** + * A custom extension that provides a keyboard shortcut + * and panel with UI for jumping to a specific line number. + */ export const goToLinePanel = () => { return [ Prec.high( diff --git a/services/web/frontend/js/features/source-editor/extensions/highlight-active-line.ts b/services/web/frontend/js/features/source-editor/extensions/highlight-active-line.ts index af0e7df064..8fedf1c194 100644 --- a/services/web/frontend/js/features/source-editor/extensions/highlight-active-line.ts +++ b/services/web/frontend/js/features/source-editor/extensions/highlight-active-line.ts @@ -11,6 +11,10 @@ import { import { sourceOnly } from './visual/visual' import { fullHeightCoordsAtPos } from '../utils/layer' +/** + * An alternative version of the built-in highlightActiveLine extension, + * using a custom approach for highlighting the full height of the active “visual line” of a wrapped line. + */ export const highlightActiveLine = (visual: boolean) => { // this extension should only be active in the source editor return sourceOnly(visual, [ diff --git a/services/web/frontend/js/features/source-editor/extensions/highlight-special-chars.ts b/services/web/frontend/js/features/source-editor/extensions/highlight-special-chars.ts new file mode 100644 index 0000000000..d794f7d097 --- /dev/null +++ b/services/web/frontend/js/features/source-editor/extensions/highlight-special-chars.ts @@ -0,0 +1,18 @@ +import { sourceOnly } from './visual/visual' +import { highlightSpecialChars as _highlightSpecialChars } from '@codemirror/view' + +/** + * The built-in extension which highlights unusual whitespace characters, + * configured to highlight additional space characters. + */ +export const highlightSpecialChars = (visual: boolean) => + sourceOnly( + visual, + _highlightSpecialChars({ + addSpecialChars: new RegExp( + // non standard space characters (https://jkorpela.fi/chars/spaces.html) + '[\u00A0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u200B\u202F\u205F\u3000\uFEFF]', + /x/.unicode != null ? 'gu' : 'g' + ), + }) + ) diff --git a/services/web/frontend/js/features/source-editor/extensions/indentation-markers.ts b/services/web/frontend/js/features/source-editor/extensions/indentation-markers.ts index c0cc2fe9d0..2811b5026c 100644 --- a/services/web/frontend/js/features/source-editor/extensions/indentation-markers.ts +++ b/services/web/frontend/js/features/source-editor/extensions/indentation-markers.ts @@ -2,6 +2,10 @@ import { Extension } from '@codemirror/state' import { indentationMarkers as markers } from '@replit/codemirror-indentation-markers' import { sourceOnly } from './visual/visual' +/** + * A third-party extension which adds markers to show the indentation level. + * Configured to omit markers in the first column and to keep the same style for markers in the active block. + */ export const indentationMarkers = (visual: boolean): Extension => sourceOnly(visual, [ markers({ hideFirstIndent: true, highlightActiveBlock: false }), diff --git a/services/web/frontend/js/features/source-editor/extensions/index.ts b/services/web/frontend/js/features/source-editor/extensions/index.ts index 4cf4658a04..eb566db96e 100644 --- a/services/web/frontend/js/features/source-editor/extensions/index.ts +++ b/services/web/frontend/js/features/source-editor/extensions/index.ts @@ -1,6 +1,5 @@ import { EditorView, - highlightSpecialChars, rectangularSelection, tooltips, crosshairCursor, @@ -38,13 +37,14 @@ import importOverleafModules from '../../../../macros/import-overleaf-module.mac import { emptyLineFiller } from './empty-line-filler' import { goToLinePanel } from './go-to-line' import { drawSelection } from './draw-selection' -import { sourceOnly, visual } from './visual/visual' +import { visual } from './visual/visual' import { inlineBackground } from './inline-background' import { indentationMarkers } from './indentation-markers' import { codemirrorDevTools } from '../languages/latex/codemirror-dev-tools' import { keymaps } from './keymaps' import { shortcuts } from './shortcuts' import { effectListeners } from './effect-listeners' +import { highlightSpecialChars } from './highlight-special-chars' const moduleExtensions: Array<() => Extension> = importOverleafModules( 'sourceEditorExtensions' @@ -52,32 +52,38 @@ const moduleExtensions: Array<() => Extension> = importOverleafModules( export const createExtensions = (options: Record): Extension[] => [ lineNumbers(), - sourceOnly( - false, - highlightSpecialChars({ - addSpecialChars: new RegExp( - // non standard space characters (https://jkorpela.fi/chars/spaces.html) - '[\u00A0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u200B\u202F\u205F\u3000\uFEFF]', - /x/.unicode != null ? 'gu' : 'g' - ), - }) - ), + highlightSpecialChars(options.visual.visual), + // The built-in extension that manages the history stack, + // configured to increase the maximum delay between adjacent grouped edits history({ newGroupDelay: 250 }), + // The built-in extension that displays buttons for folding code in a gutter element, + // configured with custom openText and closeText symbols. foldGutter({ openText: '▾', closedText: '▸', }), drawSelection(), + // A built-in facet that is set to true to allow multiple selections. + // This makes the editor more like a code editor than Google Docs or Microsoft Word, + // which only have single selections. EditorState.allowMultipleSelections.of(true), + // A built-in extension that enables soft line wrapping. EditorView.lineWrapping, + // A built-in extension that re-indents input if the language defines an indentOnInput field in its language data. indentOnInput(), lineWrappingIndentation(options.visual.visual), indentationMarkers(options.visual.visual), bracketMatching(), bracketSelection(), + // A built-in extension that enables rectangular selections, created by dragging a new selection while holding down Alt. rectangularSelection(), + // A built-in extension that turns the pointer into a crosshair while Alt is pressed. crosshairCursor(), + // A built-in extension that shows where dragged content will be dropped. dropCursor(), + // A built-in extension that is used for configuring tooltip behaviour, + // configured so that the tooltip parent is the document body, + // to avoid cutting off tooltips which overflow the editor. tooltips({ parent: document.body, }), @@ -85,15 +91,16 @@ export const createExtensions = (options: Record): Extension[] => [ goToLinePanel(), filterCharacters(), - // `autoComplete` needs to be before `keybindings` so that arrow key handling + // NOTE: `autoComplete` needs to be before `keybindings` so that arrow key handling // in the autocomplete pop-up takes precedence over Vim/Emacs key bindings autoComplete(options.settings), - // `keybindings` needs to be before `language` so that Vim/Emacs bindings take + // NOTE: `keybindings` needs to be before `language` so that Vim/Emacs bindings take // precedence over language-specific keyboard shortcuts keybindings(), - annotations(), // NOTE: must be before `language` + // NOTE: `annotations` needs to be before `language` + annotations(), language(options.currentDoc, options.metadata, options.settings), theme(options.theme), realtime(options.currentDoc, options.handleError), @@ -107,15 +114,19 @@ export const createExtensions = (options: Record): Extension[] => [ spelling(options.spelling), shortcuts, symbolPalette(), - emptyLineFiller(), // NOTE: must be before `trackChanges` + // NOTE: `emptyLineFiller` needs to be before `trackChanges`, + // so the decorations are added in the correct order. + emptyLineFiller(), trackChanges(options.currentDoc, options.changeManager), visual(options.currentDoc, options.visual), verticalOverflow(), highlightActiveLine(options.visual.visual), + // The built-in extension that highlights the active line in the gutter. highlightActiveLineGutter(), inlineBackground(options.visual.visual), codemirrorDevTools(), exceptionLogger(), + // CodeMirror extensions provided by modules moduleExtensions.map(extension => extension()), thirdPartyExtensions(), effectListeners(), diff --git a/services/web/frontend/js/features/source-editor/extensions/inline-background.ts b/services/web/frontend/js/features/source-editor/extensions/inline-background.ts index d95d80ef06..52b28a0b46 100644 --- a/services/web/frontend/js/features/source-editor/extensions/inline-background.ts +++ b/services/web/frontend/js/features/source-editor/extensions/inline-background.ts @@ -43,6 +43,10 @@ function createTheme(halfLeading: number) { }) } +/** + * A custom extension which measures the height of the first non-space position and provides a CSS variable via an editor theme, + * used for extending elements over the whole line height using padding. + */ const plugin = ViewPlugin.define( view => { let halfLeading = 0 diff --git a/services/web/frontend/js/features/source-editor/extensions/keybindings.ts b/services/web/frontend/js/features/source-editor/extensions/keybindings.ts index c2f8a62169..2b0adfd7ad 100644 --- a/services/web/frontend/js/features/source-editor/extensions/keybindings.ts +++ b/services/web/frontend/js/features/source-editor/extensions/keybindings.ts @@ -210,6 +210,11 @@ const options = [ const keybindingsConf = new Compartment() +/** + * Third-party extensions providing Emacs and Vim keybindings, + * implemented as wrappers around the CodeMirror 5 interface, + * with some customisation (particularly related to search). + */ export const keybindings = () => { return keybindingsConf.of(Prec.highest([])) } diff --git a/services/web/frontend/js/features/source-editor/extensions/keymaps.ts b/services/web/frontend/js/features/source-editor/extensions/keymaps.ts index 99431f0f1c..7dfaee8d02 100644 --- a/services/web/frontend/js/features/source-editor/extensions/keymaps.ts +++ b/services/web/frontend/js/features/source-editor/extensions/keymaps.ts @@ -37,9 +37,14 @@ const filteredDefaultKeymap = defaultKeymap.filter( ) export const keymaps = keymap.of([ + // The default CodeMirror keymap, with a few key bindings filtered out. ...filteredDefaultKeymap, + // Key bindings for undo/redo/undoSelection/redoSelection ...historyKeymap, + // Key bindings for “open lint panel” and “next diagnostic” ...lintKeymap, + // Key bindings for folding actions ...foldingKeymap, + // Key bindings for scrolling the viewport ...scrollOneLineKeymap, ]) diff --git a/services/web/frontend/js/features/source-editor/extensions/language.ts b/services/web/frontend/js/features/source-editor/extensions/language.ts index c6db122cac..a03593bdb7 100644 --- a/services/web/frontend/js/features/source-editor/extensions/language.ts +++ b/services/web/frontend/js/features/source-editor/extensions/language.ts @@ -20,6 +20,9 @@ type Options = { syntaxValidation: boolean } +/** + * A state field that stores the metadata parsed from a project on the server. + */ export const metadataState = StateField.define({ create: () => undefined, update: (value, transaction) => { @@ -32,6 +35,10 @@ export const metadataState = StateField.define({ }, }) +/** + * The parser and support extensions for each supported language, + * which are loaded dynamically as needed. + */ export const language = ( currentDoc: CurrentDoc, metadata: Metadata, @@ -47,10 +54,16 @@ export const language = ( } return [ - // Default to four-space indentation, which prevents a shift in line - // indentation markers when LaTeX loads + /** + * Default to four-space indentation and set the configuration in advance, + * to prevent a shift in line indentation markers when the LaTeX language loads. + */ languageConf.of(indentUnit.of(' ')), metadataState, + /** + * A view plugin which loads the appropriate language for the current file extension, + * then dispatches an effect so other extensions can update accordingly. + */ ViewPlugin.define(view => { // load the language asynchronously languageDescription.load().then(support => { diff --git a/services/web/frontend/js/features/source-editor/extensions/line-numbers.ts b/services/web/frontend/js/features/source-editor/extensions/line-numbers.ts index ec90f7ab79..5768e6876b 100644 --- a/services/web/frontend/js/features/source-editor/extensions/line-numbers.ts +++ b/services/web/frontend/js/features/source-editor/extensions/line-numbers.ts @@ -2,10 +2,15 @@ import { EditorSelection, Extension } from '@codemirror/state' import { BlockInfo, EditorView, - lineNumbers as cmLineNumbers, + lineNumbers as _lineNumbers, } from '@codemirror/view' import { DebouncedFunc, throttle } from 'lodash' +/** + * The built-in extension which displays line numbers in the gutter, + * configured with a mousedown/mouseup handler that selects lines of the document + * when dragging a selection in the gutter. + */ export function lineNumbers(): Extension { let listener: DebouncedFunc<(event: MouseEvent) => boolean> | null @@ -37,7 +42,7 @@ export function lineNumbers(): Extension { } // Wrapper around the built-in codemirror lineNumbers() extension - return cmLineNumbers({ + return _lineNumbers({ domEventHandlers: { mousedown: (view, line, event) => { // Disable default focusing of line number diff --git a/services/web/frontend/js/features/source-editor/extensions/line-wrapping-indentation.ts b/services/web/frontend/js/features/source-editor/extensions/line-wrapping-indentation.ts index fc79588ef4..38dd85b51a 100644 --- a/services/web/frontend/js/features/source-editor/extensions/line-wrapping-indentation.ts +++ b/services/web/frontend/js/features/source-editor/extensions/line-wrapping-indentation.ts @@ -12,6 +12,12 @@ const MAX_INDENT_FRACTION = 0.9 const setMaxIndentEffect = StateEffect.define() +/** + * Calculates the indentation needed for each line based on the spaces and tabs at the start of the line. + * Decorates each line with a `style` attribute, to ensure that wrapped lines share the same indentation. + * + * NOTE: performance increases linearly with document size, but decorating only the viewport causes unacceptable layout shifts when scrolling. + */ export const lineWrappingIndentation = (visual: boolean) => { // this extension should only be active in the source editor return sourceOnly(visual, [ diff --git a/services/web/frontend/js/features/source-editor/extensions/phrases.ts b/services/web/frontend/js/features/source-editor/extensions/phrases.ts index ad7b11f47d..ddcb0aa8ec 100644 --- a/services/web/frontend/js/features/source-editor/extensions/phrases.ts +++ b/services/web/frontend/js/features/source-editor/extensions/phrases.ts @@ -2,6 +2,9 @@ import { Compartment, EditorState, TransactionSpec } from '@codemirror/state' const phrasesConf = new Compartment() +/** + * A built-in extension providing a mapping between translation keys and translations. + */ export const phrases = (phrases: Record) => { return phrasesConf.of(EditorState.phrases.of(phrases)) } diff --git a/services/web/frontend/js/features/source-editor/extensions/realtime.ts b/services/web/frontend/js/features/source-editor/extensions/realtime.ts index 6fabd6d653..8d5be212c2 100644 --- a/services/web/frontend/js/features/source-editor/extensions/realtime.ts +++ b/services/web/frontend/js/features/source-editor/extensions/realtime.ts @@ -20,6 +20,9 @@ import { ShareDoc } from '../../../../../types/share-doc' * - frontend/js/ide/connection/EditorWatchdogManager.js */ +/** + * A custom extension that connects the CodeMirror 6 editor to the currently open ShareJS document. + */ export const realtime = ( { currentDoc }: { currentDoc: CurrentDoc }, handleError: (error: Error) => void diff --git a/services/web/frontend/js/features/source-editor/extensions/scroll-one-line.ts b/services/web/frontend/js/features/source-editor/extensions/scroll-one-line.ts index eb46c48f04..284b7676c1 100644 --- a/services/web/frontend/js/features/source-editor/extensions/scroll-one-line.ts +++ b/services/web/frontend/js/features/source-editor/extensions/scroll-one-line.ts @@ -18,7 +18,9 @@ const scrollDownOneLine: Command = (view: EditorView) => { return true } -// Applied to Windows and Linux only +/** + * Custom keymap for Windows and Linux for scrolling the viewport up/down one line with Ctrl+ArrowUp/Down. + */ export const scrollOneLineKeymap = [ { linux: 'Ctrl-ArrowUp', diff --git a/services/web/frontend/js/features/source-editor/extensions/scroll-position.ts b/services/web/frontend/js/features/source-editor/extensions/scroll-position.ts index ac73af4d58..132481e5a3 100644 --- a/services/web/frontend/js/features/source-editor/extensions/scroll-position.ts +++ b/services/web/frontend/js/features/source-editor/extensions/scroll-position.ts @@ -16,6 +16,12 @@ type LineInfo = { middle: BlockInfo } +/** + * A custom extension that: + * a) stores the scroll position (first visible line number) in localStorage when the view is destroyed, + * or the window is closed, or when switching between Source and Rich Text, and + * b) dispatches the scroll position (middle visible line) when it changes, for use in the outline. + */ export const scrollPosition = ({ currentDoc: { doc_id: docId }, }: { diff --git a/services/web/frontend/js/features/source-editor/extensions/search.ts b/services/web/frontend/js/features/source-editor/extensions/search.ts index cf8f84a0e4..e08bdfefed 100644 --- a/services/web/frontend/js/features/source-editor/extensions/search.ts +++ b/services/web/frontend/js/features/source-editor/extensions/search.ts @@ -1,5 +1,5 @@ import { - search as searchExtension, + search as _search, setSearchQuery, getSearchQuery, openSearchPanel, @@ -105,6 +105,9 @@ const highlightSelectionMatchesExtension = highlightSelectionMatches({ // TODO: move this into EditorContext? let searchQuery: SearchQuery | null +/** + * A collection of extensions related to the search feature. + */ export const search = () => { let open = false @@ -118,8 +121,11 @@ export const search = () => { // a stored selection for use in "within selection" searches storedSelectionState, - // a wrapper round `search`, which creates a custom panel element and passes it to React by dispatching an event - searchExtension({ + /** + * The CodeMirror `search` extension, configured to create a custom panel element + * and to scroll the search match into the centre of the viewport when needed. + */ + _search({ literal: true, // centre the search match if it was outside the visible area scrollToMatch: (range: SelectionRange, view: EditorView) => { diff --git a/services/web/frontend/js/features/source-editor/extensions/shortcuts.ts b/services/web/frontend/js/features/source-editor/extensions/shortcuts.ts index 01c3bb7de2..c4a8c4ddff 100644 --- a/services/web/frontend/js/features/source-editor/extensions/shortcuts.ts +++ b/services/web/frontend/js/features/source-editor/extensions/shortcuts.ts @@ -39,6 +39,9 @@ const toggleTrackChangesFromKbdShortcut = () => { return true } +/** + * Custom key bindings for motion, transformation, selection, history, etc. + */ export const shortcuts = Prec.high( keymap.of([ { diff --git a/services/web/frontend/js/features/source-editor/extensions/spelling/index.ts b/services/web/frontend/js/features/source-editor/extensions/spelling/index.ts index bdbb669e8e..3d32a350cc 100644 --- a/services/web/frontend/js/features/source-editor/extensions/spelling/index.ts +++ b/services/web/frontend/js/features/source-editor/extensions/spelling/index.ts @@ -22,8 +22,12 @@ const spellCheckLanguageFacet = Facet.define() type Options = { spellCheckLanguage?: string } -/* - * Create the spelling extensions array, based on options passed. +/** + * A custom extension that creates a spell checker for the current language (from the user settings). + * The spell check runs on the server whenever a line changes. + * The mis-spelled words, ignored words and spell-checked words are stored in a state field. + * Mis-spelled words are decorated with a Mark decoration. + * The suggestions menu is displayed in a tooltip, activated with a right-click on the decoration. */ export const spelling = ({ spellCheckLanguage }: Options) => { return [ diff --git a/services/web/frontend/js/features/source-editor/extensions/symbol-palette.ts b/services/web/frontend/js/features/source-editor/extensions/symbol-palette.ts index 1eb7705938..b23b7e05b1 100644 --- a/services/web/frontend/js/features/source-editor/extensions/symbol-palette.ts +++ b/services/web/frontend/js/features/source-editor/extensions/symbol-palette.ts @@ -1,6 +1,9 @@ import { ViewPlugin } from '@codemirror/view' import { EditorSelection } from '@codemirror/state' +/** + * A custom extension that listens for an `editor:insert-symbol` event and inserts the given content into the document. + */ export const symbolPalette = () => { return ViewPlugin.define(view => { const listener = (event: Event) => { diff --git a/services/web/frontend/js/features/source-editor/extensions/theme.ts b/services/web/frontend/js/features/source-editor/extensions/theme.ts index 5ca7d2456e..ac59bfe9c5 100644 --- a/services/web/frontend/js/features/source-editor/extensions/theme.ts +++ b/services/web/frontend/js/features/source-editor/extensions/theme.ts @@ -21,6 +21,9 @@ type Options = { export const theme = (options: Options) => [ baseTheme, staticTheme, + /** + * Syntax highlighting, using a highlighter which maps tags to class names. + */ syntaxHighlighting(classHighlighter), optionsThemeConf.of(createThemeFromOptions(options)), selectedThemeConf.of([]), @@ -65,7 +68,9 @@ const createThemeFromOptions = ({ lineHeight = 'normal', overallTheme = '', }: Options) => { - // theme styles that depend on settings + /** + * Theme styles that depend on settings. + */ return [ EditorView.editorAttributes.of({ class: overallTheme === '' ? 'overall-theme-dark' : 'overall-theme-light', @@ -112,7 +117,9 @@ const createThemeFromOptions = ({ ] } -// base styles that can have &dark and &light variants +/** + * Base styles that can have &dark and &light variants + */ const baseTheme = EditorView.baseTheme({ // use a background color for lint error ranges '.cm-lintRange-error': { @@ -146,7 +153,9 @@ const baseTheme = EditorView.baseTheme({ }, }) -// theme styles that don't depend on settings +/** + * Theme styles that don't depend on settings. + */ // TODO: move some/all of these into baseTheme? const staticTheme = EditorView.theme({ // make the editor fill the available height diff --git a/services/web/frontend/js/features/source-editor/extensions/third-party-extensions.ts b/services/web/frontend/js/features/source-editor/extensions/third-party-extensions.ts index 4bd74c8e6a..4944ea11e6 100644 --- a/services/web/frontend/js/features/source-editor/extensions/third-party-extensions.ts +++ b/services/web/frontend/js/features/source-editor/extensions/third-party-extensions.ts @@ -1,6 +1,10 @@ import type { Extension } from '@codemirror/state' import CodeMirror, { CodeMirrorVim } from './bundle' +/** + * A custom extension that allows additional CodeMirror extensions to be provided by external code, + * e.g. browser extensions. + */ export const thirdPartyExtensions = (): Extension => { const extensions: Extension[] = [] diff --git a/services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts b/services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts index a77eee2cba..adc3543855 100644 --- a/services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts +++ b/services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts @@ -24,6 +24,10 @@ export function createToolbarPanel() { return { dom, top: true } } +/** + * A panel which contains the editor toolbar, provided by a state field which allows the toolbar to be toggled, + * and styles for the toolbar. + */ export const toolbarPanel = () => [ toolbarState, EditorView.theme({ diff --git a/services/web/frontend/js/features/source-editor/extensions/track-changes.ts b/services/web/frontend/js/features/source-editor/extensions/track-changes.ts index e8533a1a62..1cea83d527 100644 --- a/services/web/frontend/js/features/source-editor/extensions/track-changes.ts +++ b/services/web/frontend/js/features/source-editor/extensions/track-changes.ts @@ -50,6 +50,10 @@ type Options = { loadingThreads: boolean } +/** + * A custom extension that initialises the change manager, passes any updates to it, + * and produces decorations for tracked changes and comments. + */ export const trackChanges = ( { currentDoc, loadingThreads }: Options, changeManager: ChangeManager diff --git a/services/web/frontend/js/features/source-editor/extensions/vertical-overflow.ts b/services/web/frontend/js/features/source-editor/extensions/vertical-overflow.ts index e17d82edaf..134c8cee60 100644 --- a/services/web/frontend/js/features/source-editor/extensions/vertical-overflow.ts +++ b/services/web/frontend/js/features/source-editor/extensions/vertical-overflow.ts @@ -13,6 +13,11 @@ import { WidgetType, } from '@codemirror/view' +/** + * A custom extension which stores values for padding needed + * a) at the top and bottom of the editor, to match the height of the review panel, and + * b) at the bottom of the editor content, so the last line of the document can be scrolled to the top of the editor. + */ export function verticalOverflow(): Extension { return [ overflowPaddingState, diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts b/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts index 6f6374a04b..c71173b0af 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts +++ b/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts @@ -109,6 +109,7 @@ const hasClosingBrace = (node: SyntaxNode) => node.getChild('EnvNameGroup')?.getChild('CloseBrace') /** + * A state field that decorates ranges of text (including multiple lines) with Widget or Line decorations. * Atomic decorations replace a range of content with an uneditable widget. * Decorations that span multiple lines must be contained in a StateField, not a ViewPlugin. */ diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/list-item-marker.ts b/services/web/frontend/js/features/source-editor/extensions/visual/list-item-marker.ts index d12dc7ea84..e9bbfcb35d 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/list-item-marker.ts +++ b/services/web/frontend/js/features/source-editor/extensions/visual/list-item-marker.ts @@ -7,7 +7,10 @@ import { import { syntaxTree } from '@codemirror/language' import { SyntaxNode } from '@lezer/common' -// avoid placing the cursor in front of a list item marker +/** + * A transaction filter which modifies a transaction if it places the cursor in front of a list item marker, + * to ensure that the cursor is positioned after the marker. + */ export const listItemMarker = EditorState.transactionFilter.of(tr => { if (tr.selection) { let selection = tr.selection diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/mark-decorations.ts b/services/web/frontend/js/features/source-editor/extensions/visual/mark-decorations.ts index 26f69a8ad8..dce01251da 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/mark-decorations.ts +++ b/services/web/frontend/js/features/source-editor/extensions/visual/mark-decorations.ts @@ -11,6 +11,7 @@ import { centeringNodeForEnvironment } from '../../utils/tree-operations/figure' import { Tree } from '@lezer/common' /** + * A view plugin that decorates ranges of text with Mark decorations. * Mark decorations add attributes to elements within a range. */ export const markDecorations = ViewPlugin.define( diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/selection.ts b/services/web/frontend/js/features/source-editor/extensions/visual/selection.ts index 92a964b6b8..9d7074b55b 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/selection.ts +++ b/services/web/frontend/js/features/source-editor/extensions/visual/selection.ts @@ -57,6 +57,10 @@ export const mouseDownEffect = StateEffect.define() export const hasMouseDownEffect = hasEffect(mouseDownEffect) export const updateHasMouseDownEffect = updateHasEffect(mouseDownEffect) +/** + * A listener for mousedown and mouseup events, dispatching an event + * to record the current mousedown status, which is stored in a state field. + */ export const mouseDownListener = EditorView.domEventHandlers({ mousedown: (event, view) => { // not wrapped in a timeout, so update listeners know that the mouse is down before they process the selection diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/skip-preamble-cursor.ts b/services/web/frontend/js/features/source-editor/extensions/visual/skip-preamble-cursor.ts index 9fd64d4a3e..36840e9c04 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/skip-preamble-cursor.ts +++ b/services/web/frontend/js/features/source-editor/extensions/visual/skip-preamble-cursor.ts @@ -3,6 +3,10 @@ import { EditorSelection } from '@codemirror/state' import { findStartOfDocumentContent } from '../../utils/tree-operations/environments' import { syntaxTree } from '@codemirror/language' import { extendForwardsOverEmptyLines } from './selection' + +/** + * A view plugin that moves the cursor from the start of the preamble into the document body when the doc is opened. + */ export const skipPreambleWithCursor = ViewPlugin.define((view: EditorView) => { let checkedOnce = false return { diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/visual-keymap.ts b/services/web/frontend/js/features/source-editor/extensions/visual/visual-keymap.ts index b1f4384e35..33c1991dc5 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/visual-keymap.ts +++ b/services/web/frontend/js/features/source-editor/extensions/visual/visual-keymap.ts @@ -13,6 +13,10 @@ import { indentIncrease, } from '../toolbar/commands' +/** + * A keymap which provides behaviours for the visual editor, + * including lists and text formatting. + */ export const visualKeymap = Prec.highest( keymap.of([ // create a new list item with the same indentation diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/visual-theme.ts b/services/web/frontend/js/features/source-editor/extensions/visual/visual-theme.ts index 4768201918..acc656dbd2 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/visual-theme.ts +++ b/services/web/frontend/js/features/source-editor/extensions/visual/visual-theme.ts @@ -2,6 +2,9 @@ import { EditorView } from '@codemirror/view' import { HighlightStyle, syntaxHighlighting } from '@codemirror/language' import { tags } from '@lezer/highlight' +/** + * A syntax highlighter for content types that are only styled in the visual editor. + */ export const visualHighlightStyle = syntaxHighlighting( HighlightStyle.define([ { tag: tags.link, class: 'ol-cm-link-text' }, diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts b/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts index 50d8985309..5eafc262b0 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts +++ b/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts @@ -110,6 +110,11 @@ export const sourceOnly = (visual: boolean, extension: Extension) => { } const parsedAttributesConf = new Compartment() + +/** + * A view plugin which shows the editor content, makes it focusable, + * and restores the scroll position, once the initial decorations have been applied. + */ const showContentWhenParsed = [ parsedAttributesConf.of([EditorView.editable.of(false)]), ViewPlugin.define(view => { @@ -164,6 +169,9 @@ const showContentWhenParsed = [ }), ] +/** + * A transaction extender which scrolls mouse clicks into view, in case decorations have moved the cursor out of view. + */ const scrollJumpAdjuster = EditorState.transactionExtender.of(tr => { // Attach a "scrollIntoView" effect on all mouse selections to adjust for // any jumps that may occur when hiding/showing decorations. diff --git a/services/web/frontend/js/features/source-editor/extensions/wait-for-parser.ts b/services/web/frontend/js/features/source-editor/extensions/wait-for-parser.ts index 96c754edec..95036edd8e 100644 --- a/services/web/frontend/js/features/source-editor/extensions/wait-for-parser.ts +++ b/services/web/frontend/js/features/source-editor/extensions/wait-for-parser.ts @@ -14,6 +14,9 @@ type ParserWait = { resolve: () => void } +/** + * A custom extension that resolves a Promise when the parser has built the syntax tree up to a given position. + */ export const parserWatcher = ViewPlugin.fromClass( class { waits: ParserWait[] = [] diff --git a/services/web/frontend/js/features/source-editor/languages/latex/codemirror-dev-tools.ts b/services/web/frontend/js/features/source-editor/languages/latex/codemirror-dev-tools.ts index ea77f0bbdd..8e27e19130 100644 --- a/services/web/frontend/js/features/source-editor/languages/latex/codemirror-dev-tools.ts +++ b/services/web/frontend/js/features/source-editor/languages/latex/codemirror-dev-tools.ts @@ -24,6 +24,10 @@ const enabled = customLocalStorage.getItem('cm6-dev-tools') === 'on' const devToolsConf = new Compartment() +/** + * A panel which displays an outline of the current document's syntax tree alongside the document, + * to assist with CodeMirror extension development. + */ export const codemirrorDevTools = () => { return enabled ? [devToolsButton, devToolsConf.of(createExtension())] : [] }