Path: blob/main/src/vs/editor/common/services/textModelSync/textModelSync.impl.ts
3296 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 { IntervalTimer } from '../../../../base/common/async.js';6import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';7import { URI } from '../../../../base/common/uri.js';8import { IWebWorkerClient, IWebWorkerServer } from '../../../../base/common/worker/webWorker.js';9import { IPosition, Position } from '../../core/position.js';10import { IRange, Range } from '../../core/range.js';11import { ensureValidWordDefinition, getWordAtText, IWordAtPosition } from '../../core/wordHelper.js';12import { IDocumentColorComputerTarget } from '../../languages/defaultDocumentColorsComputer.js';13import { ILinkComputerTarget } from '../../languages/linkComputer.js';14import { MirrorTextModel as BaseMirrorModel, IModelChangedEvent } from '../../model/mirrorTextModel.js';15import { IMirrorModel, IWordRange } from '../editorWebWorker.js';16import { IModelService } from '../model.js';17import { IRawModelData, IWorkerTextModelSyncChannelServer } from './textModelSync.protocol.js';1819/**20* Stop syncing a model to the worker if it was not needed for 1 min.21*/22export const STOP_SYNC_MODEL_DELTA_TIME_MS = 60 * 1000;2324export const WORKER_TEXT_MODEL_SYNC_CHANNEL = 'workerTextModelSync';2526export class WorkerTextModelSyncClient extends Disposable {2728public static create(workerClient: IWebWorkerClient<any>, modelService: IModelService): WorkerTextModelSyncClient {29return new WorkerTextModelSyncClient(30workerClient.getChannel<IWorkerTextModelSyncChannelServer>(WORKER_TEXT_MODEL_SYNC_CHANNEL),31modelService32);33}3435private readonly _proxy: IWorkerTextModelSyncChannelServer;36private readonly _modelService: IModelService;37private _syncedModels: { [modelUrl: string]: IDisposable } = Object.create(null);38private _syncedModelsLastUsedTime: { [modelUrl: string]: number } = Object.create(null);3940constructor(proxy: IWorkerTextModelSyncChannelServer, modelService: IModelService, keepIdleModels: boolean = false) {41super();42this._proxy = proxy;43this._modelService = modelService;4445if (!keepIdleModels) {46const timer = new IntervalTimer();47timer.cancelAndSet(() => this._checkStopModelSync(), Math.round(STOP_SYNC_MODEL_DELTA_TIME_MS / 2));48this._register(timer);49}50}5152public override dispose(): void {53for (const modelUrl in this._syncedModels) {54dispose(this._syncedModels[modelUrl]);55}56this._syncedModels = Object.create(null);57this._syncedModelsLastUsedTime = Object.create(null);58super.dispose();59}6061public ensureSyncedResources(resources: URI[], forceLargeModels: boolean = false): void {62for (const resource of resources) {63const resourceStr = resource.toString();6465if (!this._syncedModels[resourceStr]) {66this._beginModelSync(resource, forceLargeModels);67}68if (this._syncedModels[resourceStr]) {69this._syncedModelsLastUsedTime[resourceStr] = (new Date()).getTime();70}71}72}7374private _checkStopModelSync(): void {75const currentTime = (new Date()).getTime();7677const toRemove: string[] = [];78for (const modelUrl in this._syncedModelsLastUsedTime) {79const elapsedTime = currentTime - this._syncedModelsLastUsedTime[modelUrl];80if (elapsedTime > STOP_SYNC_MODEL_DELTA_TIME_MS) {81toRemove.push(modelUrl);82}83}8485for (const e of toRemove) {86this._stopModelSync(e);87}88}8990private _beginModelSync(resource: URI, forceLargeModels: boolean): void {91const model = this._modelService.getModel(resource);92if (!model) {93return;94}95if (!forceLargeModels && model.isTooLargeForSyncing()) {96return;97}9899const modelUrl = resource.toString();100101this._proxy.$acceptNewModel({102url: model.uri.toString(),103lines: model.getLinesContent(),104EOL: model.getEOL(),105versionId: model.getVersionId()106});107108const toDispose = new DisposableStore();109toDispose.add(model.onDidChangeContent((e) => {110this._proxy.$acceptModelChanged(modelUrl.toString(), e);111}));112toDispose.add(model.onWillDispose(() => {113this._stopModelSync(modelUrl);114}));115toDispose.add(toDisposable(() => {116this._proxy.$acceptRemovedModel(modelUrl);117}));118119this._syncedModels[modelUrl] = toDispose;120}121122private _stopModelSync(modelUrl: string): void {123const toDispose = this._syncedModels[modelUrl];124delete this._syncedModels[modelUrl];125delete this._syncedModelsLastUsedTime[modelUrl];126dispose(toDispose);127}128}129130export class WorkerTextModelSyncServer implements IWorkerTextModelSyncChannelServer {131132private readonly _models: { [uri: string]: MirrorModel };133134constructor() {135this._models = Object.create(null);136}137138public bindToServer(workerServer: IWebWorkerServer): void {139workerServer.setChannel(WORKER_TEXT_MODEL_SYNC_CHANNEL, this);140}141142public getModel(uri: string): ICommonModel | undefined {143return this._models[uri];144}145146public getModels(): ICommonModel[] {147const all: MirrorModel[] = [];148Object.keys(this._models).forEach((key) => all.push(this._models[key]));149return all;150}151152$acceptNewModel(data: IRawModelData): void {153this._models[data.url] = new MirrorModel(URI.parse(data.url), data.lines, data.EOL, data.versionId);154}155156$acceptModelChanged(uri: string, e: IModelChangedEvent): void {157if (!this._models[uri]) {158return;159}160const model = this._models[uri];161model.onEvents(e);162}163164$acceptRemovedModel(uri: string): void {165if (!this._models[uri]) {166return;167}168delete this._models[uri];169}170}171172export class MirrorModel extends BaseMirrorModel implements ICommonModel {173174public get uri(): URI {175return this._uri;176}177178public get eol(): string {179return this._eol;180}181182public getValue(): string {183return this.getText();184}185186public findMatches(regex: RegExp): RegExpMatchArray[] {187const matches = [];188for (let i = 0; i < this._lines.length; i++) {189const line = this._lines[i];190const offsetToAdd = this.offsetAt(new Position(i + 1, 1));191const iteratorOverMatches = line.matchAll(regex);192for (const match of iteratorOverMatches) {193if (match.index || match.index === 0) {194match.index = match.index + offsetToAdd;195}196matches.push(match);197}198}199return matches;200}201202public getLinesContent(): string[] {203return this._lines.slice(0);204}205206public getLineCount(): number {207return this._lines.length;208}209210public getLineContent(lineNumber: number): string {211return this._lines[lineNumber - 1];212}213214public getWordAtPosition(position: IPosition, wordDefinition: RegExp): Range | null {215216const wordAtText = getWordAtText(217position.column,218ensureValidWordDefinition(wordDefinition),219this._lines[position.lineNumber - 1],2200221);222223if (wordAtText) {224return new Range(position.lineNumber, wordAtText.startColumn, position.lineNumber, wordAtText.endColumn);225}226227return null;228}229230public getWordUntilPosition(position: IPosition, wordDefinition: RegExp): IWordAtPosition {231const wordAtPosition = this.getWordAtPosition(position, wordDefinition);232if (!wordAtPosition) {233return {234word: '',235startColumn: position.column,236endColumn: position.column237};238}239return {240word: this._lines[position.lineNumber - 1].substring(wordAtPosition.startColumn - 1, position.column - 1),241startColumn: wordAtPosition.startColumn,242endColumn: position.column243};244}245246247public words(wordDefinition: RegExp): Iterable<string> {248249const lines = this._lines;250const wordenize = this._wordenize.bind(this);251252let lineNumber = 0;253let lineText = '';254let wordRangesIdx = 0;255let wordRanges: IWordRange[] = [];256257return {258*[Symbol.iterator]() {259while (true) {260if (wordRangesIdx < wordRanges.length) {261const value = lineText.substring(wordRanges[wordRangesIdx].start, wordRanges[wordRangesIdx].end);262wordRangesIdx += 1;263yield value;264} else {265if (lineNumber < lines.length) {266lineText = lines[lineNumber];267wordRanges = wordenize(lineText, wordDefinition);268wordRangesIdx = 0;269lineNumber += 1;270} else {271break;272}273}274}275}276};277}278279public getLineWords(lineNumber: number, wordDefinition: RegExp): IWordAtPosition[] {280const content = this._lines[lineNumber - 1];281const ranges = this._wordenize(content, wordDefinition);282const words: IWordAtPosition[] = [];283for (const range of ranges) {284words.push({285word: content.substring(range.start, range.end),286startColumn: range.start + 1,287endColumn: range.end + 1288});289}290return words;291}292293private _wordenize(content: string, wordDefinition: RegExp): IWordRange[] {294const result: IWordRange[] = [];295let match: RegExpExecArray | null;296297wordDefinition.lastIndex = 0; // reset lastIndex just to be sure298299while (match = wordDefinition.exec(content)) {300if (match[0].length === 0) {301// it did match the empty string302break;303}304result.push({ start: match.index, end: match.index + match[0].length });305}306return result;307}308309public getValueInRange(range: IRange): string {310range = this._validateRange(range);311312if (range.startLineNumber === range.endLineNumber) {313return this._lines[range.startLineNumber - 1].substring(range.startColumn - 1, range.endColumn - 1);314}315316const lineEnding = this._eol;317const startLineIndex = range.startLineNumber - 1;318const endLineIndex = range.endLineNumber - 1;319const resultLines: string[] = [];320321resultLines.push(this._lines[startLineIndex].substring(range.startColumn - 1));322for (let i = startLineIndex + 1; i < endLineIndex; i++) {323resultLines.push(this._lines[i]);324}325resultLines.push(this._lines[endLineIndex].substring(0, range.endColumn - 1));326327return resultLines.join(lineEnding);328}329330public offsetAt(position: IPosition): number {331position = this._validatePosition(position);332this._ensureLineStarts();333return this._lineStarts!.getPrefixSum(position.lineNumber - 2) + (position.column - 1);334}335336public positionAt(offset: number): IPosition {337offset = Math.floor(offset);338offset = Math.max(0, offset);339340this._ensureLineStarts();341const out = this._lineStarts!.getIndexOf(offset);342const lineLength = this._lines[out.index].length;343344// Ensure we return a valid position345return {346lineNumber: 1 + out.index,347column: 1 + Math.min(out.remainder, lineLength)348};349}350351private _validateRange(range: IRange): IRange {352353const start = this._validatePosition({ lineNumber: range.startLineNumber, column: range.startColumn });354const end = this._validatePosition({ lineNumber: range.endLineNumber, column: range.endColumn });355356if (start.lineNumber !== range.startLineNumber357|| start.column !== range.startColumn358|| end.lineNumber !== range.endLineNumber359|| end.column !== range.endColumn) {360361return {362startLineNumber: start.lineNumber,363startColumn: start.column,364endLineNumber: end.lineNumber,365endColumn: end.column366};367}368369return range;370}371372private _validatePosition(position: IPosition): IPosition {373if (!Position.isIPosition(position)) {374throw new Error('bad position');375}376let { lineNumber, column } = position;377let hasChanged = false;378379if (lineNumber < 1) {380lineNumber = 1;381column = 1;382hasChanged = true;383384} else if (lineNumber > this._lines.length) {385lineNumber = this._lines.length;386column = this._lines[lineNumber - 1].length + 1;387hasChanged = true;388389} else {390const maxCharacter = this._lines[lineNumber - 1].length + 1;391if (column < 1) {392column = 1;393hasChanged = true;394}395else if (column > maxCharacter) {396column = maxCharacter;397hasChanged = true;398}399}400401if (!hasChanged) {402return position;403} else {404return { lineNumber, column };405}406}407}408409export interface ICommonModel extends ILinkComputerTarget, IDocumentColorComputerTarget, IMirrorModel {410uri: URI;411version: number;412eol: string;413getValue(): string;414415getLinesContent(): string[];416getLineCount(): number;417getLineContent(lineNumber: number): string;418getLineWords(lineNumber: number, wordDefinition: RegExp): IWordAtPosition[];419words(wordDefinition: RegExp): Iterable<string>;420getWordUntilPosition(position: IPosition, wordDefinition: RegExp): IWordAtPosition;421getValueInRange(range: IRange): string;422getWordAtPosition(position: IPosition, wordDefinition: RegExp): Range | null;423offsetAt(position: IPosition): number;424positionAt(offset: number): IPosition;425findMatches(regex: RegExp): RegExpMatchArray[];426}427428429