| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import * as CodeMirror from '../../../third_party/codemirror.next/codemirror.next.js'; |
| |
| /** |
| * The CodeMirror effect used to change the highlighted execution position. |
| * |
| * Usage: |
| * ```js |
| * view.dispatch({effects: setHighlightedPosition.of(position)}); |
| * ``` |
| */ |
| export const setHighlightedPosition = CodeMirror.StateEffect.define<number>(); |
| |
| /** |
| * The CodeMirror effect used to clear the highlighted execution position. |
| * |
| * Usage: |
| * ```js |
| * view.dispatch({effects: clearHighlightedPosition.of()}); |
| * ``` |
| */ |
| export const clearHighlightedPosition = CodeMirror.StateEffect.define<void>(); |
| |
| /** |
| * Constructs a CodeMirror extension that can be used to decorate the current execution |
| * line (and token), for example when the debugger is paused, with specific CSS classes. |
| * |
| * @param executionLineClassName The CSS class name to use for decorating the execution line (e.g. `'cm-executionLine'`). |
| * @param executionTokenClassName The CSS class name to use for decorating the execution token (e.g. `'cm-executionToken'`). |
| * @returns a CodeMirror extension that highlights the current execution line and token when set. |
| */ |
| export function positionHighlighter( |
| executionLineClassName: string, |
| executionTokenClassName: string, |
| ): CodeMirror.Extension { |
| const executionLine = CodeMirror.Decoration.line({attributes: {class: executionLineClassName}}); |
| const executionToken = CodeMirror.Decoration.mark({attributes: {class: executionTokenClassName}}); |
| |
| const positionHighlightedState = CodeMirror.StateField.define<null|number>({ |
| create() { |
| return null; |
| }, |
| |
| update(pos, tr) { |
| if (pos) { |
| pos = tr.changes.mapPos(pos, -1, CodeMirror.MapMode.TrackDel); |
| } |
| for (const effect of tr.effects) { |
| if (effect.is(clearHighlightedPosition)) { |
| pos = null; |
| } else if (effect.is(setHighlightedPosition)) { |
| pos = Math.max(0, Math.min(effect.value, tr.newDoc.length - 1)); |
| } |
| } |
| return pos; |
| }, |
| }); |
| |
| function getHighlightedPosition(state: CodeMirror.EditorState): null|number { |
| return state.field(positionHighlightedState); |
| } |
| |
| class PositionHighlighter { |
| tree: CodeMirror.Tree; |
| decorations: CodeMirror.DecorationSet; |
| |
| constructor({state}: CodeMirror.EditorView) { |
| this.tree = CodeMirror.syntaxTree(state); |
| this.decorations = this.#computeDecorations(state, getHighlightedPosition(state)); |
| } |
| |
| update(update: CodeMirror.ViewUpdate): void { |
| const tree = CodeMirror.syntaxTree(update.state); |
| const position = getHighlightedPosition(update.state); |
| const positionChanged = position !== getHighlightedPosition(update.startState); |
| if (tree.length !== this.tree.length || positionChanged) { |
| this.tree = tree; |
| this.decorations = this.#computeDecorations(update.state, position); |
| } else { |
| this.decorations = this.decorations.map(update.changes); |
| } |
| } |
| |
| #computeDecorations(state: CodeMirror.EditorState, position: null|number): CodeMirror.DecorationSet { |
| const builder = new CodeMirror.RangeSetBuilder<CodeMirror.Decoration>(); |
| if (position !== null) { |
| const {doc} = state; |
| const line = doc.lineAt(position); |
| builder.add(line.from, line.from, executionLine); |
| const syntaxTree = CodeMirror.syntaxTree(state); |
| const syntaxNode = syntaxTree.resolveInner(position, 1); |
| const tokenEnd = Math.min(line.to, syntaxNode.to); |
| if (tokenEnd > position) { |
| builder.add(position, tokenEnd, executionToken); |
| } |
| } |
| return builder.finish(); |
| } |
| } |
| |
| const positionHighlighterSpec = { |
| decorations: ({decorations}: PositionHighlighter) => decorations, |
| }; |
| |
| return [ |
| positionHighlightedState, |
| CodeMirror.ViewPlugin.fromClass(PositionHighlighter, positionHighlighterSpec), |
| ]; |
| } |