Просмотр исходного кода

Merge pull request #13241 from overleaf/ae-extensions-documentation

Add documentation for CodeMirror extensions

GitOrigin-RevId: e5f07084173f201919272f9d46dcdaef4b817874
Alf Eaton 3 лет назад
Родитель
Сommit
e025088065
48 измененных файлов с 282 добавлено и 38 удалено
  1. 14 2
      services/web/frontend/js/features/source-editor/extensions/annotations.ts
  2. 14 1
      services/web/frontend/js/features/source-editor/extensions/auto-complete.ts
  3. 3 0
      services/web/frontend/js/features/source-editor/extensions/auto-pair.ts
  4. 10 2
      services/web/frontend/js/features/source-editor/extensions/bracket-matching.ts
  5. 3 0
      services/web/frontend/js/features/source-editor/extensions/cursor-highlights.ts
  6. 5 0
      services/web/frontend/js/features/source-editor/extensions/cursor-position.ts
  7. 6 3
      services/web/frontend/js/features/source-editor/extensions/draw-selection.ts
  8. 5 0
      services/web/frontend/js/features/source-editor/extensions/editable.ts
  9. 4 0
      services/web/frontend/js/features/source-editor/extensions/effect-listeners.ts
  10. 4 0
      services/web/frontend/js/features/source-editor/extensions/empty-line-filler.ts
  11. 4 0
      services/web/frontend/js/features/source-editor/extensions/exception-logger.ts
  12. 3 0
      services/web/frontend/js/features/source-editor/extensions/filter-characters.ts
  13. 3 0
      services/web/frontend/js/features/source-editor/extensions/folding-keymap.ts
  14. 4 0
      services/web/frontend/js/features/source-editor/extensions/font-load.ts
  15. 4 0
      services/web/frontend/js/features/source-editor/extensions/go-to-line.ts
  16. 4 0
      services/web/frontend/js/features/source-editor/extensions/highlight-active-line.ts
  17. 18 0
      services/web/frontend/js/features/source-editor/extensions/highlight-special-chars.ts
  18. 4 0
      services/web/frontend/js/features/source-editor/extensions/indentation-markers.ts
  19. 27 16
      services/web/frontend/js/features/source-editor/extensions/index.ts
  20. 4 0
      services/web/frontend/js/features/source-editor/extensions/inline-background.ts
  21. 5 0
      services/web/frontend/js/features/source-editor/extensions/keybindings.ts
  22. 5 0
      services/web/frontend/js/features/source-editor/extensions/keymaps.ts
  23. 15 2
      services/web/frontend/js/features/source-editor/extensions/language.ts
  24. 7 2
      services/web/frontend/js/features/source-editor/extensions/line-numbers.ts
  25. 6 0
      services/web/frontend/js/features/source-editor/extensions/line-wrapping-indentation.ts
  26. 3 0
      services/web/frontend/js/features/source-editor/extensions/phrases.ts
  27. 3 0
      services/web/frontend/js/features/source-editor/extensions/realtime.ts
  28. 3 1
      services/web/frontend/js/features/source-editor/extensions/scroll-one-line.ts
  29. 6 0
      services/web/frontend/js/features/source-editor/extensions/scroll-position.ts
  30. 9 3
      services/web/frontend/js/features/source-editor/extensions/search.ts
  31. 3 0
      services/web/frontend/js/features/source-editor/extensions/shortcuts.ts
  32. 6 2
      services/web/frontend/js/features/source-editor/extensions/spelling/index.ts
  33. 3 0
      services/web/frontend/js/features/source-editor/extensions/symbol-palette.ts
  34. 12 3
      services/web/frontend/js/features/source-editor/extensions/theme.ts
  35. 4 0
      services/web/frontend/js/features/source-editor/extensions/third-party-extensions.ts
  36. 4 0
      services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts
  37. 4 0
      services/web/frontend/js/features/source-editor/extensions/track-changes.ts
  38. 5 0
      services/web/frontend/js/features/source-editor/extensions/vertical-overflow.ts
  39. 1 0
      services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts
  40. 4 1
      services/web/frontend/js/features/source-editor/extensions/visual/list-item-marker.ts
  41. 1 0
      services/web/frontend/js/features/source-editor/extensions/visual/mark-decorations.ts
  42. 4 0
      services/web/frontend/js/features/source-editor/extensions/visual/selection.ts
  43. 4 0
      services/web/frontend/js/features/source-editor/extensions/visual/skip-preamble-cursor.ts
  44. 4 0
      services/web/frontend/js/features/source-editor/extensions/visual/visual-keymap.ts
  45. 3 0
      services/web/frontend/js/features/source-editor/extensions/visual/visual-theme.ts
  46. 8 0
      services/web/frontend/js/features/source-editor/extensions/visual/visual.ts
  47. 3 0
      services/web/frontend/js/features/source-editor/extensions/wait-for-parser.ts
  48. 4 0
      services/web/frontend/js/features/source-editor/languages/latex/codemirror-dev-tools.ts

