Merge pull request #13241 from overleaf/ae-extensions-documentation
Add documentation for CodeMirror extensions GitOrigin-RevId: e5f07084173f201919272f9d46dcdaef4b817874
This commit is contained in:
@@ -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>
|
||||
>({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
}: {
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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[]>({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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)) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) })
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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, [
|
||||
|
||||
@@ -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'
|
||||
),
|
||||
})
|
||||
)
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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([]))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
])
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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
@@ -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, [
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 },
|
||||
}: {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -39,6 +39,9 @@ const toggleTrackChangesFromKbdShortcut = () => {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom key bindings for motion, transformation, selection, history, etc.
|
||||
*/
|
||||
export const shortcuts = Prec.high(
|
||||
keymap.of([
|
||||
{
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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[] = []
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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())] : []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user