Path: blob/main/src/vs/editor/common/services/editorWebWorker.ts
3294 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { stringDiff } from '../../../base/common/diff/diff.js';6import { IDisposable } from '../../../base/common/lifecycle.js';7import { URI } from '../../../base/common/uri.js';8import { IWebWorkerServerRequestHandler } from '../../../base/common/worker/webWorker.js';9import { Position } from '../core/position.js';10import { IRange, Range } from '../core/range.js';11import { EndOfLineSequence, ITextModel } from '../model.js';12import { IMirrorTextModel, IModelChangedEvent } from '../model/mirrorTextModel.js';13import { IColorInformation, IInplaceReplaceSupportResult, ILink, TextEdit } from '../languages.js';14import { computeLinks } from '../languages/linkComputer.js';15import { BasicInplaceReplace } from '../languages/supports/inplaceReplaceSupport.js';16import { DiffAlgorithmName, IDiffComputationResult, ILineChange, IUnicodeHighlightsResult } from './editorWorker.js';17import { createMonacoBaseAPI } from './editorBaseApi.js';18import { StopWatch } from '../../../base/common/stopwatch.js';19import { UnicodeTextModelHighlighter, UnicodeHighlighterOptions } from './unicodeTextModelHighlighter.js';20import { DiffComputer, IChange } from '../diff/legacyLinesDiffComputer.js';21import { ILinesDiffComputer, ILinesDiffComputerOptions } from '../diff/linesDiffComputer.js';22import { DetailedLineRangeMapping } from '../diff/rangeMapping.js';23import { linesDiffComputers } from '../diff/linesDiffComputers.js';24import { IDocumentDiffProviderOptions } from '../diff/documentDiffProvider.js';25import { BugIndicatingError } from '../../../base/common/errors.js';26import { computeDefaultDocumentColors } from '../languages/defaultDocumentColorsComputer.js';27import { FindSectionHeaderOptions, SectionHeader, findSectionHeaders } from './findSectionHeaders.js';28import { IRawModelData, IWorkerTextModelSyncChannelServer } from './textModelSync/textModelSync.protocol.js';29import { ICommonModel, WorkerTextModelSyncServer } from './textModelSync/textModelSync.impl.js';30import { ISerializedStringEdit, StringEdit } from '../core/edits/stringEdit.js';31import { StringText } from '../core/text/abstractText.js';32import { ensureDependenciesAreSet } from '../core/text/positionToOffset.js';3334export interface IMirrorModel extends IMirrorTextModel {35readonly uri: URI;36readonly version: number;37getValue(): string;38}3940export interface IWorkerContext<H = {}> {41/**42* A proxy to the main thread host object.43*/44host: H;45/**46* Get all available mirror models in this worker.47*/48getMirrorModels(): IMirrorModel[];49}5051/**52* Range of a word inside a model.53* @internal54*/55export interface IWordRange {56/**57* The index where the word starts.58*/59readonly start: number;60/**61* The index where the word ends.62*/63readonly end: number;64}6566/**67* @internal68*/69export class EditorWorker implements IDisposable, IWorkerTextModelSyncChannelServer, IWebWorkerServerRequestHandler {70_requestHandlerBrand: any;7172private readonly _workerTextModelSyncServer = new WorkerTextModelSyncServer();7374constructor(75private readonly _foreignModule: any | null = null76) { }7778dispose(): void {79}8081public async $ping() {82return 'pong';83}8485protected _getModel(uri: string): ICommonModel | undefined {86return this._workerTextModelSyncServer.getModel(uri);87}8889public getModels(): ICommonModel[] {90return this._workerTextModelSyncServer.getModels();91}9293public $acceptNewModel(data: IRawModelData): void {94this._workerTextModelSyncServer.$acceptNewModel(data);95}9697public $acceptModelChanged(uri: string, e: IModelChangedEvent): void {98this._workerTextModelSyncServer.$acceptModelChanged(uri, e);99}100101public $acceptRemovedModel(uri: string): void {102this._workerTextModelSyncServer.$acceptRemovedModel(uri);103}104105public async $computeUnicodeHighlights(url: string, options: UnicodeHighlighterOptions, range?: IRange): Promise<IUnicodeHighlightsResult> {106const model = this._getModel(url);107if (!model) {108return { ranges: [], hasMore: false, ambiguousCharacterCount: 0, invisibleCharacterCount: 0, nonBasicAsciiCharacterCount: 0 };109}110return UnicodeTextModelHighlighter.computeUnicodeHighlights(model, options, range);111}112113public async $findSectionHeaders(url: string, options: FindSectionHeaderOptions): Promise<SectionHeader[]> {114const model = this._getModel(url);115if (!model) {116return [];117}118return findSectionHeaders(model, options);119}120121// ---- BEGIN diff --------------------------------------------------------------------------122123public async $computeDiff(originalUrl: string, modifiedUrl: string, options: IDocumentDiffProviderOptions, algorithm: DiffAlgorithmName): Promise<IDiffComputationResult | null> {124const original = this._getModel(originalUrl);125const modified = this._getModel(modifiedUrl);126if (!original || !modified) {127return null;128}129130const result = EditorWorker.computeDiff(original, modified, options, algorithm);131return result;132}133134private static computeDiff(originalTextModel: ICommonModel | ITextModel, modifiedTextModel: ICommonModel | ITextModel, options: IDocumentDiffProviderOptions, algorithm: DiffAlgorithmName): IDiffComputationResult {135const diffAlgorithm: ILinesDiffComputer = algorithm === 'advanced' ? linesDiffComputers.getDefault() : linesDiffComputers.getLegacy();136137const originalLines = originalTextModel.getLinesContent();138const modifiedLines = modifiedTextModel.getLinesContent();139140const result = diffAlgorithm.computeDiff(originalLines, modifiedLines, options);141142const identical = (result.changes.length > 0 ? false : this._modelsAreIdentical(originalTextModel, modifiedTextModel));143144function getLineChanges(changes: readonly DetailedLineRangeMapping[]): ILineChange[] {145return changes.map(m => ([m.original.startLineNumber, m.original.endLineNumberExclusive, m.modified.startLineNumber, m.modified.endLineNumberExclusive, m.innerChanges?.map(m => [146m.originalRange.startLineNumber,147m.originalRange.startColumn,148m.originalRange.endLineNumber,149m.originalRange.endColumn,150m.modifiedRange.startLineNumber,151m.modifiedRange.startColumn,152m.modifiedRange.endLineNumber,153m.modifiedRange.endColumn,154])]));155}156157return {158identical,159quitEarly: result.hitTimeout,160changes: getLineChanges(result.changes),161moves: result.moves.map(m => ([162m.lineRangeMapping.original.startLineNumber,163m.lineRangeMapping.original.endLineNumberExclusive,164m.lineRangeMapping.modified.startLineNumber,165m.lineRangeMapping.modified.endLineNumberExclusive,166getLineChanges(m.changes)167])),168};169}170171private static _modelsAreIdentical(original: ICommonModel | ITextModel, modified: ICommonModel | ITextModel): boolean {172const originalLineCount = original.getLineCount();173const modifiedLineCount = modified.getLineCount();174if (originalLineCount !== modifiedLineCount) {175return false;176}177for (let line = 1; line <= originalLineCount; line++) {178const originalLine = original.getLineContent(line);179const modifiedLine = modified.getLineContent(line);180if (originalLine !== modifiedLine) {181return false;182}183}184return true;185}186187public async $computeDirtyDiff(originalUrl: string, modifiedUrl: string, ignoreTrimWhitespace: boolean): Promise<IChange[] | null> {188const original = this._getModel(originalUrl);189const modified = this._getModel(modifiedUrl);190if (!original || !modified) {191return null;192}193194const originalLines = original.getLinesContent();195const modifiedLines = modified.getLinesContent();196const diffComputer = new DiffComputer(originalLines, modifiedLines, {197shouldComputeCharChanges: false,198shouldPostProcessCharChanges: false,199shouldIgnoreTrimWhitespace: ignoreTrimWhitespace,200shouldMakePrettyDiff: true,201maxComputationTime: 1000202});203return diffComputer.computeDiff().changes;204}205206public $computeStringDiff(original: string, modified: string, options: { maxComputationTimeMs: number }, algorithm: DiffAlgorithmName): ISerializedStringEdit {207return computeStringDiff(original, modified, options, algorithm).toJson();208}209210// ---- END diff --------------------------------------------------------------------------211212213// ---- BEGIN minimal edits ---------------------------------------------------------------214215private static readonly _diffLimit = 100000;216217public async $computeMoreMinimalEdits(modelUrl: string, edits: TextEdit[], pretty: boolean): Promise<TextEdit[]> {218const model = this._getModel(modelUrl);219if (!model) {220return edits;221}222223const result: TextEdit[] = [];224let lastEol: EndOfLineSequence | undefined = undefined;225226edits = edits.slice(0).sort((a, b) => {227if (a.range && b.range) {228return Range.compareRangesUsingStarts(a.range, b.range);229}230// eol only changes should go to the end231const aRng = a.range ? 0 : 1;232const bRng = b.range ? 0 : 1;233return aRng - bRng;234});235236// merge adjacent edits237let writeIndex = 0;238for (let readIndex = 1; readIndex < edits.length; readIndex++) {239if (Range.getEndPosition(edits[writeIndex].range).equals(Range.getStartPosition(edits[readIndex].range))) {240edits[writeIndex].range = Range.fromPositions(Range.getStartPosition(edits[writeIndex].range), Range.getEndPosition(edits[readIndex].range));241edits[writeIndex].text += edits[readIndex].text;242} else {243writeIndex++;244edits[writeIndex] = edits[readIndex];245}246}247edits.length = writeIndex + 1;248249for (let { range, text, eol } of edits) {250251if (typeof eol === 'number') {252lastEol = eol;253}254255if (Range.isEmpty(range) && !text) {256// empty change257continue;258}259260const original = model.getValueInRange(range);261text = text.replace(/\r\n|\n|\r/g, model.eol);262263if (original === text) {264// noop265continue;266}267268// make sure diff won't take too long269if (Math.max(text.length, original.length) > EditorWorker._diffLimit) {270result.push({ range, text });271continue;272}273274// compute diff between original and edit.text275const changes = stringDiff(original, text, pretty);276const editOffset = model.offsetAt(Range.lift(range).getStartPosition());277278for (const change of changes) {279const start = model.positionAt(editOffset + change.originalStart);280const end = model.positionAt(editOffset + change.originalStart + change.originalLength);281const newEdit: TextEdit = {282text: text.substr(change.modifiedStart, change.modifiedLength),283range: { startLineNumber: start.lineNumber, startColumn: start.column, endLineNumber: end.lineNumber, endColumn: end.column }284};285286if (model.getValueInRange(newEdit.range) !== newEdit.text) {287result.push(newEdit);288}289}290}291292if (typeof lastEol === 'number') {293result.push({ eol: lastEol, text: '', range: { startLineNumber: 0, startColumn: 0, endLineNumber: 0, endColumn: 0 } });294}295296return result;297}298299public $computeHumanReadableDiff(modelUrl: string, edits: TextEdit[], options: ILinesDiffComputerOptions): TextEdit[] {300const model = this._getModel(modelUrl);301if (!model) {302return edits;303}304305const result: TextEdit[] = [];306let lastEol: EndOfLineSequence | undefined = undefined;307308edits = edits.slice(0).sort((a, b) => {309if (a.range && b.range) {310return Range.compareRangesUsingStarts(a.range, b.range);311}312// eol only changes should go to the end313const aRng = a.range ? 0 : 1;314const bRng = b.range ? 0 : 1;315return aRng - bRng;316});317318for (let { range, text, eol } of edits) {319320if (typeof eol === 'number') {321lastEol = eol;322}323324if (Range.isEmpty(range) && !text) {325// empty change326continue;327}328329const original = model.getValueInRange(range);330text = text.replace(/\r\n|\n|\r/g, model.eol);331332if (original === text) {333// noop334continue;335}336337// make sure diff won't take too long338if (Math.max(text.length, original.length) > EditorWorker._diffLimit) {339result.push({ range, text });340continue;341}342343// compute diff between original and edit.text344345const originalLines = original.split(/\r\n|\n|\r/);346const modifiedLines = text.split(/\r\n|\n|\r/);347348const diff = linesDiffComputers.getDefault().computeDiff(originalLines, modifiedLines, options);349350const start = Range.lift(range).getStartPosition();351352function addPositions(pos1: Position, pos2: Position): Position {353return new Position(pos1.lineNumber + pos2.lineNumber - 1, pos2.lineNumber === 1 ? pos1.column + pos2.column - 1 : pos2.column);354}355356function getText(lines: string[], range: Range): string[] {357const result: string[] = [];358for (let i = range.startLineNumber; i <= range.endLineNumber; i++) {359const line = lines[i - 1];360if (i === range.startLineNumber && i === range.endLineNumber) {361result.push(line.substring(range.startColumn - 1, range.endColumn - 1));362} else if (i === range.startLineNumber) {363result.push(line.substring(range.startColumn - 1));364} else if (i === range.endLineNumber) {365result.push(line.substring(0, range.endColumn - 1));366} else {367result.push(line);368}369}370return result;371}372373for (const c of diff.changes) {374if (c.innerChanges) {375for (const x of c.innerChanges) {376result.push({377range: Range.fromPositions(378addPositions(start, x.originalRange.getStartPosition()),379addPositions(start, x.originalRange.getEndPosition())380),381text: getText(modifiedLines, x.modifiedRange).join(model.eol)382});383}384} else {385throw new BugIndicatingError('The experimental diff algorithm always produces inner changes');386}387}388}389390if (typeof lastEol === 'number') {391result.push({ eol: lastEol, text: '', range: { startLineNumber: 0, startColumn: 0, endLineNumber: 0, endColumn: 0 } });392}393394return result;395}396397// ---- END minimal edits ---------------------------------------------------------------398399public async $computeLinks(modelUrl: string): Promise<ILink[] | null> {400const model = this._getModel(modelUrl);401if (!model) {402return null;403}404405return computeLinks(model);406}407408// --- BEGIN default document colors -----------------------------------------------------------409410public async $computeDefaultDocumentColors(modelUrl: string): Promise<IColorInformation[] | null> {411const model = this._getModel(modelUrl);412if (!model) {413return null;414}415return computeDefaultDocumentColors(model);416}417418// ---- BEGIN suggest --------------------------------------------------------------------------419420private static readonly _suggestionsLimit = 10000;421422public async $textualSuggest(modelUrls: string[], leadingWord: string | undefined, wordDef: string, wordDefFlags: string): Promise<{ words: string[]; duration: number } | null> {423424const sw = new StopWatch();425const wordDefRegExp = new RegExp(wordDef, wordDefFlags);426const seen = new Set<string>();427428outer: for (const url of modelUrls) {429const model = this._getModel(url);430if (!model) {431continue;432}433434for (const word of model.words(wordDefRegExp)) {435if (word === leadingWord || !isNaN(Number(word))) {436continue;437}438seen.add(word);439if (seen.size > EditorWorker._suggestionsLimit) {440break outer;441}442}443}444445return { words: Array.from(seen), duration: sw.elapsed() };446}447448449// ---- END suggest --------------------------------------------------------------------------450451//#region -- word ranges --452453public async $computeWordRanges(modelUrl: string, range: IRange, wordDef: string, wordDefFlags: string): Promise<{ [word: string]: IRange[] }> {454const model = this._getModel(modelUrl);455if (!model) {456return Object.create(null);457}458const wordDefRegExp = new RegExp(wordDef, wordDefFlags);459const result: { [word: string]: IRange[] } = Object.create(null);460for (let line = range.startLineNumber; line < range.endLineNumber; line++) {461const words = model.getLineWords(line, wordDefRegExp);462for (const word of words) {463if (!isNaN(Number(word.word))) {464continue;465}466let array = result[word.word];467if (!array) {468array = [];469result[word.word] = array;470}471array.push({472startLineNumber: line,473startColumn: word.startColumn,474endLineNumber: line,475endColumn: word.endColumn476});477}478}479return result;480}481482//#endregion483484public async $navigateValueSet(modelUrl: string, range: IRange, up: boolean, wordDef: string, wordDefFlags: string): Promise<IInplaceReplaceSupportResult | null> {485const model = this._getModel(modelUrl);486if (!model) {487return null;488}489490const wordDefRegExp = new RegExp(wordDef, wordDefFlags);491492if (range.startColumn === range.endColumn) {493range = {494startLineNumber: range.startLineNumber,495startColumn: range.startColumn,496endLineNumber: range.endLineNumber,497endColumn: range.endColumn + 1498};499}500501const selectionText = model.getValueInRange(range);502503const wordRange = model.getWordAtPosition({ lineNumber: range.startLineNumber, column: range.startColumn }, wordDefRegExp);504if (!wordRange) {505return null;506}507const word = model.getValueInRange(wordRange);508const result = BasicInplaceReplace.INSTANCE.navigateValueSet(range, selectionText, wordRange, word, up);509return result;510}511512// ---- BEGIN foreign module support --------------------------------------------------------------------------513514// foreign method request515public $fmr(method: string, args: any[]): Promise<any> {516if (!this._foreignModule || typeof this._foreignModule[method] !== 'function') {517return Promise.reject(new Error('Missing requestHandler or method: ' + method));518}519520try {521return Promise.resolve(this._foreignModule[method].apply(this._foreignModule, args));522} catch (e) {523return Promise.reject(e);524}525}526527// ---- END foreign module support --------------------------------------------------------------------------528}529530// This is only available in a Web Worker531declare function importScripts(...urls: string[]): void;532533if (typeof importScripts === 'function') {534// Running in a web worker535globalThis.monaco = createMonacoBaseAPI();536}537538/**539* @internal540*/541export function computeStringDiff(original: string, modified: string, options: { maxComputationTimeMs: number }, algorithm: DiffAlgorithmName): StringEdit {542const diffAlgorithm: ILinesDiffComputer = algorithm === 'advanced' ? linesDiffComputers.getDefault() : linesDiffComputers.getLegacy();543544ensureDependenciesAreSet();545546const originalText = new StringText(original);547const originalLines = originalText.getLines();548const modifiedText = new StringText(modified);549const modifiedLines = modifiedText.getLines();550551const result = diffAlgorithm.computeDiff(originalLines, modifiedLines, { ignoreTrimWhitespace: false, maxComputationTimeMs: options.maxComputationTimeMs, computeMoves: false, extendToSubwords: false });552553const textEdit = DetailedLineRangeMapping.toTextEdit(result.changes, modifiedText);554const strEdit = originalText.getTransformer().getStringEdit(textEdit);555556return strEdit;557}558559560