+ 14 - 2
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<Diagnostic[]>()
 
+/**
+ * A state field for the compile log diagnostics
+ */
 export const compileDiagnosticsState = StateField.define<
   RangeSet<DiagnosticRangeValue>
 >({

+ 14 - 1
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

+ 3 - 0
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

+ 10 - 2
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
       }

+ 3 - 0
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,

+ 5 - 0
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 },
 }: {

+ 6 - 3
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]
 }

+ 5 - 0
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([

+ 4 - 0
services/web/frontend/js/features/source-editor/extensions/effect-listeners.ts

@@ -14,6 +14,10 @@ type EffectListener = {
 const addEffectListenerEffect = StateEffect.define<EffectListener>()
 const removeEffectListenerEffect = StateEffect.define<EffectListener>()
 
+/**
+ * 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<EffectListener[]>({

+ 4 - 0
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

+ 4 - 0
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, {

+ 3 - 0
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)) {

+ 3 - 0
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',

+ 4 - 0
services/web/frontend/js/features/source-editor/extensions/font-load.ts

@@ -5,6 +5,10 @@ import { updateHasEffect } from '../utils/effects'
 const fontLoadEffect = StateEffect.define<readonly FontFace[]>()
 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) })

+ 4 - 0
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(

+ 4 - 0
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, [

+ 18 - 0
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'
+      ),
+    })
+  )

+ 4 - 0
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 }),

+ 27 - 16
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<string, any>): 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<string, any>): 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<string, any>): 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(),

+ 4 - 0
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

+ 5 - 0
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([]))
 }

+ 5 - 0
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,
 ])

+ 15 - 2
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<Metadata | undefined>({
   create: () => undefined,
   update: (value, transaction) => {
@@ -32,6 +35,10 @@ export const metadataState = StateField.define<Metadata | undefined>({
   },
 })
 
+/**
+ * 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 => {

+ 7 - 2
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

+ 6 - 0
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<number>()
 
+/**
+ * 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, [

+ 3 - 0
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<string, string>) => {
   return phrasesConf.of(EditorState.phrases.of(phrases))
 }

+ 3 - 0
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

+ 3 - 1
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',

+ 6 - 0
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 },
 }: {

+ 9 - 3
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) => {

+ 3 - 0
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([
     {

+ 6 - 2
services/web/frontend/js/features/source-editor/extensions/spelling/index.ts

@@ -22,8 +22,12 @@ const spellCheckLanguageFacet = Facet.define<string | undefined>()
 
 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 [

+ 3 - 0
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) => {

+ 12 - 3
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

+ 4 - 0
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[] = []
 

+ 4 - 0
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({

+ 4 - 0
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

+ 5 - 0
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,

+ 1 - 0
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.
  */

+ 4 - 1
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

+ 1 - 0
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(

+ 4 - 0
services/web/frontend/js/features/source-editor/extensions/visual/selection.ts

@@ -57,6 +57,10 @@ export const mouseDownEffect = StateEffect.define<boolean>()
 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

+ 4 - 0
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 {

+ 4 - 0
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

+ 3 - 0
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' },

+ 8 - 0
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.

+ 3 - 0
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[] = []

+ 4 - 0
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())] : []
 }