UNPKG

@theia/monaco

Version:
526 lines • 21.5 kB
"use strict"; // ***************************************************************************** // Copyright (C) 2017 TypeFox and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** Object.defineProperty(exports, "__esModule", { value: true }); exports.MonacoEditorModel = exports.TextDocumentSaveReason = void 0; const vscode_languageserver_protocol_1 = require("@theia/core/shared/vscode-languageserver-protocol"); Object.defineProperty(exports, "TextDocumentSaveReason", { enumerable: true, get: function () { return vscode_languageserver_protocol_1.TextDocumentSaveReason; } }); const disposable_1 = require("@theia/core/lib/common/disposable"); const event_1 = require("@theia/core/lib/common/event"); const cancellation_1 = require("@theia/core/lib/common/cancellation"); const resource_1 = require("@theia/core/lib/common/resource"); const saveable_1 = require("@theia/core/lib/browser/saveable"); const monaco = require("@theia/monaco-editor-core"); const standaloneServices_1 = require("@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices"); const language_1 = require("@theia/monaco-editor-core/esm/vs/editor/common/languages/language"); const model_1 = require("@theia/monaco-editor-core/esm/vs/editor/common/services/model"); const textModel_1 = require("@theia/monaco-editor-core/esm/vs/editor/common/model/textModel"); const editor_generated_preference_schema_1 = require("@theia/editor/lib/browser/editor-generated-preference-schema"); const buffer_1 = require("@theia/core/lib/common/buffer"); const core_1 = require("@theia/core"); class MonacoEditorModel { get onContentChanged() { return (listener, thisArgs, disposables) => this.onDidChangeContent(() => listener(), thisArgs, disposables); } constructor(resource, m2p, p2m, logger, editorPreferences) { var _a; this.resource = resource; this.m2p = m2p; this.p2m = p2m; this.logger = logger; this.editorPreferences = editorPreferences; this.suppressOpenEditorWhenDirty = false; this.lineNumbersMinChars = 3; /* @deprecated there is no general save timeout, each participant should introduce a sensible timeout */ this.onWillSaveLoopTimeOut = 1500; this.toDispose = new disposable_1.DisposableCollection(); this.toDisposeOnAutoSave = new disposable_1.DisposableCollection(); this.onDidChangeContentEmitter = new event_1.Emitter(); this.onDidChangeContent = this.onDidChangeContentEmitter.event; this.onDidSaveModelEmitter = new event_1.Emitter(); this.onDidSaveModel = this.onDidSaveModelEmitter.event; this.onDidChangeValidEmitter = new event_1.Emitter(); this.onDidChangeValid = this.onDidChangeValidEmitter.event; this.onDidChangeEncodingEmitter = new event_1.Emitter(); this.onDidChangeEncoding = this.onDidChangeEncodingEmitter.event; this.onDidChangeReadOnly = (_a = this.resource.onDidChangeReadOnly) !== null && _a !== void 0 ? _a : event_1.Event.None; this.onWillSaveModelListeners = new core_1.ListenerList; this.onModelWillSaveModel = this.onWillSaveModelListeners.registration; /** * Use `valid` to access it. * Use `setValid` to mutate it. */ this._valid = false; this._dirty = false; this.onDirtyChangedEmitter = new event_1.Emitter(); this.pendingOperation = Promise.resolve(); this.syncCancellationTokenSource = new cancellation_1.CancellationTokenSource(); this.ignoreDirtyEdits = false; this.saveCancellationTokenSource = new cancellation_1.CancellationTokenSource(); this.ignoreContentChanges = false; this.contentChanges = []; this.toDispose.push(resource); this.toDispose.push(this.toDisposeOnAutoSave); this.toDispose.push(this.onDidChangeContentEmitter); this.toDispose.push(this.onDidSaveModelEmitter); this.toDispose.push(this.onDirtyChangedEmitter); this.toDispose.push(this.onDidChangeEncodingEmitter); this.toDispose.push(this.onDidChangeValidEmitter); this.toDispose.push(disposable_1.Disposable.create(() => this.cancelSave())); this.toDispose.push(disposable_1.Disposable.create(() => this.cancelSync())); this.resolveModel = this.readContents().then(content => this.initialize(content || '')); } undo() { this.model.undo(); } redo() { this.model.redo(); } dispose() { this.toDispose.dispose(); } isDisposed() { return this.toDispose.disposed; } resolve() { return this.resolveModel; } isResolved() { return Boolean(this.model); } setEncoding(encoding, mode) { if (mode === 1 /* EncodingMode.Decode */ && this.dirty) { return Promise.resolve(); } if (!this.setPreferredEncoding(encoding)) { return Promise.resolve(); } if (mode === 1 /* EncodingMode.Decode */) { return this.sync(); } return this.scheduleSave(this.cancelSave(), true, { saveReason: saveable_1.SaveReason.Manual }); } getEncoding() { return this.preferredEncoding || this.contentEncoding; } setPreferredEncoding(encoding) { if (encoding === this.preferredEncoding || (!this.preferredEncoding && encoding === this.contentEncoding)) { return false; } this.preferredEncoding = encoding; this.onDidChangeEncodingEmitter.fire(encoding); return true; } updateContentEncoding() { const contentEncoding = this.resource.encoding; if (!contentEncoding || this.contentEncoding === contentEncoding) { return; } this.contentEncoding = contentEncoding; if (!this.preferredEncoding) { this.onDidChangeEncodingEmitter.fire(contentEncoding); } } /** * #### Important * Only this method can create an instance of `monaco.editor.IModel`, * there should not be other calls to `monaco.editor.createModel`. */ initialize(value) { if (!this.toDispose.disposed) { const uri = monaco.Uri.parse(this.resource.uri.toString()); let firstLine; if (typeof value === 'string') { firstLine = value; const firstLF = value.indexOf('\n'); if (firstLF !== -1) { firstLine = value.substring(0, firstLF); } } else { firstLine = value.getFirstLineText(1000); } const languageSelection = standaloneServices_1.StandaloneServices.get(language_1.ILanguageService).createByFilepathOrFirstLine(uri, firstLine); this.model = standaloneServices_1.StandaloneServices.get(model_1.IModelService).createModel(value, languageSelection, uri); this.resourceVersion = this.resource.version; this.setDirty(this._dirty || (!!this.resource.initiallyDirty)); this.updateSavedVersionId(); this.toDispose.push(this.model); this.toDispose.push(this.model.onDidChangeContent(event => this.fireDidChangeContent(event))); if (this.resource.onDidChangeContents) { this.toDispose.push(this.resource.onDidChangeContents(() => this.sync())); } } } /** * Whether it is possible to load content from the underlying resource. */ get valid() { return this._valid; } setValid(valid) { if (valid === this._valid) { return; } this._valid = valid; this.onDidChangeValidEmitter.fire(undefined); } get dirty() { return this._dirty; } setDirty(dirty) { if (dirty === this._dirty) { return; } this._dirty = dirty; if (dirty === false) { this.updateSavedVersionId(); } this.onDirtyChangedEmitter.fire(undefined); } updateSavedVersionId() { this.bufferSavedVersionId = this.model.getAlternativeVersionId(); } get onDirtyChanged() { return this.onDirtyChangedEmitter.event; } get uri() { return this.resource.uri.toString(); } get autosaveable() { return this.resource.autosaveable; } get languageId() { return this._languageId !== undefined ? this._languageId : this.model.getLanguageId(); } getLanguageId() { return this.languageId; } /** * It's a hack to dispatch close notification with an old language id; don't use it. */ setLanguageId(languageId) { this._languageId = languageId; } get version() { return this.model.getVersionId(); } /** * Return selected text by Range or all text by default */ getText(range) { if (!range) { return this.model.getValue(); } else { return this.model.getValueInRange(this.p2m.asRange(range)); } } positionAt(offset) { const { lineNumber, column } = this.model.getPositionAt(offset); return this.m2p.asPosition(lineNumber, column); } offsetAt(position) { return this.model.getOffsetAt(this.p2m.asPosition(position)); } get lineCount() { return this.model.getLineCount(); } /** * Retrieves a line in a text document expressed as a one-based position. */ getLineContent(lineNumber) { return this.model.getLineContent(lineNumber); } getLineMaxColumn(lineNumber) { return this.model.getLineMaxColumn(lineNumber); } toValidPosition(position) { const { lineNumber, column } = this.model.validatePosition(this.p2m.asPosition(position)); return this.m2p.asPosition(lineNumber, column); } toValidRange(range) { return this.m2p.asRange(this.model.validateRange(this.p2m.asRange(range))); } get readOnly() { var _a; return (_a = this.resource.readOnly) !== null && _a !== void 0 ? _a : false; } isReadonly() { return this.readOnly; } get onDispose() { return this.toDispose.onDispose; } get onWillDispose() { return this.toDispose.onDispose; } // We have a TypeScript problem here. There is a const enum `DefaultEndOfLine` used for ITextModel and a non-const redeclaration of that enum in the public API in // Monaco.editor. The values will be the same, but TS won't accept that the two enums are equivalent, so it says these types are irreconcilable. get textEditorModel() { // @ts-expect-error ts(2322) return this.model; } /** * Find all matches in an editor for the given options. * @param options the options for finding matches. * * @returns the list of matches. */ findMatches(options) { var _a, _b; const wordSeparators = (_b = (_a = this.editorPreferences) === null || _a === void 0 ? void 0 : _a['editor.wordSeparators']) !== null && _b !== void 0 ? _b : editor_generated_preference_schema_1.editorGeneratedPreferenceProperties['editor.wordSeparators'].default; const results = this.model.findMatches(options.searchString, false, options.isRegex, options.matchCase, // eslint-disable-next-line no-null/no-null options.matchWholeWord ? wordSeparators : null, true, options.limitResultCount); const extractedMatches = []; results.forEach(r => { if (r.matches) { extractedMatches.push({ matches: r.matches, range: vscode_languageserver_protocol_1.Range.create(r.range.startLineNumber, r.range.startColumn, r.range.endLineNumber, r.range.endColumn) }); } }); return extractedMatches; } async load() { await this.resolveModel; return this; } save(options) { return this.scheduleSave(undefined, undefined, { saveReason: vscode_languageserver_protocol_1.TextDocumentSaveReason.Manual, ...options }); } async run(operation) { if (this.toDispose.disposed) { return; } return this.pendingOperation = this.pendingOperation.then(async () => { try { await operation(); } catch (e) { console.error(e); } }); } cancelSync() { this.trace(log => log('MonacoEditorModel.cancelSync')); this.syncCancellationTokenSource.cancel(); this.syncCancellationTokenSource = new cancellation_1.CancellationTokenSource(); return this.syncCancellationTokenSource.token; } async sync() { const token = this.cancelSync(); return this.run(() => this.doSync(token)); } async doSync(token) { this.trace(log => log('MonacoEditorModel.doSync - enter')); if (token.isCancellationRequested) { this.trace(log => log('MonacoEditorModel.doSync - exit - cancelled')); return; } const value = await this.readContents(); if (value === undefined) { this.trace(log => log('MonacoEditorModel.doSync - exit - resource not found')); return; } if (token.isCancellationRequested) { this.trace(log => log('MonacoEditorModel.doSync - exit - cancelled while looking for a resource')); return; } if (this._dirty) { this.trace(log => log('MonacoEditorModel.doSync - exit - pending dirty changes')); return; } this.resourceVersion = this.resource.version; this.updateModel(() => standaloneServices_1.StandaloneServices.get(model_1.IModelService).updateModel(this.model, value), { ignoreDirty: true, ignoreContentChanges: true }); this.trace(log => log('MonacoEditorModel.doSync - exit')); } async readContents() { try { const options = { encoding: this.getEncoding() }; const content = await (this.resource.readStream ? this.resource.readStream(options) : this.resource.readContents(options)); let value; if (typeof content === 'string') { value = content; } else { value = (0, textModel_1.createTextBufferFactoryFromStream)(content); } this.updateContentEncoding(); this.setValid(true); return value; } catch (e) { this.setValid(false); if (resource_1.ResourceError.NotFound.is(e)) { return undefined; } throw e; } } markAsDirty() { this.trace(log => log('MonacoEditorModel.markAsDirty - enter')); if (this.ignoreDirtyEdits) { this.trace(log => log('MonacoEditorModel.markAsDirty - exit - ignoring dirty changes enabled')); return; } this.cancelSync(); this.setDirty(true); this.trace(log => log('MonacoEditorModel.markAsDirty - exit')); } cancelSave() { this.trace(log => log('MonacoEditorModel.cancelSave')); this.saveCancellationTokenSource.cancel(); this.saveCancellationTokenSource = new cancellation_1.CancellationTokenSource(); return this.saveCancellationTokenSource.token; } scheduleSave(token = this.cancelSave(), overwriteEncoding, options) { return this.run(() => this.doSave(token, overwriteEncoding, options)); } pushContentChanges(contentChanges) { if (!this.ignoreContentChanges) { this.contentChanges.push(...contentChanges); } } fireDidChangeContent(event) { this.trace(log => log(`MonacoEditorModel.fireDidChangeContent - enter - ${JSON.stringify(event, undefined, 2)}`)); if (this.model.getAlternativeVersionId() === this.bufferSavedVersionId) { this.setDirty(false); } else { this.markAsDirty(); } const changeContentEvent = this.asContentChangedEvent(event); this.onDidChangeContentEmitter.fire(changeContentEvent); this.pushContentChanges(changeContentEvent.contentChanges); this.trace(log => log('MonacoEditorModel.fireDidChangeContent - exit')); } asContentChangedEvent(event) { const contentChanges = event.changes.map(change => this.asTextDocumentContentChangeEvent(change)); return { model: this, contentChanges }; } asTextDocumentContentChangeEvent(change) { const range = this.m2p.asRange(change.range); const rangeOffset = change.rangeOffset; const rangeLength = change.rangeLength; const text = change.text; return { range, rangeOffset, rangeLength, text }; } applyEdits(operations, options) { return this.updateModel(() => this.model.applyEdits(operations), options); } updateModel(doUpdate, options) { const resolvedOptions = { ignoreDirty: false, ignoreContentChanges: false, ...options }; const { ignoreDirtyEdits, ignoreContentChanges } = this; this.ignoreDirtyEdits = resolvedOptions.ignoreDirty; this.ignoreContentChanges = resolvedOptions.ignoreContentChanges; try { return doUpdate(); } finally { this.ignoreDirtyEdits = ignoreDirtyEdits; this.ignoreContentChanges = ignoreContentChanges; } } async doSave(token, overwriteEncoding, options) { if (token.isCancellationRequested || !this.resource.saveContents) { return; } await this.fireWillSaveModel(token, options); if (token.isCancellationRequested) { return; } const changes = [...this.contentChanges]; if ((changes.length === 0 && !this.resource.initiallyDirty) && !overwriteEncoding && (options === null || options === void 0 ? void 0 : options.saveReason) !== vscode_languageserver_protocol_1.TextDocumentSaveReason.Manual) { return; } const currentToSaveVersion = this.model.getAlternativeVersionId(); const contentLength = this.model.getValueLength(); const content = this.model.getValue(); try { const encoding = this.getEncoding(); const version = this.resourceVersion; await resource_1.Resource.save(this.resource, { changes, content, contentLength, options: { encoding, overwriteEncoding, version } }, token); this.contentChanges.splice(0, changes.length); this.resourceVersion = this.resource.version; this.updateContentEncoding(); this.setValid(true); if (token.isCancellationRequested && this.model.getAlternativeVersionId() !== currentToSaveVersion) { return; } this.setDirty(false); this.fireDidSaveModel(); } catch (e) { if (!resource_1.ResourceError.OutOfSync.is(e)) { throw e; } } } async fireWillSaveModel(token, options) { await core_1.Listener.await({ model: this, token, options }, this.onWillSaveModelListeners); } fireDidSaveModel() { this.onDidSaveModelEmitter.fire(this.model); } async revert(options) { this.trace(log => log('MonacoEditorModel.revert - enter')); this.cancelSave(); const soft = options && options.soft; if (soft !== true) { const dirty = this._dirty; this._dirty = false; try { await this.sync(); } finally { this._dirty = dirty; } } this.setDirty(false); this.trace(log => log('MonacoEditorModel.revert - exit')); } createSnapshot(preserveBOM) { return { read: () => this.model.getValue(undefined, preserveBOM) }; } applySnapshot(snapshot) { var _a; const value = (_a = saveable_1.Saveable.Snapshot.read(snapshot)) !== null && _a !== void 0 ? _a : ''; this.model.setValue(value); } async serialize() { return buffer_1.BinaryBuffer.fromString(this.model.getValue()); } trace(loggable) { if (this.logger) { this.logger.debug((log) => loggable((message, ...params) => log(message, ...params, this.resource.uri.toString(true)))); } } } exports.MonacoEditorModel = MonacoEditorModel; //# sourceMappingURL=monaco-editor-model.js.map