UNPKG

monaco-editor

Version:
525 lines (524 loc) • 21.2 kB
/*--------------------------------------------------------------------------------------------- * 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(); }