UNPKG

monaco-editor-core

Version:

A browser based code editor

305 lines (304 loc) • 11.3 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IntervalTimer } from '../../../../base/common/async.js'; import { Disposable, DisposableStore, dispose, toDisposable } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; import { Position } from '../../core/position.js'; import { Range } from '../../core/range.js'; import { ensureValidWordDefinition, getWordAtText } from '../../core/wordHelper.js'; import { MirrorTextModel as BaseMirrorModel } from '../../model/mirrorTextModel.js'; /** * Stop syncing a model to the worker if it was not needed for 1 min. */ export const STOP_SYNC_MODEL_DELTA_TIME_MS = 60 * 1000; export class WorkerTextModelSyncClient extends Disposable { constructor(proxy, modelService, keepIdleModels = false) { super(); this._syncedModels = Object.create(null); this._syncedModelsLastUsedTime = Object.create(null); this._proxy = proxy; this._modelService = modelService; if (!keepIdleModels) { const timer = new IntervalTimer(); timer.cancelAndSet(() => this._checkStopModelSync(), Math.round(STOP_SYNC_MODEL_DELTA_TIME_MS / 2)); this._register(timer); } } dispose() { for (const modelUrl in this._syncedModels) { dispose(this._syncedModels[modelUrl]); } this._syncedModels = Object.create(null); this._syncedModelsLastUsedTime = Object.create(null); super.dispose(); } ensureSyncedResources(resources, forceLargeModels = false) { for (const resource of resources) { const resourceStr = resource.toString(); if (!this._syncedModels[resourceStr]) { this._beginModelSync(resource, forceLargeModels); } if (this._syncedModels[resourceStr]) { this._syncedModelsLastUsedTime[resourceStr] = (new Date()).getTime(); } } } _checkStopModelSync() { const currentTime = (new Date()).getTime(); const toRemove = []; for (const modelUrl in this._syncedModelsLastUsedTime) { const elapsedTime = currentTime - this._syncedModelsLastUsedTime[modelUrl]; if (elapsedTime > STOP_SYNC_MODEL_DELTA_TIME_MS) { toRemove.push(modelUrl); } } for (const e of toRemove) { this._stopModelSync(e); } } _beginModelSync(resource, forceLargeModels) { const model = this._modelService.getModel(resource); if (!model) { return; } if (!forceLargeModels && model.isTooLargeForSyncing()) { return; } const modelUrl = resource.toString(); this._proxy.$acceptNewModel({ url: model.uri.toString(), lines: model.getLinesContent(), EOL: model.getEOL(), versionId: model.getVersionId() }); const toDispose = new DisposableStore(); toDispose.add(model.onDidChangeContent((e) => { this._proxy.$acceptModelChanged(modelUrl.toString(), e); })); toDispose.add(model.onWillDispose(() => { this._stopModelSync(modelUrl); })); toDispose.add(toDisposable(() => { this._proxy.$acceptRemovedModel(modelUrl); })); this._syncedModels[modelUrl] = toDispose; } _stopModelSync(modelUrl) { const toDispose = this._syncedModels[modelUrl]; delete this._syncedModels[modelUrl]; delete this._syncedModelsLastUsedTime[modelUrl]; dispose(toDispose); } } export class WorkerTextModelSyncServer { constructor() { 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(uri, e) { if (!this._models[uri]) { return; } const model = this._models[uri]; model.onEvents(e); } $acceptRemovedModel(uri) { if (!this._models[uri]) { return; } delete this._models[uri]; } } export 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 }; } } }