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

Add documentation for CodeMirror extensions

GitOrigin-RevId: e5f07084173f201919272f9d46dcdaef4b817874
This commit is contained in:
Alf Eaton
2023-07-17 10:28:53 +00:00
committed by Copybot
parent 05671f10d4
commit e025088065
48 changed files with 282 additions and 38 deletions
@@ -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 theres 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
@@ -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,
@@ -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.
*/
@@ -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
@@ -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[] = []
@@ -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())] : []
}