/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import URI from 'vs/base/common/uri'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { EmitterEvent } from 'vs/base/common/eventEmitter'; import { setDisposableTimeout } from 'vs/base/common/async'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IThreadService } from 'vs/workbench/services/thread/common/threadService'; import { IDisposable, dispose, IReference } from 'vs/base/common/lifecycle'; import { TextFileModelChangeEvent, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { TPromise } from 'vs/base/common/winjs.base'; import { IFileService } from 'vs/platform/files/common/files'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { ExtHostContext, MainThreadDocumentsShape, ExtHostDocumentsShape } from './extHost.protocol'; import { ITextModelResolverService } from 'vs/editor/common/services/resolverService'; import { ICodeEditorService } from 'vs/editor/common/services/codeEditorService'; import { ITextSource } from 'vs/editor/common/model/textSource'; import { MainThreadDocumentsAndEditors } from './mainThreadDocumentsAndEditors'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { ITextEditorModel } from "vs/workbench/common/editor"; class TimeoutReference { private static _delay = 1000 * 60 * 3; private _timer: IDisposable; private _disposed = false; constructor( readonly codeEditorService: ICodeEditorService, readonly editorGroupService: IEditorGroupService, readonly reference: IReference ) { const check = () => { if (!this.isUsed()) { this.dispose(); } else { this._timer = setDisposableTimeout(check, TimeoutReference._delay); } }; this._timer = setDisposableTimeout(check, TimeoutReference._delay); } dispose(): void { if (!this._disposed) { this._disposed = true; dispose(this.reference, this._timer); } } private isUsed(): boolean { for (const editor of this.codeEditorService.listCodeEditors()) { if (editor.getModel() === this.reference.object.textEditorModel) { return true; } } for (const group of this.editorGroupService.getStacksModel().groups) { if (group.contains(this.reference.object.textEditorModel.uri)) { return true; } } return false; } } export class MainThreadDocuments extends MainThreadDocumentsShape { private _modelService: IModelService; private _modeService: IModeService; private _textModelResolverService: ITextModelResolverService; private _textFileService: ITextFileService; private _codeEditorService: ICodeEditorService; private _fileService: IFileService; private _untitledEditorService: IUntitledEditorService; private _editorGroupService: IEditorGroupService; private _toDispose: IDisposable[]; private _modelToDisposeMap: { [modelUrl: string]: IDisposable; }; private _proxy: ExtHostDocumentsShape; private _modelIsSynced: { [modelId: string]: boolean; }; private _resourceContentProvider: { [handle: number]: IDisposable }; constructor( documentsAndEditors: MainThreadDocumentsAndEditors, @IThreadService threadService: IThreadService, @IModelService modelService: IModelService, @IModeService modeService: IModeService, @ITextFileService textFileService: ITextFileService, @ICodeEditorService codeEditorService: ICodeEditorService, @IFileService fileService: IFileService, @ITextModelResolverService textModelResolverService: ITextModelResolverService, @IUntitledEditorService untitledEditorService: IUntitledEditorService, @IEditorGroupService editorGroupService: IEditorGroupService ) { super(); this._modelService = modelService; this._modeService = modeService; this._textModelResolverService = textModelResolverService; this._textFileService = textFileService; this._codeEditorService = codeEditorService; this._fileService = fileService; this._untitledEditorService = untitledEditorService; this._editorGroupService = editorGroupService; this._proxy = threadService.get(ExtHostContext.ExtHostDocuments); this._modelIsSynced = {}; this._toDispose = []; this._toDispose.push(documentsAndEditors.onDocumentAdd(models => models.forEach(this._onModelAdded, this))); this._toDispose.push(documentsAndEditors.onDocumentRemove(urls => urls.forEach(this._onModelRemoved, this))); modelService.onModelModeChanged(this._onModelModeChanged, this, this._toDispose); this._toDispose.push(textFileService.models.onModelSaved(e => { if (this._shouldHandleFileEvent(e)) { this._proxy.$acceptModelSaved(e.resource.toString()); } })); this._toDispose.push(textFileService.models.onModelReverted(e => { if (this._shouldHandleFileEvent(e)) { this._proxy.$acceptModelReverted(e.resource.toString()); } })); this._toDispose.push(textFileService.models.onModelDirty(e => { if (this._shouldHandleFileEvent(e)) { this._proxy.$acceptModelDirty(e.resource.toString()); } })); this._modelToDisposeMap = Object.create(null); this._resourceContentProvider = Object.create(null); } public dispose(): void { Object.keys(this._modelToDisposeMap).forEach((modelUrl) => { this._modelToDisposeMap[modelUrl].dispose(); }); this._modelToDisposeMap = Object.create(null); this._toDispose = dispose(this._toDispose); } private _shouldHandleFileEvent(e: TextFileModelChangeEvent): boolean { const model = this._modelService.getModel(e.resource); return model && !model.isTooLargeForHavingARichMode(); } private _onModelAdded(model: editorCommon.IModel): void { // Same filter as in mainThreadEditorsTracker if (model.isTooLargeForHavingARichMode()) { // don't synchronize too large models return null; } let modelUrl = model.uri; this._modelIsSynced[modelUrl.toString()] = true; this._modelToDisposeMap[modelUrl.toString()] = model.addBulkListener((events) => this._onModelEvents(modelUrl, events)); } private _onModelModeChanged(event: { model: editorCommon.IModel; oldModeId: string; }): void { let { model, oldModeId } = event; let modelUrl = model.uri; if (!this._modelIsSynced[modelUrl.toString()]) { return; } this._proxy.$acceptModelModeChanged(model.uri.toString(), oldModeId, model.getLanguageIdentifier().language); } private _onModelRemoved(modelUrl: string): void { if (!this._modelIsSynced[modelUrl]) { return; } delete this._modelIsSynced[modelUrl]; this._modelToDisposeMap[modelUrl].dispose(); delete this._modelToDisposeMap[modelUrl]; } private _onModelEvents(modelUrl: URI, events: EmitterEvent[]): void { let changedEvents: editorCommon.IModelContentChangedEvent2[] = []; for (let i = 0, len = events.length; i < len; i++) { let e = events[i]; switch (e.getType()) { case editorCommon.EventType.ModelContentChanged2: changedEvents.push(e.getData()); break; } } if (changedEvents.length > 0) { this._proxy.$acceptModelChanged(modelUrl.toString(), changedEvents, this._textFileService.isDirty(modelUrl)); } } // --- from extension host process $trySaveDocument(uri: URI): TPromise { return this._textFileService.save(uri); } $tryOpenDocument(uri: URI): TPromise { if (!uri.scheme || !(uri.fsPath || uri.authority)) { return TPromise.wrapError(`Invalid uri. Scheme and authority or path must be set.`); } let promise: TPromise; switch (uri.scheme) { case 'untitled': promise = this._handleUnititledScheme(uri); break; case 'file': default: promise = this._handleAsResourceInput(uri); break; } return promise.then(success => { if (!success) { return TPromise.wrapError('cannot open ' + uri.toString()); } return undefined; }, err => { return TPromise.wrapError('cannot open ' + uri.toString() + '. Detail: ' + toErrorMessage(err)); }); } $tryCreateDocument(options?: { language: string }): TPromise { return this._doCreateUntitled(void 0, options ? options.language : void 0); } private _handleAsResourceInput(uri: URI): TPromise { return this._textModelResolverService.createModelReference(uri).then(ref => { // TimeoutReference will check every 3 min if the // reference is still in use. This is quite harsh to // extensions but we don't want them to make us hold // on to model indefinitely this._toDispose.push(new TimeoutReference(this._codeEditorService, this._editorGroupService, ref)); const result = !!ref.object; return result; }); } private _handleUnititledScheme(uri: URI): TPromise { let asFileUri = URI.file(uri.fsPath); return this._fileService.resolveFile(asFileUri).then(stats => { // don't create a new file ontop of an existing file return TPromise.wrapError('file already exists on disk'); }, err => this._doCreateUntitled(asFileUri).then(resource => !!resource)); } private _doCreateUntitled(uri?: URI, modeId?: string): TPromise { let input = this._untitledEditorService.createOrGet(uri, modeId); return input.resolve(true).then(model => { if (!this._modelIsSynced[input.getResource().toString()]) { throw new Error(`expected URI ${input.getResource().toString()} to have come to LIFE`); } return this._proxy.$acceptModelDirty(input.getResource().toString()); // mark as dirty }).then(() => { return input.getResource(); }); } // --- virtual document logic $registerTextContentProvider(handle: number, scheme: string): void { this._resourceContentProvider[handle] = this._textModelResolverService.registerTextModelContentProvider(scheme, { provideTextContent: (uri: URI): TPromise => { return this._proxy.$provideTextDocumentContent(handle, uri).then(value => { if (typeof value === 'string') { const firstLineText = value.substr(0, 1 + value.search(/\r?\n/)); const mode = this._modeService.getOrCreateModeByFilenameOrFirstLine(uri.fsPath, firstLineText); return this._modelService.createModel(value, mode, uri); } return undefined; }); } }); } $unregisterTextContentProvider(handle: number): void { const registration = this._resourceContentProvider[handle]; if (registration) { registration.dispose(); delete this._resourceContentProvider[handle]; } } $onVirtualDocumentChange(uri: URI, value: ITextSource): void { const model = this._modelService.getModel(uri); if (!model) { return; } const raw: ITextSource = { lines: value.lines, length: value.length, BOM: value.BOM, EOL: value.EOL, containsRTL: value.containsRTL, isBasicASCII: value.isBasicASCII, }; if (!model.equals(raw)) { model.setValueFromTextSource(raw); } } }