monaco-editor
Version:
A browser based code editor
525 lines (524 loc) • 21.2 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { stringDiff } from '../../../base/common/diff/diff.js';
import { URI } from '../../../base/common/uri.js';
import { Position } from '../core/position.js';
import { Range } from '../core/range.js';
import { MirrorTextModel as BaseMirrorModel } from '../model/mirrorTextModel.js';
import { ensureValidWordDefinition, getWordAtText } from '../core/wordHelper.js';
import { computeLinks } from '../languages/linkComputer.js';
import { BasicInplaceReplace } from '../languages/supports/inplaceReplaceSupport.js';
import { createMonacoBaseAPI } from './editorBaseApi.js';
import { StopWatch } from '../../../base/common/stopwatch.js';
import { UnicodeTextModelHighlighter } from './unicodeTextModelHighlighter.js';
import { linesDiffComputers } from '../diff/linesDiffComputers.js';
import { createProxyObject, getAllMethodNames } from '../../../base/common/objects.js';
import { computeDefaultDocumentColors } from '../languages/defaultDocumentColorsComputer.js';
/**
* @internal
*/
class MirrorModel extends BaseMirrorModel {
get uri() {
return this._uri;
}
get eol() {
return this._eol;
}
getValue() {
return this.getText();
}
findMatches(regex) {
const matches = [];
for (let i = 0; i < this._lines.length; i++) {
const line = this._lines[i];
const offsetToAdd = this.offsetAt(new Position(i + 1, 1));
const iteratorOverMatches = line.matchAll(regex);
for (const match of iteratorOverMatches) {
if (match.index || match.index === 0) {
match.index = match.index + offsetToAdd;
}
matches.push(match);
}
}
return matches;
}
getLinesContent() {
return this._lines.slice(0);
}
getLineCount() {
return this._lines.length;
}
getLineContent(lineNumber) {
return this._lines[lineNumber - 1];
}
getWordAtPosition(position, wordDefinition) {
const wordAtText = getWordAtText(position.column, ensureValidWordDefinition(wordDefinition), this._lines[position.lineNumber - 1], 0);
if (wordAtText) {
return new Range(position.lineNumber, wordAtText.startColumn, position.lineNumber, wordAtText.endColumn);
}
return null;
}
words(wordDefinition) {
const lines = this._lines;
const wordenize = this._wordenize.bind(this);
let lineNumber = 0;
let lineText = '';
let wordRangesIdx = 0;
let wordRanges = [];
return {
*[Symbol.iterator]() {
while (true) {
if (wordRangesIdx < wordRanges.length) {
const value = lineText.substring(wordRanges[wordRangesIdx].start, wordRanges[wordRangesIdx].end);
wordRangesIdx += 1;
yield value;
}
else {
if (lineNumber < lines.length) {
lineText = lines[lineNumber];
wordRanges = wordenize(lineText, wordDefinition);
wordRangesIdx = 0;
lineNumber += 1;
}
else {
break;
}
}
}
}
};
}
getLineWords(lineNumber, wordDefinition) {
const content = this._lines[lineNumber - 1];
const ranges = this._wordenize(content, wordDefinition);
const words = [];
for (const range of ranges) {
words.push({
word: content.substring(range.start, range.end),
startColumn: range.start + 1,
endColumn: range.end + 1
});
}
return words;
}
_wordenize(content, wordDefinition) {
const result = [];
let match;
wordDefinition.lastIndex = 0; // reset lastIndex just to be sure
while (match = wordDefinition.exec(content)) {
if (match[0].length === 0) {
// it did match the empty string
break;
}
result.push({ start: match.index, end: match.index + match[0].length });
}
return result;
}
getValueInRange(range) {
range = this._validateRange(range);
if (range.startLineNumber === range.endLineNumber) {
return this._lines[range.startLineNumber - 1].substring(range.startColumn - 1, range.endColumn - 1);
}
const lineEnding = this._eol;
const startLineIndex = range.startLineNumber - 1;
const endLineIndex = range.endLineNumber - 1;
const resultLines = [];
resultLines.push(this._lines[startLineIndex].substring(range.startColumn - 1));
for (let i = startLineIndex + 1; i < endLineIndex; i++) {
resultLines.push(this._lines[i]);
}
resultLines.push(this._lines[endLineIndex].substring(0, range.endColumn - 1));
return resultLines.join(lineEnding);
}
offsetAt(position) {
position = this._validatePosition(position);
this._ensureLineStarts();
return this._lineStarts.getPrefixSum(position.lineNumber - 2) + (position.column - 1);
}
positionAt(offset) {
offset = Math.floor(offset);
offset = Math.max(0, offset);
this._ensureLineStarts();
const out = this._lineStarts.getIndexOf(offset);
const lineLength = this._lines[out.index].length;
// Ensure we return a valid position
return {
lineNumber: 1 + out.index,
column: 1 + Math.min(out.remainder, lineLength)
};
}
_validateRange(range) {
const start = this._validatePosition({ lineNumber: range.startLineNumber, column: range.startColumn });
const end = this._validatePosition({ lineNumber: range.endLineNumber, column: range.endColumn });
if (start.lineNumber !== range.startLineNumber
|| start.column !== range.startColumn
|| end.lineNumber !== range.endLineNumber
|| end.column !== range.endColumn) {
return {
startLineNumber: start.lineNumber,
startColumn: start.column,
endLineNumber: end.lineNumber,
endColumn: end.column
};
}
return range;
}
_validatePosition(position) {
if (!Position.isIPosition(position)) {
throw new Error('bad position');
}
let { lineNumber, column } = position;
let hasChanged = false;
if (lineNumber < 1) {
lineNumber = 1;
column = 1;
hasChanged = true;
}
else if (lineNumber > this._lines.length) {
lineNumber = this._lines.length;
column = this._lines[lineNumber - 1].length + 1;
hasChanged = true;
}
else {
const maxCharacter = this._lines[lineNumber - 1].length + 1;
if (column < 1) {
column = 1;
hasChanged = true;
}
else if (column > maxCharacter) {
column = maxCharacter;
hasChanged = true;
}
}
if (!hasChanged) {
return position;
}
else {
return { lineNumber, column };
}
}
}
/**
* @internal
*/
export class EditorSimpleWorker {
constructor(host, foreignModuleFactory) {
this._host = host;
this._models = Object.create(null);
this._foreignModuleFactory = foreignModuleFactory;
this._foreignModule = null;
}
dispose() {
this._models = Object.create(null);
}
_getModel(uri) {
return this._models[uri];
}
_getModels() {
const all = [];
Object.keys(this._models).forEach((key) => all.push(this._models[key]));
return all;
}
acceptNewModel(data) {
this._models[data.url] = new MirrorModel(URI.parse(data.url), data.lines, data.EOL, data.versionId);
}
acceptModelChanged(strURL, e) {
if (!this._models[strURL]) {
return;
}
const model = this._models[strURL];
model.onEvents(e);
}
acceptRemovedModel(strURL) {
if (!this._models[strURL]) {
return;
}
delete this._models[strURL];
}
async computeUnicodeHighlights(url, options, range) {
const model = this._getModel(url);
if (!model) {
return { ranges: [], hasMore: false, ambiguousCharacterCount: 0, invisibleCharacterCount: 0, nonBasicAsciiCharacterCount: 0 };
}
return UnicodeTextModelHighlighter.computeUnicodeHighlights(model, options, range);
}
// ---- BEGIN diff --------------------------------------------------------------------------
async computeDiff(originalUrl, modifiedUrl, options, algorithm) {
const original = this._getModel(originalUrl);
const modified = this._getModel(modifiedUrl);
if (!original || !modified) {
return null;
}
const result = EditorSimpleWorker.computeDiff(original, modified, options, algorithm);
return result;
}
static computeDiff(originalTextModel, modifiedTextModel, options, algorithm) {
const diffAlgorithm = algorithm === 'advanced' ? linesDiffComputers.getDefault() : linesDiffComputers.getLegacy();
const originalLines = originalTextModel.getLinesContent();
const modifiedLines = modifiedTextModel.getLinesContent();
const result = diffAlgorithm.computeDiff(originalLines, modifiedLines, options);
const identical = (result.changes.length > 0 ? false : this._modelsAreIdentical(originalTextModel, modifiedTextModel));
function getLineChanges(changes) {
return changes.map(m => {
var _a;
return ([m.original.startLineNumber, m.original.endLineNumberExclusive, m.modified.startLineNumber, m.modified.endLineNumberExclusive, (_a = m.innerChanges) === null || _a === void 0 ? void 0 : _a.map(m => [
m.originalRange.startLineNumber,
m.originalRange.startColumn,
m.originalRange.endLineNumber,
m.originalRange.endColumn,
m.modifiedRange.startLineNumber,
m.modifiedRange.startColumn,
m.modifiedRange.endLineNumber,
m.modifiedRange.endColumn,
])]);
});
}
return {
identical,
quitEarly: result.hitTimeout,
changes: getLineChanges(result.changes),
moves: result.moves.map(m => ([
m.lineRangeMapping.original.startLineNumber,
m.lineRangeMapping.original.endLineNumberExclusive,
m.lineRangeMapping.modified.startLineNumber,
m.lineRangeMapping.modified.endLineNumberExclusive,
getLineChanges(m.changes)
])),
};
}
static _modelsAreIdentical(original, modified) {
const originalLineCount = original.getLineCount();
const modifiedLineCount = modified.getLineCount();
if (originalLineCount !== modifiedLineCount) {
return false;
}
for (let line = 1; line <= originalLineCount; line++) {
const originalLine = original.getLineContent(line);
const modifiedLine = modified.getLineContent(line);
if (originalLine !== modifiedLine) {
return false;
}
}
return true;
}
async computeMoreMinimalEdits(modelUrl, edits, pretty) {
const model = this._getModel(modelUrl);
if (!model) {
return edits;
}
const result = [];
let lastEol = undefined;
edits = edits.slice(0).sort((a, b) => {
if (a.range && b.range) {
return Range.compareRangesUsingStarts(a.range, b.range);
}
// eol only changes should go to the end
const aRng = a.range ? 0 : 1;
const bRng = b.range ? 0 : 1;
return aRng - bRng;
});
// merge adjacent edits
let writeIndex = 0;
for (let readIndex = 1; readIndex < edits.length; readIndex++) {
if (Range.getEndPosition(edits[writeIndex].range).equals(Range.getStartPosition(edits[readIndex].range))) {
edits[writeIndex].range = Range.fromPositions(Range.getStartPosition(edits[writeIndex].range), Range.getEndPosition(edits[readIndex].range));
edits[writeIndex].text += edits[readIndex].text;
}
else {
writeIndex++;
edits[writeIndex] = edits[readIndex];
}
}
edits.length = writeIndex + 1;
for (let { range, text, eol } of edits) {
if (typeof eol === 'number') {
lastEol = eol;
}
if (Range.isEmpty(range) && !text) {
// empty change
continue;
}
const original = model.getValueInRange(range);
text = text.replace(/\r\n|\n|\r/g, model.eol);
if (original === text) {
// noop
continue;
}
// make sure diff won't take too long
if (Math.max(text.length, original.length) > EditorSimpleWorker._diffLimit) {
result.push({ range, text });
continue;
}
// compute diff between original and edit.text
const changes = stringDiff(original, text, pretty);
const editOffset = model.offsetAt(Range.lift(range).getStartPosition());
for (const change of changes) {
const start = model.positionAt(editOffset + change.originalStart);
const end = model.positionAt(editOffset + change.originalStart + change.originalLength);
const newEdit = {
text: text.substr(change.modifiedStart, change.modifiedLength),
range: { startLineNumber: start.lineNumber, startColumn: start.column, endLineNumber: end.lineNumber, endColumn: end.column }
};
if (model.getValueInRange(newEdit.range) !== newEdit.text) {
result.push(newEdit);
}
}
}
if (typeof lastEol === 'number') {
result.push({ eol: lastEol, text: '', range: { startLineNumber: 0, startColumn: 0, endLineNumber: 0, endColumn: 0 } });
}
return result;
}
// ---- END minimal edits ---------------------------------------------------------------
async computeLinks(modelUrl) {
const model = this._getModel(modelUrl);
if (!model) {
return null;
}
return computeLinks(model);
}
// --- BEGIN default document colors -----------------------------------------------------------
async computeDefaultDocumentColors(modelUrl) {
const model = this._getModel(modelUrl);
if (!model) {
return null;
}
return computeDefaultDocumentColors(model);
}
async textualSuggest(modelUrls, leadingWord, wordDef, wordDefFlags) {
const sw = new StopWatch();
const wordDefRegExp = new RegExp(wordDef, wordDefFlags);
const seen = new Set();
outer: for (const url of modelUrls) {
const model = this._getModel(url);
if (!model) {
continue;
}
for (const word of model.words(wordDefRegExp)) {
if (word === leadingWord || !isNaN(Number(word))) {
continue;
}
seen.add(word);
if (seen.size > EditorSimpleWorker._suggestionsLimit) {
break outer;
}
}
}
return { words: Array.from(seen), duration: sw.elapsed() };
}
// ---- END suggest --------------------------------------------------------------------------
//#region -- word ranges --
async computeWordRanges(modelUrl, range, wordDef, wordDefFlags) {
const model = this._getModel(modelUrl);
if (!model) {
return Object.create(null);
}
const wordDefRegExp = new RegExp(wordDef, wordDefFlags);
const result = Object.create(null);
for (let line = range.startLineNumber; line < range.endLineNumber; line++) {
const words = model.getLineWords(line, wordDefRegExp);
for (const word of words) {
if (!isNaN(Number(word.word))) {
continue;
}
let array = result[word.word];
if (!array) {
array = [];
result[word.word] = array;
}
array.push({
startLineNumber: line,
startColumn: word.startColumn,
endLineNumber: line,
endColumn: word.endColumn
});
}
}
return result;
}
//#endregion
async navigateValueSet(modelUrl, range, up, wordDef, wordDefFlags) {
const model = this._getModel(modelUrl);
if (!model) {
return null;
}
const wordDefRegExp = new RegExp(wordDef, wordDefFlags);
if (range.startColumn === range.endColumn) {
range = {
startLineNumber: range.startLineNumber,
startColumn: range.startColumn,
endLineNumber: range.endLineNumber,
endColumn: range.endColumn + 1
};
}
const selectionText = model.getValueInRange(range);
const wordRange = model.getWordAtPosition({ lineNumber: range.startLineNumber, column: range.startColumn }, wordDefRegExp);
if (!wordRange) {
return null;
}
const word = model.getValueInRange(wordRange);
const result = BasicInplaceReplace.INSTANCE.navigateValueSet(range, selectionText, wordRange, word, up);
return result;
}
// ---- BEGIN foreign module support --------------------------------------------------------------------------
loadForeignModule(moduleId, createData, foreignHostMethods) {
const proxyMethodRequest = (method, args) => {
return this._host.fhr(method, args);
};
const foreignHost = createProxyObject(foreignHostMethods, proxyMethodRequest);
const ctx = {
host: foreignHost,
getMirrorModels: () => {
return this._getModels();
}
};
if (this._foreignModuleFactory) {
this._foreignModule = this._foreignModuleFactory(ctx, createData);
// static foreing module
return Promise.resolve(getAllMethodNames(this._foreignModule));
}
// ESM-comment-begin
// return new Promise<any>((resolve, reject) => {
// require([moduleId], (foreignModule: { create: IForeignModuleFactory }) => {
// this._foreignModule = foreignModule.create(ctx, createData);
//
// resolve(getAllMethodNames(this._foreignModule));
//
// }, reject);
// });
// ESM-comment-end
// ESM-uncomment-begin
return Promise.reject(new Error(`Unexpected usage`));
// ESM-uncomment-end
}
// foreign method request
fmr(method, args) {
if (!this._foreignModule || typeof this._foreignModule[method] !== 'function') {
return Promise.reject(new Error('Missing requestHandler or method: ' + method));
}
try {
return Promise.resolve(this._foreignModule[method].apply(this._foreignModule, args));
}
catch (e) {
return Promise.reject(e);
}
}
}
// ---- END diff --------------------------------------------------------------------------
// ---- BEGIN minimal edits ---------------------------------------------------------------
EditorSimpleWorker._diffLimit = 100000;
// ---- BEGIN suggest --------------------------------------------------------------------------
EditorSimpleWorker._suggestionsLimit = 10000;
/**
* Called on the worker side
* @internal
*/
export function create(host) {
return new EditorSimpleWorker(host, null);
}
if (typeof importScripts === 'function') {
// Running in a web worker
globalThis.monaco = createMonacoBaseAPI();
}