/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// Maximum allowed margin (in number of lines) from top or bottom of the editor // while shifting to a line which was initially out of view. const MAX_VERTICAL_OFFSET = 3;
// The id for the current source in the editor (selected source). This is used to: // * cache the scroll snapshot for tracking scroll positions and the symbols, // * know when an actual source is displayed (and not only a loading/error message)
#currentDocumentId = null;
#currentDocument = null;
#CodeMirror6;
#compartments;
#effects;
#lastDirty;
#loadedKeyMaps;
#ownerDoc;
#prefObserver;
#win;
#lineGutterMarkers = new Map();
#lineContentMarkers = new Map();
#posContentMarkers = new Map();
#editorDOMEventHandlers = {};
#gutterDOMEventHandlers = {}; // A cache of all the scroll snapshots for the all the sources that // are currently open in the editor. The keys for the Map are the id's // for the source and the values are the scroll snapshots for the sources.
#scrollSnapshots = new Map();
#updateListener = null;
#beforeUpdateListener = null;
// This stores the language support objects used to syntax highlight code, // These are keyed of the modes.
#languageModes = new Map();
this.version = null; this.config = {
cm6: false,
value: "",
mode: Editor.modes.text,
indentUnit: tabSize,
tabSize,
contextMenu: null,
matchBrackets: true,
highlightSelectionMatches: {
wordsOnly: true,
},
extraKeys: {},
indentWithTabs: useTabs,
inputStyle: "accessibleTextArea", // This is set to the biggest value for setTimeout (See https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#Maximum_delay_value) // This is because codeMirror queries the underlying textArea for some things that // can't be retrieved with events in some browser (but we're fine in Firefox).
pollInterval: Math.pow(2, 31) - 1,
styleActiveLine: true,
autoCloseBrackets: "()[]{}''\"\"``",
autoCloseEnabled: useAutoClose,
theme: "mozilla",
themeSwitching: true,
autocomplete: false,
autocompleteOpts: {}, // Expect a CssProperties object (see devtools/client/fronts/css-properties.js)
cssProperties: null, // Set to `true` to prevent the search addon to be activated.
disableSearchAddon: false, // When the search addon is activated (i.e disableSearchAddon == false), // `useSearchAddonPanel` determines if the default search panel for the search addon should be used. // Set to `false` when a custom search panel is used. // Note: This can probably be removed when Bug 1941575 is fixed, and custom search panel is used everywhere
useSearchAddonPanel: true,
maxHighlightLength: 1000, // Disable codeMirror setTimeout-based cursor blinking (will be replaced by a CSS animation)
cursorBlinkRate: 0, // List of non-printable chars that will be displayed in the editor, showing their // unicode version. We only add a few characters to the default list: // - \u202d LEFT-TO-RIGHT OVERRIDE // - \u202e RIGHT-TO-LEFT OVERRIDE // - \u2066 LEFT-TO-RIGHT ISOLATE // - \u2067 RIGHT-TO-LEFT ISOLATE // - \u2069 POP DIRECTIONAL ISOLATE
specialChars: // eslint-disable-next-line no-control-regex
/[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200f\u2028\u2029\u202d\u202e\u2066\u2067\u2069\ufeff\ufff9-\ufffc]/,
specialCharPlaceholder: char => { // Use the doc provided to the setup function if we don't have a reference to a codeMirror // editor yet (this can happen when an Editor is being created with existing content) const doc = this.#ownerDoc; const el = doc.createElement("span");
el.classList.add("cm-non-printable-char");
el.append(doc.createTextNode(`\\u${char.codePointAt(0).toString(16)}`)); return el;
}, // In CodeMirror 5, adds a `CodeMirror-selectedtext` class on selected text that // can be used to set the selected text color, which isn't possible by default. // This is especially useful for High Contrast Mode where we do need to adjust the // selection text color
styleSelectedText: true,
};
// Disable ctrl-[ and ctrl-] because toolbox uses those shortcuts. this.config.extraKeys[Editor.keyFor("indentLess")] = false; this.config.extraKeys[Editor.keyFor("indentMore")] = false;
// Disable Alt-B and Alt-F to navigate groups (respectively previous and next) since: // - it's not standard in input fields // - it also inserts a character which feels weird this.config.extraKeys["Alt-B"] = false; this.config.extraKeys["Alt-F"] = false;
// Disable Ctrl/Cmd + U as it's used for "View Source". It's okay to disable Ctrl+U as // the underlying command, `undoSelection`, isn't standard in input fields and isn't // widely known. this.config.extraKeys[Editor.accel("U")] = false;
if (!config.disableSearchAddon) { // Override the default search shortcut so the built-in UI doesn't get hidden // when hitting Enter (so the user can cycle through results). this.config.extraKeys[Editor.accel("F")] = () =>
editors.get(this).execCommand("findPersistent");
}
// Disable keys that trigger events with a null-string `which` property. // It looks like some of those (e.g. the Function key), can trigger a poll // which fails to see that there's a selection, which end up replacing the // selected text with an empty string. // TODO: We should investigate the root cause. this.config.extraKeys["'\u0000'"] = false;
// Overwrite default config with user-provided, if needed.
Object.keys(config).forEach(k => { if (k != "extraKeys") { this.config[k] = config[k]; return;
}
if (!this.config.gutters) { this.config.gutters = [];
} if ( this.config.lineNumbers &&
!this.config.gutters.includes("CodeMirror-linenumbers")
) { this.config.gutters.push("CodeMirror-linenumbers");
}
// Remember the initial value of autoCloseBrackets. this.config.autoCloseBracketsSaved = this.config.autoCloseBrackets;
// If the tab behaviour is not explicitly set to `false` from the config, set a tab behavior. // If something is selected, indent those lines. If nothing is selected and we're // indenting with tabs, insert one tab. Otherwise insert N // whitespaces where N == indentUnit option. if (this.config.extraKeys.Tab !== false) { this.config.extraKeys.Tab = cm => { if (config.extraKeys?.Tab) { // If a consumer registers its own extraKeys.Tab, we execute it before doing // anything else. If it returns false, that mean that all the key handling work is // done, so we can do an early return. const res = config.extraKeys.Tab(cm); if (res === false) { return;
}
}
if (cm.somethingSelected()) {
cm.indentSelection("add"); return;
}
if (this.config.indentWithTabs) {
cm.replaceSelection("\t", "end", "+input"); return;
}
let num = cm.getOption("indentUnit"); if (cm.getCursor().ch !== 0) {
num -= cm.getCursor().ch % num;
}
cm.replaceSelection(" ".repeat(num), "end", "+input");
};
if (this.config.cssProperties) { // Ensure that autocompletion has cssProperties if it's passed in via the options. this.config.autocompleteOpts.cssProperties = this.config.cssProperties;
}
}
}
/** *ExposestheCodeMirrorinstance.Wewanttogetawayfromtryingto *abstractawaytheAPIentirely,andthismakesiteasiertointegratein *variousenvironmentsanddocomplexthings.
*/
get codeMirror() { if (!editors.has(this)) { thrownew Error( "CodeMirror instance does not exist. You must wait " + "for it to be appended to the DOM."
);
} return editors.get(this);
}
/** *ReturnwhetherthereisaCodeMirrorinstanceassociatedwiththisEditor.
*/
get hasCodeMirror() { return editors.has(this);
}
appendToLocalElement(el) { const win = el.ownerDocument.defaultView; this.#abortController = new win.AbortController(); if (this.config.cm6) { this.#setupCm6(el);
} else { this.#setup(el);
}
}
// This update listener allows listening to the changes // to the codemiror editor.
setUpdateListener(listener = null) { this.#updateListener = listener;
}
if (this.config.cssProperties) { // Replace the propertyKeywords, colorKeywords and valueKeywords // properties of the CSS MIME type with the values provided by the CSS properties // database. const { propertyKeywords, colorKeywords, valueKeywords } = getCSSKeywords( this.config.cssProperties
);
// Create a CodeMirror instance add support for context menus, // overwrite the default controller (otherwise items in the top and // context menus won't work).
const cm = win.CodeMirror(el, this.config); this.Doc = win.CodeMirror.Doc;
// Disable APZ for source editors. It currently causes the line numbers to // "tear off" and swim around on top of the content. Bug 1160601 tracks // finding a solution that allows APZ to work with CodeMirror.
cm.getScrollerElement().addEventListener( "wheel",
ev => { // By handling the wheel events ourselves, we force the platform to // scroll synchronously, like it did before APZ. However, we lose smooth // scrolling for users with mouse wheels. This seems acceptible vs. // doing nothing and letting the gutter slide around.
ev.preventDefault();
win.CodeMirror.defineExtension("l10n", name => { return L10N.getStr(name);
});
if (!this.config.disableSearchAddon) { this.#initSearchShortcuts(win);
} else { // Hotfix for Bug 1527898. We should remove those overrides as part of Bug 1527903.
Object.assign(win.CodeMirror.commands, {
find: null,
findPersistent: null,
findPersistentNext: null,
findPersistentPrev: null,
findNext: null,
findPrev: null,
clearSearch: null,
replace: null,
replaceAll: null,
});
}
// Retrieve the cursor blink rate from user preference, or fall back to CodeMirror's // default value.
let cursorBlinkingRate = win.CodeMirror.defaults.cursorBlinkRate; if (Services.prefs.prefHasUserValue(CARET_BLINK_TIME)) {
cursorBlinkingRate = Services.prefs.getIntPref(
CARET_BLINK_TIME,
cursorBlinkingRate
);
} // This will be used in the animation-duration property we set on the cursor to // implement the blinking animation. If cursorBlinkingRate is 0 or less, the cursor // won't blink.
cm.getWrapperElement().style.setProperty( "--caret-blink-time",
`${Math.max(0, cursorBlinkingRate)}ms`
);
// Init a map of the loaded keymap files. Should be of the form Map<String->Boolean>. this.#loadedKeyMaps = new Set(); this.#prefObserver.on(KEYMAP_PREF, this.setKeyMap); this.setKeyMap();
win.editor = this; const editorReadyEvent = new win.CustomEvent("editorReady");
win.dispatchEvent(editorReadyEvent);
}
this.#compartments = {
tabSizeCompartment: new Compartment(),
indentCompartment: new Compartment(),
lineWrapCompartment: new Compartment(),
lineNumberCompartment: new Compartment(),
lineNumberMarkersCompartment: new Compartment(),
searchHighlightCompartment: new Compartment(),
domEventHandlersCompartment: new Compartment(),
foldGutterCompartment: new Compartment(),
languageCompartment: new Compartment(),
readOnlyCompartment: new Compartment(),
};
// Track the scroll snapshot for the current document at the end of the scroll this.#editorDOMEventHandlers.scroll = [
debounce(this.#cacheScrollSnapshot, 250),
];
const extensions = [
bracketMatching(), this.#compartments.indentCompartment.of(indentUnit.of(indentStr)), this.#compartments.tabSizeCompartment.of(
EditorState.tabSize.of(this.config.tabSize)
), this.#compartments.lineWrapCompartment.of( this.config.lineWrapping ? EditorView.lineWrapping : []
), this.#compartments.readOnlyCompartment.of(
EditorState.readOnly.of(this.config.readOnly)
), this.#compartments.lineNumberCompartment.of( this.config.lineNumbers ? lineNumbers() : []
),
codeFolding({
placeholderText: "↔",
}), this.#compartments.foldGutterCompartment.of( this.config.enableCodeFolding ? this.#foldGutterConfiguration() : []
),
syntaxHighlighting(lezerHighlight.classHighlighter),
EditorView.updateListener.of(v => { if (!cm.isDocumentLoadComplete) { // Check that the full syntax tree is available the current viewport if (syntaxTreeAvailable(v.state, v.view.viewState.viewport.to)) {
cm.isDocumentLoadComplete = true;
}
} if (v.viewportChanged || v.docChanged) { if (v.docChanged) {
cm.isDocumentLoadComplete = false;
} // reset line gutter markers for the new visible ranges // when the viewport changes(e.g when the page is scrolled). if (this.#lineGutterMarkers.size > 0) { this.setLineGutterMarkers();
}
} // Any custom defined update listener should be called if (typeofthis.#updateListener == "function") { this.#updateListener(v);
}
}), this.#compartments.domEventHandlersCompartment.of(
EditorView.domEventHandlers(this.#createEventHandlers())
), this.#compartments.lineNumberMarkersCompartment.of([]),
lineContentMarkerExtension,
positionContentMarkerExtension, this.#compartments.searchHighlightCompartment.of( this.#searchHighlighterExtension([])
), this.#compartments.languageCompartment.of(languageMode),
highlightSelectionMatches(), // keep last so other extension take precedence
codemirror.minimalSetup,
EditorState.transactionFilter.of(tr => { if (tr.docChanged) { // A change is about to happen, any custom defined // before update listener should be called if (typeofthis.#beforeUpdateListener == "function") { const a = [];
tr.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
a.push({
from: lezerUtils.positionToLocation(tr.state.doc, fromA),
to: lezerUtils.positionToLocation(tr.newDoc, toB),
origin: !inserted.length ? "+delete" : "+input",
text: inserted.toString(), // This is always false for CM6, setting this is just keep the // output expected uniform with that returned by CM5.
canceled: false,
});
}); this.#beforeUpdateListener(a);
}
} return tr;
}),
];
if (Services.prefs.getBoolPref(AUTO_CLOSE)) {
extensions.push(closeBrackets());
}
if (this.config.placeholder) {
extensions.push(placeholder(this.config.placeholder));
}
if (this.config.keyMap) {
extensions.push(Prec.highest(keymap.of(this.config.keyMap)));
}
if (Services.prefs.prefHasUserValue(CARET_BLINK_TIME)) { // We need to multiply the preference value by 2 to match Firefox cursor rate const cursorBlinkRate = Services.prefs.getIntPref(CARET_BLINK_TIME) * 2;
extensions.push(
drawSelection({
cursorBlinkRate,
})
);
}
const cm = new EditorView({
parent: el,
extensions,
});
// For now, we only need to pipe the blur event
cm.contentDOM.addEventListener("blur", e => this.emit("blur", e), {
signal: this.#abortController?.signal,
});
}
let decorationLines; if (marker.shouldMarkAllLines) {
decorationLines = []; for (let i = vStartLine.number; i <= vEndLine.number; i++) {
decorationLines.push({ line: i });
}
} else {
decorationLines = marker.lines;
}
for (const { line, value } of decorationLines) { // Make sure the position is within the viewport if (line < vStartLine.number || line > vEndLine.number) { continue;
}
for (const marker of allMarkers) {
_buildDecorationsForMarker(marker, transaction, allNewDecorations);
}
return markerDecorations.update({ // This filters out all the old decorations
filter: () => false,
add: allNewDecorations,
sort: true,
});
}
function removeDecorations(markerDecorations, markerId) { return markerDecorations.update({
filter: (from, to, decoration) => { return decoration.markerType !== markerId;
},
});
}
// The effects used to create the transaction when markers are // either added and removed. const addEffect = StateEffect.define(); const removeEffect = StateEffect.define();
const lineContentMarkerExtension = StateField.define({
create() { return Decoration.none;
},
update(markerDecorations, transaction) { // Map the decorations through the transaction changes, this is important // as it remaps the decorations from positions in the old document to // positions in the new document.
markerDecorations = markerDecorations.map(transaction.changes); for (const effect of transaction.effects) { // When a new marker is added if (effect.is(addEffect)) {
markerDecorations = updateDecorations(
markerDecorations,
effect.value,
transaction
);
} elseif (effect.is(removeEffect)) { // when a marker is removed
markerDecorations = removeDecorations(
markerDecorations,
effect.value
);
} else { const cachedMarkers = lineContentMarkers.values(); // For updates that are not related to this marker decoration, // we want to update the decorations when the editor is scrolled // and a new viewport is loaded.
markerDecorations = updateDecorationsForAllMarkers(
markerDecorations,
cachedMarkers,
transaction
);
}
} return markerDecorations;
},
provide: field => EditorView.decorations.from(field),
});
#createEventHandlers() { const eventHandlers = {}; for (const eventName in this.#editorDOMEventHandlers) { const handlers = this.#editorDOMEventHandlers[eventName];
eventHandlers[eventName] = (event, editor) => { if (!event.target) { return;
} for (const handler of handlers) { // Wait a cycle so the codemirror updates to the current cursor position, // information, TODO: Currently noticed this issue with CM6, not ideal but should // investigate further Bug 1890895.
event.target.documentGlobal.setTimeout(() => { const view = editor.viewState; const cursorPos = lezerUtils.positionToLocation(
view.state.doc,
view.state.selection.main.head
);
handler(event, view, cursorPos.line, cursorPos.column);
}, 0);
}
};
} return eventHandlers;
}
// Update the cache of dom event handlers for (const eventName in domEventHandlers) { if (!this.#editorDOMEventHandlers[eventName]) { this.#editorDOMEventHandlers[eventName] = [];
} this.#editorDOMEventHandlers[eventName].push(domEventHandlers[eventName]);
}
/** *Thisaddsamarkerusedtoaddclassestoeditorlinebasedonacondition. * *@property{object}marker *Therulerenderingamarkerorclass. *@property{object}marker.id *Theuniqueidentifierforthismarker *@property{string}marker.lineClassName *Thecssclasstoapplytotheline *@property{Array<object>}marker.lines *Thelinestoaddmarkersto.Eachlineobjecthasa`line`and`value`property. *@property{boolean}marker.renderAsBlock *Thespecifiesthatthewidgetshouldberenderedasablockelement.defaultsto`false`.Thisisoptional. *@property{boolean}marker.shouldMarkAllLines *Settotruetoapplythemarkertoallthelines.Insuchcase,`positions`isignored.Thisisoptional. *@property{Function}marker.createLineElementNode *ThisshouldreturntheDOMelementwhichisusedforthemarker.Thelinenumberispassedasaparameter. *Thisisoptional.
*/
setLineContentMarker(marker) { const cm = editors.get(this); // We store the marker an the view state, this is gives access to view data // when defining updates to the StateField.
marker._view = cm; this.#lineContentMarkers.set(marker.id, marker);
cm.dispatch({
effects: this.#effects.lineContentMarkerEffect.addEffect.of(marker),
});
}
function _buildDecorationsForPositionMarkers(
marker,
transaction,
newMarkerDecorations
) { const viewport = marker._view.viewport; // If the viewport changes and does ont match the state, // lets not try to update the decorations because the positions // would not longer be valid. if (viewport.to > transaction.state.doc.length) { return;
} const vStartLine = transaction.state.doc.lineAt(viewport.from); const vEndLine = transaction.state.doc.lineAt(viewport.to);
for (const position of marker.positions) { // If codemirror positions are provided (e.g from search cursor) // compare that directly. if (position.from && position.to) { if (position.from >= viewport.from && position.to <= viewport.to) { if (marker.positionClassName) { // Markers used: // 1. active-selection-marker const classDecoration = Decoration.mark({ class: marker.positionClassName,
});
classDecoration.markerType = marker.id;
newMarkerDecorations.push(
classDecoration.range(position.from, position.to)
);
}
} continue;
} // If line and column are provided if (
position.line >= vStartLine.number &&
position.line <= vEndLine.number
) { const line = transaction.state.doc.line(position.line); // Make sure to track any indentation at the beginning of the line const column = Math.max(position.column, getIndentation(line.text)); const pos = line.from + column;
if (marker.createPositionElementNode) { // Markers used: // 1. column-breakpoint-marker const isFirstNonSpaceColumn = ONLY_SPACES_REGEXP.test(
line.text.substr(0, column)
); const nodeDecoration = Decoration.widget({
widget: new NodeWidget({
line: position.line,
column: position.column,
isFirstNonSpaceColumn,
positionData: position.positionData,
markerId: marker.id,
createElementNode: marker.createPositionElementNode,
customEq: marker.customEq,
}), // Make sure the widget is rendered after the cursor // see https://codemirror.net/docs/ref/#view.Decoration^widget^spec.side for details.
side: 1,
});
nodeDecoration.markerType = marker.id;
newMarkerDecorations.push(nodeDecoration.range(pos, pos));
} if (marker.positionClassName) { // Markers used: // 1. exception-position-marker // 2. debug-position-marker const tokenAtPos = syntaxTree(transaction.state).resolve(pos, 1); // While trying to update the markers, during content changes, the syntax tree is not // guaranteed to be complete, so there is the possibility of getting wrong `from` and `to` values for the token. // To make sure we are handling a valid token, let's check that the `from` value (which is the start position of the retrieved token) // matches the position we want. if (tokenAtPos.from !== pos) { continue;
} const tokenString = line.text.slice(
position.column,
tokenAtPos.to - line.from
); // Ignore any empty strings and opening braces if (
tokenString === "" ||
tokenString === "{" ||
tokenString === "["
) { continue;
} const classDecoration = Decoration.mark({ class: marker.positionClassName,
});
classDecoration.markerType = marker.id;
newMarkerDecorations.push(
classDecoration.range(pos, tokenAtPos.to)
);
}
}
}
}
// Sort the markers iterator thanks to `displayLast` boolean. // This is typically used by the paused location marker to be shown after the column breakpoints.
markers = Array.from(markers).sort((a, b) => { if (a.displayLast) { return1;
} if (b.displayLast) { return -1;
} return0;
});
const positionContentMarkerExtension = StateField.define({
create() { return Decoration.none;
},
update(markerDecorations, transaction) { // Map the decorations through the transaction changes, this is important // as it remaps the decorations from positions in the old document to // positions in the new document.
markerDecorations = markerDecorations.map(transaction.changes); for (const effect of transaction.effects) { if (effect.is(addEffect)) { // When a new marker is added
markerDecorations = updateDecorations(
markerDecorations,
effect.value,
transaction
);
} elseif (effect.is(removeEffect)) { // When a marker is removed
markerDecorations = removeDecorations(
markerDecorations,
effect.value
);
} else { // For updates that are not related to this marker decoration, // we want to update the decorations when the editor is scrolled // and a new viewport is loaded.
markerDecorations = updateDecorationsForAllMarkers(
markerDecorations,
cachedPositionContentMarkers.values(),
transaction
);
}
} return markerDecorations;
},
provide: field => EditorView.decorations.from(field),
});
// We store the marker an the view state, this is gives access to viewport data // when defining updates to the StateField.
marker._view = cm; this.#posContentMarkers.set(marker.id, marker);
cm.dispatch({
effects: this.#effects.positionContentMarkerEffect.addEffect.of(marker),
});
}
if (markers) { // Cache the markers for use later. See next comment for (const marker of markers) { if (!marker.id) { thrownew Error("Marker has no unique identifier");
} this.#lineGutterMarkers.set(marker.id, marker);
}
} // When no markers are passed, the cached markers are used to update the line gutters. // This is useful for re-rendering the line gutters when the viewport changes // (note: the visible ranges will be different) in this case, mainly when the editor is scrolled. elseif (!this.#lineGutterMarkers.size) { return;
}
markers = Array.from(this.#lineGutterMarkers.values());
// This creates a new GutterMarker https://codemirror.net/docs/ref/#view.GutterMarker // to represents how each line gutter is rendered in the view. // This is set as the value for the Range https://codemirror.net/docs/ref/#state.Range // which represents the line. class LineGutterMarker extends GutterMarker {
constructor(className, lineNumber, createElementNode, conditionResult) { super(); this.elementClass = className || null; this.lineNumber = lineNumber; this.createElementNode = createElementNode; this.conditionResult = conditionResult;
// Loop through the visible ranges https://codemirror.net/docs/ref/#view.EditorView.visibleRanges // (representing the lines in the current viewport) and generate a new rangeset for updating the line gutter // based on the conditions defined in the markers(for each line) provided. const builder = new RangeSetBuilder(); const { from, to } = cm.viewport;
let pos = from; while (pos <= to) { const line = cm.state.doc.lineAt(pos); for (const {
lineClassName,
condition,
createLineElementNode,
} of markers) { if (typeof condition !== "function") { thrownew Error("The `condition` is not a valid function");
} const conditionResult = condition(line.number); if (conditionResult !== false) {
builder.add(
line.from,
line.to, new LineGutterMarker(
lineClassName,
line.number,
createLineElementNode,
conditionResult
)
);
}
}
pos = line.to + 1;
}
// To update the state with the newly generated marker range set, a dispatch is called on the view // with an transaction effect created by the lineNumberMarkersCompartment, which is used to update the // lineNumberMarkers extension configuration.
cm.dispatch({
effects: this.#compartments.lineNumberMarkersCompartment.reconfigure(
lineNumberMarkers.of(builder.finish())
),
});
}
/** *Givenscreencoordinatesthisshouldreturnthelineandcolumn *related.Thisusedcurrentlytodeterminethelineandcolumns *forthetokensthatarehoveredover. * *@param{number}left-Horizontalpositionfromtheleft *@param{number}top-Verticalpositionfromthetop *@returns{object}position-Thelineandcolumnrelatedtothescreencoordinates. */ getPositionAtScreenCoords(left,top){ constcm=editors.get(this); if(this.config.cm6){ constposition=cm.posAtCoords( {x:left,y:top}, // "precise", i.e. if a specific position cannot be determined, an estimated one will be used false ); constline=cm.state.doc.lineAt(position); return{ line:line.number, column:position-line.from, }; } const{line,ch}=cm.coordsChar( {left,top}, // Use the "window" context where the coordinates are relative to the top-left corner // of the currently visible (scrolled) window. // This enables codemirror also correctly handle wrappped lines in the editor. "window" ); return{ line:line+1, column:ch, }; }
/** *Changesthecurrentlyusedsyntaxhighlightingmode. * *@param{object}mode-AnyofthemodesfromEditor.modes *@returns
*/
setMode(mode) { if (this.config.cm6) { const cm = editors.get(this); // Fallback to using js syntax highlighting if there is none found const languageMode = this.#languageModes.has(mode)
? this.#languageModes.get(mode)
: this.#languageModes.get(Editor.modes.javascript);
// If autocomplete was set up and the mode is changing, then // turn it off and back on again so the proper mode can be used. if (this.config.autocomplete) { this.setOption("autocomplete", false); this.setOption("autocomplete", true);
} returnnull;
}
/** *Insertastringintotheeditoratthecursorlocation, *movingthecursortotheendofthestring. * *@param{string}str *@param{int}numberOfCharsToReplaceCharsBeforeCursor-defaultsto0 *@param{string}origin
*/
insertStringAtCursor(
str,
numberOfCharsToReplaceCharsBeforeCursor = 0,
origin
) { const cm = editors.get(this); if (this.config.cm6) { const pos = cm.state.selection.main.head; // if the cursor position is `0` (which is the case when selecting text backward to the first position) // we are going to get a negetive offset, this would throw an error. const offset = pos - numberOfCharsToReplaceCharsBeforeCursor;
cm.dispatch({
changes: {
from: offset >= 0 ? offset : 0, // Start offset
to: pos, // End offset
insert: str, // Replacement text
},
});
} else { const cursor = cm.getCursor(); const from = {
line: cursor.line,
ch: cursor.ch - numberOfCharsToReplaceCharsBeforeCursor,
};
cm.getDoc().replaceRange(str, from, cursor, origin);
}
}
getDoc() { if (!this.config) { returnnull;
} const cm = editors.get(this); if (this.config.cm6) { if (!this.#currentDocument) { // A key for caching the WASM content in the WeakMap this.#currentDocument = { id: this.#currentDocumentId };
} returnthis.#currentDocument;
} return cm.getDoc();
}
get isWasm() { return wasm.isWasm(this.getDoc());
}
/** *Getsdetailsabouttheline * *@param{number}line *@returns{object}lineinfoobject
*/
lineInfo(line) { const cm = editors.get(this); if (this.config.cm6) { const el = this.getElementAtLine(line); return {
text: cm.state.doc.line(line).text, // TODO: Expose those, or see usage for those and do things differently
line: null,
gutterMarkers: null,
textClass: null,
bgClass: null,
wrapClass: el.className,
widgets: null,
};
}
let doc, tree; // If the specified source is already loaded in the editor, // codemirror has likely parsed most or all the source needed, // just leverage that const sourceId = location.source.id; if (this.#currentDocumentId === sourceId) {
doc = cm.state.doc; // Parse the rest of the if needed.
await forceParsing(cm, doc.length, 10000);
tree = syntaxTree(cm.state);
} else { // If the source is not currently loaded in the editor we will need // to explicitly parse its source text. // Note: The `loadSourceText` actions is called before this util `getClosestFunctionName` // to make sure source content is available to use. const sourceContent = this.#sources.get(location.source.id); if (!sourceContent) {
console.error(
`Can't find source content for ${location.source.id}, no function name can be determined`
); return"";
}
// Create a codemirror document for the current source text.
doc = cm.state.toText(sourceContent);
tree = lezerUtils.getTree(javascriptLanguage, sourceId, sourceContent);
}
// There might be multiple expressions which are within the locations. // We want to match expressions based on dots before the desired token. // // ========================== EXAMPLE 1 ================================ // Full Expression: `this.myProperty.x` // Hovered Token: `myProperty` // Found Expressions: // { name: "MemberExpression", expression: "this.myProperty.x", from: 1715, to: 1732 } // { name: "MemberExpression", expression: "this.myProperty" from: 1715, to: 1730 } * // { name: "PropertyName", expression: "myProperty" from: 1720, to: 1730 } // // ========================== EXAMPLE 2 ================================== // Full Expression: `a(b).catch` // Hovered Token: `b` // Found Expressions: // { name: "MemberExpression", expression: "a(b).catch", from: 1921 to: 1931 } // { name: "VariableName", expression: "b", from: 1923 to: 1924 } * // // We sort based on the `to` make sure we return the correct property return expressions.sort((a, b) => { if (a.to < b.to) { return -1;
} elseif (a.to > b.to) { return1;
} return0;
});
}
// Sort based on the start locations so the scopes // are in the same order as in the source. const sortedLocations = scopeUtils.sortByStart(functionLocations);
// Any function locations which are within the immediate function scope // of the paused location. const innerLocations = scopeUtils.getInnerLocations(
sortedLocations,
location
);
// Any outer locations which do not contain the immediate function // of the paused location const outerLocations = sortedLocations.filter(loc => { if (innerLocations.includes(loc)) { returnfalse;
} return !scopeUtils.containsPosition(loc, location);
});
// This operation can be very costly for large files so we sacrifice a bit of readability // for performance sake. // We initialize an array with a fixed size and we'll directly assign value for lines // that are not out of scope. This is much faster than having an empty array and pushing // into it. const sourceNumLines = cm.state.doc.lines; const sourceLines = new Array(sourceNumLines); for (let i = 0; i < sourceNumLines; i++) { const line = i + 1; if (outOfScopeLines.size == 0 || !outOfScopeLines.has(line)) {
sourceLines[i] = line;
}
}
// Finally we need to remove any undefined values, i.e. the ones that were matching // out of scope lines. return sourceLines.filter(i => i != undefined);
}
let scopeNode = null;
let level = 0; const bindingReferences = {};
// Walk up the scope tree and generate the bindings and references while (scope && scope.bindings) { const bindings = lezerUtils.getScopeBindings(scope.bindings); const seen = new Set();
scopeNode = lezerUtils.getParentScopeOfType(
scopeNode || token,
scope.type
); if (!scopeNode) { break;
}
await lezerUtils.walkCursor(scopeNode.node.cursor(), {
filterSet: lezerUtils.nodeTypeSets.bindingReferences,
enterVisitor: node => {
let bindingName = cm.state.doc.sliceString(node.from, node.to); if (!(bindingName in bindings) || seen.has(bindingName)) { return;
} const bindingData = bindings[bindingName]; const ref = {
start: lezerUtils.positionToLocation(cm.state.doc, node.from),
end: lezerUtils.positionToLocation(cm.state.doc, node.to),
}; const syntaxNode = node.node; // Previews for member expressions are built of the meta property which is // reference of the child property and so on. e.g a.b.c if (syntaxNode.parent.name == lezerUtils.nodeTypes.MemberExpression) {
ref.meta = lezerUtils.getMetaBindings(
cm.state.doc,
syntaxNode.parent
); // For member expressions use the name of the parent object as the binding name // i.e for `obj.a.b` the binding name should be `obj`
bindingName = cm.state.doc.sliceString(
syntaxNode.parent.from,
syntaxNode.parent.to
); const dotIndex = bindingName.indexOf("."); if (dotIndex > -1) {
bindingName = bindingName.substring(0, dotIndex);
}
}
if (!bindingReferences[level]) {
bindingReferences[level] = Object.create(null);
} if (!bindingReferences[level][bindingName]) { // Put the binding info and related references together for // easy and efficient access.
bindingReferences[level][bindingName] = {
...bindingData,
refs: [],
};
}
bindingReferences[level][bindingName].refs.push(ref);
seen.add(bindingName);
},
}); if (scope.type === "function") { break;
}
level++;
scope = scope.parent;
} return bindingReferences;
}
if (documentId) { this.#currentDocumentId = documentId;
} else { // Reset this ID when showing loading and error messages, // so that we keep track when an actual source is displayed this.#currentDocumentId = null;
}
if (isWasm) { // wasm? // binary does not survive as Uint8Array, converting from string const binary = value.binary; const data = new Uint8Array(binary.length); for (let i = 0; i < data.length; i++) {
data[i] = binary.charCodeAt(i);
}
const { lines, done } = wasm.getWasmText(this.getDoc(), data); const MAX_LINES = 10000000; if (lines.length > MAX_LINES) {
lines.splice(MAX_LINES, lines.length - MAX_LINES);
lines.push(";; .... text is truncated due to the size");
} if (!done) {
lines.push(";; .... possible error during wast conversion");
}
if (this.config.cm6) {
value = lines.join("\n");
} else { // cm will try to split into lines anyway, saving memory
value = { split: () => lines };
}
}
if (this.config.cm6) { if (cm.state.doc.toString() == value) { return;
}
await cm.dispatch({
changes: { from: 0, to: cm.state.doc.length, insert: value },
selection: { anchor: 0 },
annotations: [Transaction.addToHistory.of(saveTransactionToHistory)],
});
const effects = []; if (this.config?.lineNumbers) { const lineNumbersConfig = {
domEventHandlers: this.#gutterDOMEventHandlers,
}; if (isWasm) {
lineNumbersConfig.formatNumber = this.getWasmLineNumberFormatter();
}
effects.push( this.#compartments.lineNumberCompartment.reconfigure( this.config.lineNumbers ? lineNumbers(lineNumbersConfig) : []
)
);
} // Get the cached scroll snapshot for this source and restore // the scroll position. Note: The scroll has to be done in a seperate dispatch // (after the previous dispatch has set the document), this is because // it is required that the document the scroll snapshot is applied to // is the exact document it was saved on. const scrollSnapshot = this.#scrollSnapshots.get(documentId);
if (this.currentDocumentId) { // If there is no scroll snapshot explicitly cache the snapshot set as no scroll // is triggered. if (!scrollSnapshot) { this.#cacheScrollSnapshot();
}
}
} else {
cm.setValue(value);
}
clearSources(ids) { if (ids) { for (const id of ids) { this.#sources.delete(id);
}
} else { this.#sources.clear();
lezerUtils.clear();
}
}
/* Currently used only in tests */
sourcesCount() { returnthis.#sources.size;
}
/** *Reloadsthestateoftheeditorbasedonallcurrentpreferences. *Thisiscalledautomaticallywhenanyoftherelevantpreferences *change.
*/
reloadPreferences() { // Restore the saved autoCloseBrackets value if it is preffed on. const useAutoClose = Services.prefs.getBoolPref(AUTO_CLOSE); this.setOption( "autoCloseBrackets",
useAutoClose ? this.config.autoCloseBracketsSaved : false
);
// If alternative keymap is provided, use it. if (VALID_KEYMAPS.has(keyMap)) { if (!this.#loadedKeyMaps.has(keyMap)) {
Services.scriptloader.loadSubScript(VALID_KEYMAPS.get(keyMap), win); this.#loadedKeyMaps.add(keyMap);
} this.setOption("keyMap", keyMap);
} else { this.setOption("keyMap", "default");
}
}
/** *Setstheeditor'sindentationbasedonthecurrentprefsand *re-detectindentationifweshould.
*/
resetIndentUnit() { if (this.isDestroyed()) { return;
} const cm = editors.get(this); const iterFn = (start, maxEnd, callback) => { if (!this.config.cm6) { if (this.isDestroyed()) { return;
}
cm.eachLine(start, maxEnd, line => { return callback(line.text);
});
} else { const iterator = cm.state.doc.iterLines(
start + 1,
Math.min(cm.state.doc.lines, maxEnd) + 1
);
let callbackRes; do {
iterator.next();
callbackRes = callback(iterator.value);
} while (iterator.done !== true && !callbackRes);
}
};
/** *Alignstheprovidedlinetoeither"top","center"or"bottom"ofthe *editorviewwithamaximummarginofMAX_VERTICAL_OFFSETlinesfromtopor *bottom.
*/
alignLine(line, align) { const cm = editors.get(this); const from = cm.lineAtHeight(0, "page"); const to = cm.lineAtHeight(cm.getWrapperElement().clientHeight, "page"); const linesVisible = to - from; const halfVisible = Math.round(linesVisible / 2);
// If the target line is in view, skip the vertical alignment part. if (line <= to && line >= from) { return;
}
// Setting the offset so that the line always falls in the upper half // of visible lines (lower half for bottom aligned). // MAX_VERTICAL_OFFSET is the maximum allowed value. const offset = Math.min(halfVisible, MAX_VERTICAL_OFFSET);
// Bringing down the topLine to total lines in the editor if exceeding.
topLine = Math.min(topLine, this.lineCount()); this.setFirstVisibleLine(topLine);
}
this.openDialog(div, line => { // Handle LINE:COLUMN as well as LINE const match = line.toString().match(RE_JUMP_TO_LINE); if (match) { const [, matchLine, column] = match; this.setCursor({ line: matchLine - 1, ch: column ? column - 1 : 0 });
}
});
}
/** *Movesthecontentofthecurrentlineorthelinesselectedupaline.
*/
moveLineUp() { const cm = editors.get(this); const start = cm.getCursor("start"); const end = cm.getCursor("end");
if (start.line === 0) { return;
}
// Get the text in the lines selected or the current line of the cursor // and append the text of the previous line.
let value; if (start.line !== end.line) {
value =
cm.getRange(
{ line: start.line, ch: 0 },
{ line: end.line, ch: cm.getLine(end.line).length }
) + "\n";
} else {
value = cm.getLine(start.line) + "\n";
}
value += cm.getLine(start.line - 1);
// Replace the previous line and the currently selected lines with the new // value and maintain the selection of the text.
cm.replaceRange(
value,
{ line: start.line - 1, ch: 0 },
{ line: end.line, ch: cm.getLine(end.line).length }
);
cm.setSelection(
{ line: start.line - 1, ch: start.ch },
{ line: end.line - 1, ch: end.ch }
);
}
/** *Movesthecontentofthecurrentlineorthelinesselecteddownaline.
*/
moveLineDown() { const cm = editors.get(this); const start = cm.getCursor("start"); const end = cm.getCursor("end");
if (end.line + 1 === cm.lineCount()) { return;
}
// Get the text of next line and append the text in the lines selected // or the current line of the cursor.
let value = cm.getLine(end.line + 1) + "\n"; if (start.line !== end.line) {
value += cm.getRange(
{ line: start.line, ch: 0 },
{ line: end.line, ch: cm.getLine(end.line).length }
);
} else {
value += cm.getLine(start.line);
}
// Replace the currently selected lines and the next line with the new // value and maintain the selection of the text.
cm.replaceRange(
value,
{ line: start.line, ch: 0 },
{ line: end.line + 1, ch: cm.getLine(end.line + 1).length }
);
cm.setSelection(
{ line: start.line + 1, ch: start.ch },
{ line: end.line + 1, ch: end.ch }
);
}
/** *InterceptCodeMirror'sFindandreplacekeyshortcuttoselectthesearchinput
*/
findOrReplace(node, isReplaceAll) { const cm = editors.get(this); const isInput = node.tagName === "INPUT"; const isSearchInput = isInput && node.type === "search"; // replace box is a different input instance than search, and it is // located in a code mirror dialog const isDialogInput =
isInput &&
node.parentNode &&
node.parentNode.classList.contains("CodeMirror-dialog"); if (!(isSearchInput || isDialogInput)) { return;
}
if (isSearchInput || isReplaceAll) { // select the search input // it's the precise reason why we reimplement these key shortcuts
node.select();
}
// need to call it since we prevent the propagation of the event and // cancel codemirror's key handling
cm.execCommand("findPersistent");
}
/** *InterceptCodeMirror'sfindNextandfindPrevkeyshortcuttoallow *immediatelysearchfornextoccuranceaftertypingawordtosearch.
*/
findNextOrPrev(node, isFindPrev) { const cm = editors.get(this); const isInput = node.tagName === "INPUT"; const isSearchInput = isInput && node.type === "search"; if (!isSearchInput) { return;
} const query = node.value; // cm.state.search allows to automatically start searching for the next occurance // it's the precise reason why we reimplement these key shortcuts if (!cm.state.search || cm.state.search.query !== query) {
cm.state.search = {
posFrom: null,
posTo: null,
overlay: null,
query,
};
}
// need to call it since we prevent the propagation of the event and // cancel codemirror's key handling if (isFindPrev) {
cm.execCommand("findPrev");
} else {
cm.execCommand("findNext");
}
}
/** *Returnscurrentfontsizefortheeditorarea,inpixels.
*/
getFontSize() { const cm = editors.get(this); const el = cm.getWrapperElement(); const win = el.ownerDocument.defaultView;
// Save the state of a valid autoCloseBrackets string, so we can reset // it if it gets preffed off and back on. if (o === "autoCloseBrackets" && v) { this.config.autoCloseBracketsSaved = v;
}
if (o === "autocomplete") { this.config.autocomplete = v; this.setupAutoCompletion();
} else {
cm.setOption(o, v); this.config[o] = v;
}
if (o === "enableCodeFolding") { // The new value maybe explicitly force foldGUtter on or off, ignoring // the prefs service. this.updateCodeFoldingGutter();
}
}
/** *Getsanoptionfortheeditor.Formostoptionsitjustdefersto *CodeMirror.getOption,butcertainonesaremaintainedwithintheeditor *instance.
*/
getOption(o) { const cm = editors.get(this); if (o === "autocomplete") { returnthis.config.autocomplete;
}
return cm.getOption(o);
}
/** *Setsupautocompletionfortheeditor.Lazilyimportstherequired *dependenciesbecausetheyvarybyeditormode. * *Autocompletionisspecial,becausewedon'twanttoautomaticallyuse *itjustbecauseitispreffedon(itstillneedstoberequestedbythe *editor),butwedowanttoalwaysdisableitifitispreffedoff.
*/
setupAutoCompletion() { if (!this.config.autocomplete && !this.initializeAutoCompletion) { // Do nothing since there is no autocomplete config and no autocompletion have // been initialized. return;
} // The autocomplete module will overwrite this.initializeAutoCompletion // with a mode specific autocompletion handler. if (!this.initializeAutoCompletion) { this.extend(
require("resource://devtools/client/shared/sourceeditor/autocomplete.js")
);
}
/** *Getthemarkedautocompletiontextfromtheeditor * *@returns{string}Autocompletiontext
*/
getAutoCompletionText() { const cm = editors.get(this); if (this.config.cm6) { const decorations = this.getDecorationsForMarker( this.markerTypes.AUTOCOMPLETE_CONTENT_MARKER
); if (!decorations.length) { return"";
} // For the autocomplete marker we expect to find only // one decoration. const mark = decorations[0].widget.toDOM(); return mark.attributes["data-completion"].value || "";
} const mark = cm
.getAllMarks()
.find(m => m.className === AUTOCOMPLETE_MARK_CLASSNAME);
if (!mark) { return"";
} return mark.attributes["data-completion"] || "";
}
/** *Getstheelementatthespecifiedcodemirroroffset * *@param{number}offset *@return{Element|null}
*/
#getElementAtOffset(offset) { const cm = editors.get(this); const el = cm.domAtPos(offset).node; if (!el) { returnnull;
} // Text nodes do not have offset* properties, so lets use its // parent element; if (el.nodeType == nodeConstants.TEXT_NODE) { return el.parentElement;
} return el;
}
/** *Thischecksifthespecifiedposition(line/column)iswithinthecurrentviewport *bounds.ithelpsdetermineifscrollingshouldhappen. * *@param{number}line-Thelineinthesource *@param{number}column-Thecolumninthesource *@returns{boolean}
*/
isPositionVisible(line, column) { const cm = editors.get(this);
let inXView, inYView;
function withinBounds(x, min, max) { return x >= min && x <= max;
}
if (this.config.cm6) {
const pos = this.#positionToOffset(line, column);
if (pos == null) {
return false;
}
// `coordsAtPos` returns the absolute position of the line/column location
// so that we have to ensure comparing with same absolute position for
// CodeMirror DOM Element.
//
// Note that it may return the coordinates for a column breakpoint marker
// so it may still report as visible, if the marker is on the edge of the viewport
// and the displayed character at line/column is actually hidden after the scrollable area.
const coords = cm.coordsAtPos(pos);
if (!coords) {
return false;
}
const { x, y, width, height } = cm.dom.getBoundingClientRect();
const gutterEl = cm.dom.querySelector(".cm-gutters");
const gutterWidth = gutterEl ? gutterEl.clientWidth : 0;
inXView = withinBounds(
left,
scrollLeft,
// Note: 30 might relate to the margin on one of the scroll bar elements.
// See comment https://github.com/firefox-devtools/debugger/pull/5182#discussion_r163439209
scrollLeft + (scrollArea.clientWidth - 30) - charWidth
);
inYView = withinBounds(
top,
scrollTop,
scrollTop + scrollArea.clientHeight - fontHeight
);
}
return inXView && inYView;
}
/**
* Converts line/col to CM6 offset position
*
* @param {number} line - The line in the source
* @param {number} col - The column in the source
* @returns {number}
*/
#positionToOffset(line, col = 0) {
const cm = editors.get(this);
try {
const offset = cm.state.doc.line(line);
return offset.from + col;
} catch (e) {
// Line likey does not exist in viewport yet
console.warn(e.message);
}
return null;
}
/**
* This returns the line and column for the specified search cursor's position
*
* @param {RegExpSearchCursor} searchCursor
* @returns {object}
*/
getPositionFromSearchCursor(searchCursor) {
const cm = editors.get(this);
const lineFrom = cm.state.doc.lineAt(searchCursor.from);
return {
line: lineFrom.number - 1,
ch: searchCursor.to - searchCursor.match[0].length - lineFrom.from,
};
}
/**
* Scrolls the editor to the specified codemirror position
*
* @param {number} position
*/
scrollToPosition(position) {
const cm = editors.get(this);
if (!this.config.cm6) {
throw new Error("This function is only compatible with CM6");
}
const {
codemirrorView: { EditorView },
} = this.#CodeMirror6;
return cm.dispatch({
effects: EditorView.scrollIntoView(position, {
x: "nearest",
y: "center",
}),
});
}
/**
* Scrolls the editor to the specified line and column
*
* @param {number} line - The line in the source
* @param {number} column - The column in the source
* @param {string | null} yAlign - Optional value for position of the line after the line is scrolled.
* (Used by `scrollEditorIntoView` test helper)
*/
async scrollTo(line, column, yAlign) {
if (this.isDestroyed()) {
return null;
}
const cm = editors.get(this);
if (this.config.cm6) {
const {
codemirrorView: { EditorView },
} = this.#CodeMirror6;
if (!this.isPositionVisible(line, column)) {
const offset = this.#positionToOffset(line, column);
if (offset == null) {
return null;
}
return cm.dispatch({
effects: EditorView.scrollIntoView(offset, {
x: "center",
y: yAlign || "center",
}),
});
}
} else {
// For all cases where these are on the first line and column,
// avoid the possibly slow computation of cursor location on large bundles.
if (!line && !column) {
cm.scrollTo(0, 0);
return null;
}
// Used only in tests
setSelectionAt(start, end) {
const cm = editors.get(this);
if (this.config.cm6) {
const from = this.#positionToOffset(start.line, start.column);
const to = this.#positionToOffset(end.line, end.column);
if (from == null || to == null) {
return;
}
cm.dispatch({ selection: { anchor: from, head: to } });
} else {
cm.setSelection(
{ line: start.line - 1, ch: start.column },
{ line: end.line - 1, ch: end.column }
);
}
}
/**
* Move CodeMirror cursor to a given location.
* This will also scroll the editor to the specified position.
*
* @param {number} line
* @param {number} column
* @param {boolean} scroll
*/
async setCursorAt(line, column, scroll = true) {
if (scroll) {
await this.scrollTo(line, column);
}
const cm = editors.get(this);
if (this.config.cm6) {
const { lines } = cm.state.doc;
if (line > lines) {
console.error(
`Trying to set the cursor on a non-existing line ${line} > ${lines}`
);
return null;
}
const lineInfo = cm.state.doc.line(line);
if (column > lineInfo.length) {
console.error(
`Trying to set the cursor on a non-existing column ${column} > ${lineInfo.length}`
);
return null;
}
const position = lineInfo.from + column;
return cm.dispatch({ selection: { anchor: position, head: position } });
}
return this.setCursor({ line, ch: column });
}
/**
* Set the cursor at a codemirror 6 position in the document.
*
* @param {number} position
* @param {boolean} scroll
* @returns
*/
setCursorAtPosition(position, scroll = true) {
const cm = editors.get(this);
if (scroll) {
cm.scrollToPosition(position);
}
return cm.dispatch({ selection: { anchor: position, head: position } });
}
// Used only in tests
getEditorFileMode() {
const cm = editors.get(this);
if (this.config.cm6) {
return cm.contentDOM.dataset.language;
}
return cm.getOption("mode").name;
}
// Used only in tests
getEditorContent() {
const cm = editors.get(this);
if (this.config.cm6) {
return cm.state.doc.toString();
}
return cm.getValue();
}
isSearchStateReady() {
const cm = editors.get(this);
if (this.config.cm6) {
return !!this.searchState.cursors;
}
return !!cm.state.search;
}
// Used only in tests
getCoords(line, column = 0) {
const cm = editors.get(this);
if (this.config.cm6) {
const offset = this.#positionToOffset(line, column);
if (offset == null) {
return null;
}
return cm.coordsAtPos(offset);
}
// CodeMirror is 0-based while line and column arguments are 1-based.
// Pass "column=-1" when there is no column argument passed.
return cm.charCoords({ line: ~~line, ch: ~~column });
}
// Used only in tests
// Only used for CM6
getElementAtLine(line) {
const offset = this.#positionToOffset(line);
const el = this.#getElementAtOffset(offset);
return el.closest(".cm-line");
}
// Used only in tests
getSearchQuery() {
const cm = editors.get(this);
if (this.config.cm6) {
return this.searchState.query.toString();
}
return cm.state.search.query;
}
// Used only in tests
// Gets currently selected search term
getSearchSelection() {
const cm = editors.get(this);
if (this.config.cm6) {
const cursor =
this.searchState.cursors[this.searchState.currentCursorIndex];
if (!cursor) {
return { text: "", line: -1, column: -1 };
}
// Only used for CM6
getElementAtPos(line, column) {
const offset = this.#positionToOffset(line, column);
const el = this.#getElementAtOffset(offset);
return el;
}
// Used only in tests
getLineCount() {
const cm = editors.get(this);
if (this.config.cm6) {
return cm.state.doc.lines;
}
return cm.lineCount();
}
/**
* Extends an instance of the Editor object with additional
* functions. Each function will be called with context as
* the first argument. Context is a {ed, cm} object where
* 'ed' is an instance of the Editor object and 'cm' is an
* instance of the CodeMirror object. Example:
*
* function hello(ctx, name) {
* let { cm, ed } = ctx;
* cm; // CodeMirror instance
* ed; // Editor instance
* name; // 'Mozilla'
* }
*
* editor.extend({ hello: hello });
* editor.hello('Mozilla');
*/
extend(funcs) {
Object.keys(funcs).forEach(name => {
const cm = editors.get(this);
const ctx = { ed: this, cm, Editor };
if (name === "initialize") {
funcs[name](ctx);
return;
}
if (this.#prefObserver) {
this.#prefObserver.destroy();
}
// Remove the link between the document and code-mirror.
const cm = editors.get(this);
if (cm?.doc) {
cm.doc.cm = null;
}
// Destroy the CM6 view
if (cm?.destroy) {
cm.destroy();
}
this.emit("destroy");
}
updateCodeFoldingGutter() {
let shouldFoldGutter = this.config.enableCodeFolding;
const foldGutterIndex = this.config.gutters.indexOf(
"CodeMirror-foldgutter"
);
const cm = editors.get(this);
if (shouldFoldGutter === undefined) {
shouldFoldGutter = Services.prefs.getBoolPref(ENABLE_CODE_FOLDING);
}
if (shouldFoldGutter) {
// Add the gutter before enabling foldGutter
if (foldGutterIndex === -1) {
const gutters = this.config.gutters.slice();
gutters.push("CodeMirror-foldgutter");
this.setOption("gutters", gutters);
}
this.setOption("foldGutter", true);
} else {
// No code should remain folded when folding is off.
if (cm) {
cm.execCommand("unfoldAll");
}
// Remove the gutter so it doesn't take up space
if (foldGutterIndex !== -1) {
const gutters = this.config.gutters.slice();
gutters.splice(foldGutterIndex, 1);
this.setOption("gutters", gutters);
}
switch (name) {
// replaceAll.key is Alt + find.key
case "replaceAllMac.key":
this.findOrReplace(node, true);
break;
// replaceAll.key is Shift + find.key
case "replaceAll.key":
this.findOrReplace(node, true);
break;
case "find.key":
this.findOrReplace(node, false);
break;
// findPrev.key is Shift + findNext.key
case "findPrev.key":
this.findNextOrPrev(node, true);
break;
case "findNext.key":
this.findNextOrPrev(node, false);
break;
default:
console.error("Unexpected editor key shortcut", name);
return;
}
// Prevent default for this action
event.stopPropagation();
event.preventDefault();
};
/**
* Check if a node is an input or textarea
*/
#isInputOrTextarea(element) {
const name = element.tagName.toLowerCase();
return name === "input" || name === "textarea";
}
/**
* Parse passed code string and returns an HTML string with the same classes CodeMirror
* adds to handle syntax highlighting.
*
* @param {Document} doc: A document that will be used to create elements
* @param {string} code: The code to highlight
* @returns {string} The HTML string for the parsed code
*/
highlightText(doc, code) {
if (!doc) {
return code;
}
// Since Editor is a thin layer over CodeMirror some methods
// are mapped directly—without any changes.
CM_MAPPING.forEach(name => {
Editor.prototype[name] = function (...args) {
// For CM6 all these methods (do not exist) and are not useful
// so they should do nothing.
if (this.config.cm6) {
throw new Error("This method is not valid for Codemirror 6");
}
const cm = editors.get(this);
return cm[name].apply(cm, args);
};
});
/**
* We compute the CSS property names, values, and color names to be used with
* CodeMirror to more closely reflect what is supported by the target platform.
* The database is used to replace the values used in CodeMirror while initiating
* an editor object. This is done here instead of the file codemirror/css.js so
* as to leave that file untouched and easily upgradable.
*/
function getCSSKeywords(cssProperties) {
function keySet(array) {
const keys = {};
for (let i = 0; i < array.length; ++i) {
keys[array[i]] = true;
}
return keys;
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.