chrome-devtools-frontend
Version:
Chrome DevTools UI
114 lines (100 loc) • 4.05 kB
text/typescript
// Copyright 2024 The Chromium Authors. All rights reserved.
// 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),
];
}