@jupyterlab/debugger
Version:
JupyterLab - Debugger Extension
340 lines • 12 kB
JavaScript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { ActivityMonitor } from '@jupyterlab/coreutils';
import { Signal } from '@lumino/signaling';
import { Compartment, Prec, RangeSet, StateEffect, StateField } from '@codemirror/state';
import { Decoration, EditorView, gutter, GutterMarker } from '@codemirror/view';
/**
* The class name added to the current line.
*/
const LINE_HIGHLIGHT_CLASS = 'jp-DebuggerEditor-highlight';
/**
* The timeout for listening to editor content changes.
*/
const EDITOR_CHANGED_TIMEOUT = 1000;
/**
* A handler for a CodeEditor.IEditor.
*/
export class EditorHandler {
/**
* Instantiate a new EditorHandler.
*
* @param options The instantiation options for a EditorHandler.
*/
constructor(options) {
var _a, _b, _c, _d;
this._src = options.src;
this._id = (_c = (_b = (_a = options.debuggerService.session) === null || _a === void 0 ? void 0 : _a.connection) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : '';
this._path = (_d = options.path) !== null && _d !== void 0 ? _d : '';
this._debuggerService = options.debuggerService;
this._editor = options.getEditor;
this._editorMonitor = new ActivityMonitor({
signal: this._src.changed,
timeout: EDITOR_CHANGED_TIMEOUT
});
this._editorMonitor.activityStopped.connect(() => {
this._sendEditorBreakpoints();
}, this);
this._debuggerService.model.breakpoints.changed.connect(async () => {
const editor = this.editor;
if (!editor || editor.isDisposed) {
return;
}
this._addBreakpointsToEditor();
});
this._debuggerService.model.breakpoints.restored.connect(async () => {
const editor = this.editor;
if (!editor || editor.isDisposed) {
return;
}
this._addBreakpointsToEditor();
});
this._debuggerService.model.callstack.currentFrameChanged.connect(() => {
const editor = this.editor;
if (editor) {
EditorHandler.clearHighlight(editor);
}
});
this._breakpointEffect = StateEffect.define({
map: (value, mapping) => ({ pos: value.pos.map(v => mapping.mapPos(v)) })
});
this._breakpointState = StateField.define({
create: () => {
return RangeSet.empty;
},
update: (breakpoints, transaction) => {
breakpoints = breakpoints.map(transaction.changes);
for (let ef of transaction.effects) {
if (ef.is(this._breakpointEffect)) {
let e = ef;
if (e.value.pos.length) {
breakpoints = breakpoints.update({
add: e.value.pos.map(v => Private.breakpointMarker.range(v)),
sort: true
});
}
else {
breakpoints = RangeSet.empty;
}
}
}
return breakpoints;
}
});
this._gutter = new Compartment();
this._highlightDeco = Decoration.line({ class: LINE_HIGHLIGHT_CLASS });
this._highlightState = StateField.define({
create: () => {
return Decoration.none;
},
update: (highlights, transaction) => {
highlights = highlights.map(transaction.changes);
for (let ef of transaction.effects) {
if (ef.is(EditorHandler._highlightEffect)) {
let e = ef;
if (e.value.pos.length) {
highlights = highlights.update({
add: e.value.pos.map(v => this._highlightDeco.range(v))
});
}
else {
highlights = Decoration.none;
}
}
}
return highlights;
},
provide: f => EditorView.decorations.from(f)
});
void options.editorReady().then(() => {
this._setupEditor();
});
}
/**
* The editor
*/
get editor() {
return this._editor();
}
/**
* Dispose the handler.
*/
dispose() {
if (this.isDisposed) {
return;
}
this._editorMonitor.dispose();
this._clearEditor();
this.isDisposed = true;
Signal.clearData(this);
}
/**
* Refresh the breakpoints display
*/
refreshBreakpoints() {
this._addBreakpointsToEditor();
}
/**
* Setup the editor.
*/
_setupEditor() {
const editor = this.editor;
if (!editor || editor.isDisposed) {
return;
}
editor.setOption('lineNumbers', true);
const breakpointGutter = [
this._breakpointState,
this._highlightState,
Prec.highest(gutter({
class: 'cm-breakpoint-gutter',
renderEmptyElements: true,
markers: v => v.state.field(this._breakpointState),
initialSpacer: () => Private.breakpointMarker,
domEventHandlers: {
mousedown: (view, line) => {
this._onGutterClick(view, line.from);
return true;
}
}
}))
];
editor.injectExtension(this._gutter.of(breakpointGutter));
this._addBreakpointsToEditor();
}
/**
* Clear the editor by removing visual elements and handlers.
*/
_clearEditor() {
const editor = this.editor;
if (!editor || editor.isDisposed) {
return;
}
EditorHandler.clearHighlight(editor);
this._clearGutter(editor);
editor.setOption('lineNumbers', false);
editor.editor.dispatch({
effects: this._gutter.reconfigure([])
});
}
/**
* Send the breakpoints from the editor UI via the debug service.
*/
_sendEditorBreakpoints() {
var _a;
if ((_a = this.editor) === null || _a === void 0 ? void 0 : _a.isDisposed) {
return;
}
const breakpoints = this._getBreakpointsFromEditor().map(lineNumber => {
var _a, _b;
return Private.createBreakpoint(((_b = (_a = this._debuggerService.session) === null || _a === void 0 ? void 0 : _a.connection) === null || _b === void 0 ? void 0 : _b.name) || '', lineNumber);
});
void this._debuggerService.updateBreakpoints(this._src.getSource(), breakpoints, this._path);
}
/**
* Handle a click on the gutter.
*
* @param editor The editor from where the click originated.
* @param position The position corresponding to the click event.
*/
_onGutterClick(editor, position) {
var _a, _b, _c;
if (this._id !== ((_b = (_a = this._debuggerService.session) === null || _a === void 0 ? void 0 : _a.connection) === null || _b === void 0 ? void 0 : _b.id)) {
return;
}
const lineNumber = editor.state.doc.lineAt(position).number;
let stateBreakpoints = editor.state.field(this._breakpointState);
let hasBreakpoint = false;
stateBreakpoints.between(position, position, () => {
hasBreakpoint = true;
});
let breakpoints = this._getBreakpoints();
if (hasBreakpoint) {
breakpoints = breakpoints.filter(ele => ele.line !== lineNumber);
}
else {
breakpoints.push(Private.createBreakpoint((_c = this._path) !== null && _c !== void 0 ? _c : this._debuggerService.session.connection.name, lineNumber));
}
breakpoints.sort((a, b) => {
return a.line - b.line;
});
void this._debuggerService.updateBreakpoints(this._src.getSource(), breakpoints, this._path);
}
/**
* Add the breakpoints to the editor.
*/
_addBreakpointsToEditor() {
var _a, _b;
if (this._id !== ((_b = (_a = this._debuggerService.session) === null || _a === void 0 ? void 0 : _a.connection) === null || _b === void 0 ? void 0 : _b.id)) {
return;
}
const editor = this.editor;
const breakpoints = this._getBreakpoints();
this._clearGutter(editor);
const breakpointPos = breakpoints.map(b => {
return editor.state.doc.line(b.line).from;
});
editor.editor.dispatch({
effects: this._breakpointEffect.of({ pos: breakpointPos })
});
}
/**
* Retrieve the breakpoints from the editor.
*/
_getBreakpointsFromEditor() {
const editor = this.editor;
const breakpoints = editor.editor.state.field(this._breakpointState);
let lines = [];
breakpoints.between(0, editor.doc.length, (from) => {
lines.push(editor.doc.lineAt(from).number);
});
return lines;
}
_clearGutter(editor) {
if (!editor) {
return;
}
const view = editor.editor;
view.dispatch({
effects: this._breakpointEffect.of({ pos: [] })
});
}
/**
* Get the breakpoints for the editor using its content (code),
* or its path (if it exists).
*/
_getBreakpoints() {
const code = this._src.getSource();
return this._debuggerService.model.breakpoints.getBreakpoints(this._path || this._debuggerService.getCodeId(code));
}
}
/**
* A namespace for EditorHandler `statics`.
*/
(function (EditorHandler) {
EditorHandler._highlightEffect = StateEffect.define({
map: (value, mapping) => ({ pos: value.pos.map(v => mapping.mapPos(v)) })
});
/**
* Highlight the current line of the frame in the given editor.
*
* @param editor The editor to highlight.
* @param line The line number.
*/
function showCurrentLine(editor, line) {
clearHighlight(editor);
const cmEditor = editor;
const linePos = cmEditor.doc.line(line).from;
cmEditor.editor.dispatch({
effects: EditorHandler._highlightEffect.of({ pos: [linePos] })
});
}
EditorHandler.showCurrentLine = showCurrentLine;
/**
* Remove all line highlighting indicators for the given editor.
*
* @param editor The editor to cleanup.
*/
function clearHighlight(editor) {
if (!editor || editor.isDisposed) {
return;
}
const cmEditor = editor;
cmEditor.editor.dispatch({
effects: EditorHandler._highlightEffect.of({ pos: [] })
});
}
EditorHandler.clearHighlight = clearHighlight;
})(EditorHandler || (EditorHandler = {}));
/**
* A namespace for module private data.
*/
var Private;
(function (Private) {
/**
* Create a marker DOM element for a breakpoint.
*/
Private.breakpointMarker = new (class extends GutterMarker {
toDOM() {
const marker = document.createTextNode('●');
return marker;
}
})();
/**
* Create a new breakpoint.
*
* @param session The name of the session.
* @param line The line number of the breakpoint.
*/
function createBreakpoint(session, line) {
return {
line,
verified: true,
source: {
name: session
}
};
}
Private.createBreakpoint = createBreakpoint;
})(Private || (Private = {}));
//# sourceMappingURL=editor.js.map