Path: blob/main/src/vs/workbench/api/browser/mainThreadDocuments.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 { toErrorMessage } from '../../../base/common/errorMessage.js';6import { IReference, dispose, Disposable } from '../../../base/common/lifecycle.js';7import { Schemas } from '../../../base/common/network.js';8import { URI, UriComponents } from '../../../base/common/uri.js';9import { ITextModel, shouldSynchronizeModel } from '../../../editor/common/model.js';10import { IModelService } from '../../../editor/common/services/model.js';11import { ITextModelService } from '../../../editor/common/services/resolverService.js';12import { IFileService, FileOperation } from '../../../platform/files/common/files.js';13import { ExtHostContext, ExtHostDocumentsShape, MainThreadDocumentsShape } from '../common/extHost.protocol.js';14import { EncodingMode, ITextFileEditorModel, ITextFileService, TextFileResolveReason } from '../../services/textfile/common/textfiles.js';15import { IUntitledTextEditorModel } from '../../services/untitled/common/untitledTextEditorModel.js';16import { IWorkbenchEnvironmentService } from '../../services/environment/common/environmentService.js';17import { toLocalResource, extUri, IExtUri } from '../../../base/common/resources.js';18import { IWorkingCopyFileService } from '../../services/workingCopy/common/workingCopyFileService.js';19import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js';20import { Emitter, Event } from '../../../base/common/event.js';21import { IPathService } from '../../services/path/common/pathService.js';22import { ResourceMap } from '../../../base/common/map.js';23import { IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';24import { ErrorNoTelemetry, onUnexpectedError } from '../../../base/common/errors.js';25import { ISerializedModelContentChangedEvent } from '../../../editor/common/textModelEvents.js';2627export class BoundModelReferenceCollection {2829private _data = new Array<{ uri: URI; length: number; dispose(): void }>();30private _length = 0;3132constructor(33private readonly _extUri: IExtUri,34private readonly _maxAge: number = 1000 * 60 * 3, // auto-dispse by age35private readonly _maxLength: number = 1024 * 1024 * 80, // auto-dispose by total length36private readonly _maxSize: number = 50 // auto-dispose by number of references37) {38//39}4041dispose(): void {42this._data = dispose(this._data);43}4445remove(uri: URI): void {46for (const entry of [...this._data] /* copy array because dispose will modify it */) {47if (this._extUri.isEqualOrParent(entry.uri, uri)) {48entry.dispose();49}50}51}5253add(uri: URI, ref: IReference<any>, length: number = 0): void {54// const length = ref.object.textEditorModel.getValueLength();55const dispose = () => {56const idx = this._data.indexOf(entry);57if (idx >= 0) {58this._length -= length;59ref.dispose();60clearTimeout(handle);61this._data.splice(idx, 1);62}63};64const handle = setTimeout(dispose, this._maxAge);65const entry = { uri, length, dispose };6667this._data.push(entry);68this._length += length;69this._cleanup();70}7172private _cleanup(): void {73// clean-up wrt total length74while (this._length > this._maxLength) {75this._data[0].dispose();76}77// clean-up wrt number of documents78const extraSize = Math.ceil(this._maxSize * 1.2);79if (this._data.length >= extraSize) {80dispose(this._data.slice(0, extraSize - this._maxSize));81}82}83}8485class ModelTracker extends Disposable {8687private _knownVersionId: number;8889constructor(90private readonly _model: ITextModel,91private readonly _onIsCaughtUpWithContentChanges: Emitter<URI>,92private readonly _proxy: ExtHostDocumentsShape,93private readonly _textFileService: ITextFileService,94) {95super();96this._knownVersionId = this._model.getVersionId();97this._store.add(this._model.onDidChangeContent((e) => {98this._knownVersionId = e.versionId;99if (e.detailedReasonsChangeLengths.length !== 1) {100onUnexpectedError(new Error(`Unexpected reasons: ${e.detailedReasons.map(r => r.toString())}`));101}102103const evt: ISerializedModelContentChangedEvent = {104changes: e.changes,105isEolChange: e.isEolChange,106isUndoing: e.isUndoing,107isRedoing: e.isRedoing,108isFlush: e.isFlush,109eol: e.eol,110versionId: e.versionId,111detailedReason: e.detailedReasons[0].metadata,112};113this._proxy.$acceptModelChanged(this._model.uri, evt, this._textFileService.isDirty(this._model.uri));114if (this.isCaughtUpWithContentChanges()) {115this._onIsCaughtUpWithContentChanges.fire(this._model.uri);116}117}));118}119120isCaughtUpWithContentChanges(): boolean {121return (this._model.getVersionId() === this._knownVersionId);122}123}124125export class MainThreadDocuments extends Disposable implements MainThreadDocumentsShape {126127private _onIsCaughtUpWithContentChanges = this._store.add(new Emitter<URI>());128readonly onIsCaughtUpWithContentChanges = this._onIsCaughtUpWithContentChanges.event;129130private readonly _proxy: ExtHostDocumentsShape;131private readonly _modelTrackers = new ResourceMap<ModelTracker>();132private readonly _modelReferenceCollection: BoundModelReferenceCollection;133134constructor(135extHostContext: IExtHostContext,136@IModelService private readonly _modelService: IModelService,137@ITextFileService private readonly _textFileService: ITextFileService,138@IFileService private readonly _fileService: IFileService,139@ITextModelService private readonly _textModelResolverService: ITextModelService,140@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,141@IUriIdentityService private readonly _uriIdentityService: IUriIdentityService,142@IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService,143@IPathService private readonly _pathService: IPathService144) {145super();146147this._modelReferenceCollection = this._store.add(new BoundModelReferenceCollection(_uriIdentityService.extUri));148149this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocuments);150151this._store.add(_modelService.onModelLanguageChanged(this._onModelModeChanged, this));152153this._store.add(_textFileService.files.onDidSave(e => {154if (this._shouldHandleFileEvent(e.model.resource)) {155this._proxy.$acceptModelSaved(e.model.resource);156}157}));158this._store.add(_textFileService.files.onDidChangeDirty(m => {159if (this._shouldHandleFileEvent(m.resource)) {160this._proxy.$acceptDirtyStateChanged(m.resource, m.isDirty());161}162}));163this._store.add(Event.any<ITextFileEditorModel | IUntitledTextEditorModel>(_textFileService.files.onDidChangeEncoding, _textFileService.untitled.onDidChangeEncoding)(m => {164if (this._shouldHandleFileEvent(m.resource)) {165const encoding = m.getEncoding();166if (encoding) {167this._proxy.$acceptEncodingChanged(m.resource, encoding);168}169}170}));171172this._store.add(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => {173const isMove = e.operation === FileOperation.MOVE;174if (isMove || e.operation === FileOperation.DELETE) {175for (const pair of e.files) {176const removed = isMove ? pair.source : pair.target;177if (removed) {178this._modelReferenceCollection.remove(removed);179}180}181}182}));183}184185override dispose(): void {186dispose(this._modelTrackers.values());187this._modelTrackers.clear();188super.dispose();189}190191isCaughtUpWithContentChanges(resource: URI): boolean {192const tracker = this._modelTrackers.get(resource);193if (tracker) {194return tracker.isCaughtUpWithContentChanges();195}196return true;197}198199private _shouldHandleFileEvent(resource: URI): boolean {200const model = this._modelService.getModel(resource);201return !!model && shouldSynchronizeModel(model);202}203204handleModelAdded(model: ITextModel): void {205// Same filter as in mainThreadEditorsTracker206if (!shouldSynchronizeModel(model)) {207// don't synchronize too large models208return;209}210this._modelTrackers.set(model.uri, new ModelTracker(model, this._onIsCaughtUpWithContentChanges, this._proxy, this._textFileService));211}212213private _onModelModeChanged(event: { model: ITextModel; oldLanguageId: string }): void {214const { model } = event;215if (!this._modelTrackers.has(model.uri)) {216return;217}218this._proxy.$acceptModelLanguageChanged(model.uri, model.getLanguageId());219}220221handleModelRemoved(modelUrl: URI): void {222if (!this._modelTrackers.has(modelUrl)) {223return;224}225this._modelTrackers.get(modelUrl)!.dispose();226this._modelTrackers.delete(modelUrl);227}228229// --- from extension host process230231async $trySaveDocument(uri: UriComponents): Promise<boolean> {232const target = await this._textFileService.save(URI.revive(uri));233return Boolean(target);234}235236async $tryOpenDocument(uriData: UriComponents, options?: { encoding?: string }): Promise<URI> {237const inputUri = URI.revive(uriData);238if (!inputUri.scheme || !(inputUri.fsPath || inputUri.authority)) {239throw new ErrorNoTelemetry(`Invalid uri. Scheme and authority or path must be set.`);240}241242const canonicalUri = this._uriIdentityService.asCanonicalUri(inputUri);243244let promise: Promise<URI>;245switch (canonicalUri.scheme) {246case Schemas.untitled:247promise = this._handleUntitledScheme(canonicalUri, options);248break;249case Schemas.file:250default:251promise = this._handleAsResourceInput(canonicalUri, options);252break;253}254255let documentUri: URI | undefined;256try {257documentUri = await promise;258} catch (err) {259throw new ErrorNoTelemetry(`cannot open ${canonicalUri.toString()}. Detail: ${toErrorMessage(err)}`);260}261if (!documentUri) {262throw new ErrorNoTelemetry(`cannot open ${canonicalUri.toString()}`);263} else if (!extUri.isEqual(documentUri, canonicalUri)) {264throw new ErrorNoTelemetry(`cannot open ${canonicalUri.toString()}. Detail: Actual document opened as ${documentUri.toString()}`);265} else if (!this._modelTrackers.has(canonicalUri)) {266throw new ErrorNoTelemetry(`cannot open ${canonicalUri.toString()}. Detail: Files above 50MB cannot be synchronized with extensions.`);267} else {268return canonicalUri;269}270}271272$tryCreateDocument(options?: { language?: string; content?: string; encoding?: string }): Promise<URI> {273return this._doCreateUntitled(undefined, options);274}275276private async _handleAsResourceInput(uri: URI, options?: { encoding?: string }): Promise<URI> {277if (options?.encoding) {278const model = await this._textFileService.files.resolve(uri, { encoding: options.encoding, reason: TextFileResolveReason.REFERENCE });279if (model.isDirty()) {280throw new ErrorNoTelemetry(`Cannot re-open a dirty text document with different encoding. Save it first.`);281}282await model.setEncoding(options.encoding, EncodingMode.Decode);283}284285const ref = await this._textModelResolverService.createModelReference(uri);286this._modelReferenceCollection.add(uri, ref, ref.object.textEditorModel.getValueLength());287return ref.object.textEditorModel.uri;288}289290private async _handleUntitledScheme(uri: URI, options?: { encoding?: string }): Promise<URI> {291const asLocalUri = toLocalResource(uri, this._environmentService.remoteAuthority, this._pathService.defaultUriScheme);292const exists = await this._fileService.exists(asLocalUri);293if (exists) {294// don't create a new file ontop of an existing file295return Promise.reject(new Error('file already exists'));296}297return await this._doCreateUntitled(Boolean(uri.path) ? uri : undefined, options);298}299300private async _doCreateUntitled(associatedResource?: URI, options?: { language?: string; content?: string; encoding?: string }): Promise<URI> {301const model = this._textFileService.untitled.create({302associatedResource,303languageId: options?.language,304initialValue: options?.content,305encoding: options?.encoding306});307if (options?.encoding) {308await model.setEncoding(options.encoding);309}310const resource = model.resource;311const ref = await this._textModelResolverService.createModelReference(resource);312if (!this._modelTrackers.has(resource)) {313ref.dispose();314throw new Error(`expected URI ${resource.toString()} to have come to LIFE`);315}316this._modelReferenceCollection.add(resource, ref, ref.object.textEditorModel.getValueLength());317Event.once(model.onDidRevert)(() => this._modelReferenceCollection.remove(resource));318this._proxy.$acceptDirtyStateChanged(resource, true); // mark as dirty319return resource;320}321}322323324