vscode/src/vs/editor/common/services/editorSimpleWorker.ts

695 lines
20 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* 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 'vs/base/common/diff/diff';
import { IDisposable } from 'vs/base/common/lifecycle';
import { globals } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import { IRequestHandler } from 'vs/base/common/worker/simpleWorker';
import { IPosition, Position } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
import { DiffComputer } from 'vs/editor/common/diff/diffComputer';
import { IChange } from 'vs/editor/common/editorCommon';
import { EndOfLineSequence, IWordAtPosition } from 'vs/editor/common/model';
import { IMirrorTextModel, IModelChangedEvent, MirrorTextModel as BaseMirrorModel } from 'vs/editor/common/model/mirrorTextModel';
import { ensureValidWordDefinition, getWordAtText } from 'vs/editor/common/model/wordHelper';
import { IInplaceReplaceSupportResult, ILink, TextEdit } from 'vs/editor/common/modes';
import { ILinkComputerTarget, computeLinks } from 'vs/editor/common/modes/linkComputer';
import { BasicInplaceReplace } from 'vs/editor/common/modes/supports/inplaceReplaceSupport';
import { IDiffComputationResult, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorkerService';
import { createMonacoBaseAPI } from 'vs/editor/common/standalone/standaloneBase';
import * as types from 'vs/base/common/types';
import { EditorWorkerHost } from 'vs/editor/common/services/editorWorkerServiceImpl';
import { StopWatch } from 'vs/base/common/stopwatch';
import { UnicodeTextModelHighlighter, UnicodeHighlighterOptions } from 'vs/editor/common/modes/unicodeTextModelHighlighter';
export interface IMirrorModel extends IMirrorTextModel {
readonly uri: URI;
readonly version: number;
getValue(): string;
}
export interface IWorkerContext<H = undefined> {
/**
* A proxy to the main thread host object.
*/
host: H;
/**
* Get all available mirror models in this worker.
*/
getMirrorModels(): IMirrorModel[];
}
/**
* @internal
*/
export interface IRawModelData {
url: string;
versionId: number;
lines: string[];
EOL: string;
}
/**
* @internal
*/
export interface ICommonModel extends ILinkComputerTarget, IMirrorModel {
uri: URI;
version: number;
eol: string;
getValue(): string;
getLinesContent(): string[];
getLineCount(): number;
getLineContent(lineNumber: number): string;
getLineWords(lineNumber: number, wordDefinition: RegExp): IWordAtPosition[];
words(wordDefinition: RegExp): Iterable<string>;
getWordUntilPosition(position: IPosition, wordDefinition: RegExp): IWordAtPosition;
getValueInRange(range: IRange): string;
getWordAtPosition(position: IPosition, wordDefinition: RegExp): Range | null;
offsetAt(position: IPosition): number;
positionAt(offset: number): IPosition;
}
/**
* Range of a word inside a model.
* @internal
*/
export interface IWordRange {
/**
* The index where the word starts.
*/
readonly start: number;
/**
* The index where the word ends.
*/
readonly end: number;
}
/**
* @internal
*/
export class MirrorModel extends BaseMirrorModel implements ICommonModel {
public get uri(): URI {
return this._uri;
}
public get eol(): string {
return this._eol;
}
public getValue(): string {
return this.getText();
}
public getLinesContent(): string[] {
return this._lines.slice(0);
}
public getLineCount(): number {
return this._lines.length;
}
public getLineContent(lineNumber: number): string {
return this._lines[lineNumber - 1];
}
public getWordAtPosition(position: IPosition, wordDefinition: RegExp): Range | null {
let 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;
}
public getWordUntilPosition(position: IPosition, wordDefinition: RegExp): IWordAtPosition {
const wordAtPosition = this.getWordAtPosition(position, wordDefinition);
if (!wordAtPosition) {
return {
word: '',
startColumn: position.column,
endColumn: position.column
};
}
return {
word: this._lines[position.lineNumber - 1].substring(wordAtPosition.startColumn - 1, position.column - 1),
startColumn: wordAtPosition.startColumn,
endColumn: position.column
};
}
public words(wordDefinition: RegExp): Iterable<string> {
const lines = this._lines;
const wordenize = this._wordenize.bind(this);
let lineNumber = 0;
let lineText = '';
let wordRangesIdx = 0;
let wordRanges: IWordRange[] = [];
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;
}
}
}
}
};
}
public getLineWords(lineNumber: number, wordDefinition: RegExp): IWordAtPosition[] {
let content = this._lines[lineNumber - 1];
let ranges = this._wordenize(content, wordDefinition);
let words: IWordAtPosition[] = [];
for (const range of ranges) {
words.push({
word: content.substring(range.start, range.end),
startColumn: range.start + 1,
endColumn: range.end + 1
});
}
return words;
}
private _wordenize(content: string, wordDefinition: RegExp): IWordRange[] {
const result: IWordRange[] = [];
let match: RegExpExecArray | null;
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;
}
public getValueInRange(range: IRange): string {
range = this._validateRange(range);
if (range.startLineNumber === range.endLineNumber) {
return this._lines[range.startLineNumber - 1].substring(range.startColumn - 1, range.endColumn - 1);
}
let lineEnding = this._eol;
let startLineIndex = range.startLineNumber - 1;
let endLineIndex = range.endLineNumber - 1;
let resultLines: string[] = [];
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);
}
public offsetAt(position: IPosition): number {
position = this._validatePosition(position);
this._ensureLineStarts();
return this._lineStarts!.getPrefixSum(position.lineNumber - 2) + (position.column - 1);
}
public positionAt(offset: number): IPosition {
offset = Math.floor(offset);
offset = Math.max(0, offset);
this._ensureLineStarts();
let out = this._lineStarts!.getIndexOf(offset);
let lineLength = this._lines[out.index].length;
// Ensure we return a valid position
return {
lineNumber: 1 + out.index,
column: 1 + Math.min(out.remainder, lineLength)
};
}
private _validateRange(range: IRange): IRange {
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;
}
private _validatePosition(position: IPosition): IPosition {
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 {
let 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 interface IForeignModuleFactory {
(ctx: IWorkerContext, createData: any): any;
}
declare const require: any;
/**
* @internal
*/
export class EditorSimpleWorker implements IRequestHandler, IDisposable {
_requestHandlerBrand: any;
protected readonly _host: EditorWorkerHost;
private _models: { [uri: string]: MirrorModel; };
private readonly _foreignModuleFactory: IForeignModuleFactory | null;
private _foreignModule: any;
constructor(host: EditorWorkerHost, foreignModuleFactory: IForeignModuleFactory | null) {
this._host = host;
this._models = Object.create(null);
this._foreignModuleFactory = foreignModuleFactory;
this._foreignModule = null;
}
public dispose(): void {
this._models = Object.create(null);
}
protected _getModel(uri: string): ICommonModel {
return this._models[uri];
}
private _getModels(): ICommonModel[] {
let all: MirrorModel[] = [];
Object.keys(this._models).forEach((key) => all.push(this._models[key]));
return all;
}
public acceptNewModel(data: IRawModelData): void {
this._models[data.url] = new MirrorModel(URI.parse(data.url), data.lines, data.EOL, data.versionId);
}
public acceptModelChanged(strURL: string, e: IModelChangedEvent): void {
if (!this._models[strURL]) {
return;
}
let model = this._models[strURL];
model.onEvents(e);
}
public acceptRemovedModel(strURL: string): void {
if (!this._models[strURL]) {
return;
}
delete this._models[strURL];
}
public async computeUnicodeHighlights(url: string, options: UnicodeHighlighterOptions, range?: IRange): Promise<IUnicodeHighlightsResult> {
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 --------------------------------------------------------------------------
public async computeDiff(originalUrl: string, modifiedUrl: string, ignoreTrimWhitespace: boolean, maxComputationTime: number): Promise<IDiffComputationResult | null> {
const original = this._getModel(originalUrl);
const modified = this._getModel(modifiedUrl);
if (!original || !modified) {
return null;
}
const originalLines = original.getLinesContent();
const modifiedLines = modified.getLinesContent();
const diffComputer = new DiffComputer(originalLines, modifiedLines, {
shouldComputeCharChanges: true,
shouldPostProcessCharChanges: true,
shouldIgnoreTrimWhitespace: ignoreTrimWhitespace,
shouldMakePrettyDiff: true,
maxComputationTime: maxComputationTime
});
const diffResult = diffComputer.computeDiff();
const identical = (diffResult.changes.length > 0 ? false : this._modelsAreIdentical(original, modified));
return {
quitEarly: diffResult.quitEarly,
identical: identical,
changes: diffResult.changes
};
}
private _modelsAreIdentical(original: ICommonModel, modified: ICommonModel): boolean {
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;
}
public async computeDirtyDiff(originalUrl: string, modifiedUrl: string, ignoreTrimWhitespace: boolean): Promise<IChange[] | null> {
let original = this._getModel(originalUrl);
let modified = this._getModel(modifiedUrl);
if (!original || !modified) {
return null;
}
let originalLines = original.getLinesContent();
let modifiedLines = modified.getLinesContent();
let diffComputer = new DiffComputer(originalLines, modifiedLines, {
shouldComputeCharChanges: false,
shouldPostProcessCharChanges: false,
shouldIgnoreTrimWhitespace: ignoreTrimWhitespace,
shouldMakePrettyDiff: true,
maxComputationTime: 1000
});
return diffComputer.computeDiff().changes;
}
// ---- END diff --------------------------------------------------------------------------
// ---- BEGIN minimal edits ---------------------------------------------------------------
private static readonly _diffLimit = 100000;
public async computeMoreMinimalEdits(modelUrl: string, edits: TextEdit[]): Promise<TextEdit[]> {
const model = this._getModel(modelUrl);
if (!model) {
return edits;
}
const result: TextEdit[] = [];
let lastEol: EndOfLineSequence | undefined = 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
let aRng = a.range ? 0 : 1;
let bRng = b.range ? 0 : 1;
return aRng - bRng;
});
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, false);
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: TextEdit = {
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 ---------------------------------------------------------------
public async computeLinks(modelUrl: string): Promise<ILink[] | null> {
let model = this._getModel(modelUrl);
if (!model) {
return null;
}
return computeLinks(model);
}
// ---- BEGIN suggest --------------------------------------------------------------------------
private static readonly _suggestionsLimit = 10000;
public async textualSuggest(modelUrls: string[], leadingWord: string | undefined, wordDef: string, wordDefFlags: string): Promise<{ words: string[], duration: number } | null> {
const sw = new StopWatch(true);
const wordDefRegExp = new RegExp(wordDef, wordDefFlags);
const seen = new Set<string>();
outer: for (let url of modelUrls) {
const model = this._getModel(url);
if (!model) {
continue;
}
for (let 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 --
public async computeWordRanges(modelUrl: string, range: IRange, wordDef: string, wordDefFlags: string): Promise<{ [word: string]: IRange[] }> {
let model = this._getModel(modelUrl);
if (!model) {
return Object.create(null);
}
const wordDefRegExp = new RegExp(wordDef, wordDefFlags);
const result: { [word: string]: IRange[] } = Object.create(null);
for (let line = range.startLineNumber; line < range.endLineNumber; line++) {
let 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
public async navigateValueSet(modelUrl: string, range: IRange, up: boolean, wordDef: string, wordDefFlags: string): Promise<IInplaceReplaceSupportResult | null> {
let model = this._getModel(modelUrl);
if (!model) {
return null;
}
let wordDefRegExp = new RegExp(wordDef, wordDefFlags);
if (range.startColumn === range.endColumn) {
range = {
startLineNumber: range.startLineNumber,
startColumn: range.startColumn,
endLineNumber: range.endLineNumber,
endColumn: range.endColumn + 1
};
}
let selectionText = model.getValueInRange(range);
let wordRange = model.getWordAtPosition({ lineNumber: range.startLineNumber, column: range.startColumn }, wordDefRegExp);
if (!wordRange) {
return null;
}
let word = model.getValueInRange(wordRange);
let result = BasicInplaceReplace.INSTANCE.navigateValueSet(range, selectionText, wordRange, word, up);
return result;
}
// ---- BEGIN foreign module support --------------------------------------------------------------------------
public loadForeignModule(moduleId: string, createData: any, foreignHostMethods: string[]): Promise<string[]> {
const proxyMethodRequest = (method: string, args: any[]): Promise<any> => {
return this._host.fhr(method, args);
};
const foreignHost = types.createProxyObject(foreignHostMethods, proxyMethodRequest);
let ctx: IWorkerContext<any> = {
host: foreignHost,
getMirrorModels: (): IMirrorModel[] => {
return this._getModels();
}
};
if (this._foreignModuleFactory) {
this._foreignModule = this._foreignModuleFactory(ctx, createData);
// static foreing module
return Promise.resolve(types.getAllMethodNames(this._foreignModule));
}
// ESM-comment-begin
return new Promise<any>((resolve, reject) => {
require([moduleId], (foreignModule: { create: IForeignModuleFactory }) => {
this._foreignModule = foreignModule.create(ctx, createData);
resolve(types.getAllMethodNames(this._foreignModule));
}, reject);
});
// ESM-comment-end
// ESM-uncomment-begin
// return Promise.reject(new Error(`Unexpected usage`));
// ESM-uncomment-end
}
// foreign method request
public fmr(method: string, args: any[]): Promise<any> {
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 foreign module support --------------------------------------------------------------------------
}
/**
* Called on the worker side
* @internal
*/
export function create(host: EditorWorkerHost): IRequestHandler {
return new EditorSimpleWorker(host, null);
}
// This is only available in a Web Worker
declare function importScripts(...urls: string[]): void;
if (typeof importScripts === 'function') {
// Running in a web worker
globals.monaco = createMonacoBaseAPI();
}