Files
vscode/src/vs/workbench/common/editor/untitledTextEditorModel.ts
T

214 lines
7.3 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IEncodingSupport, ISaveOptions, IModeSupport } from 'vs/workbench/common/editor';
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
import { URI } from 'vs/base/common/uri';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IModelService } from 'vs/editor/common/services/modelService';
import { Emitter } from 'vs/base/common/event';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService';
import { ITextBufferFactory } from 'vs/editor/common/model';
import { createTextBufferFactory } from 'vs/editor/common/model/textModel';
import { IResolvedTextEditorModel, ITextEditorModel } from 'vs/editor/common/services/resolverService';
import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents';
import { withNullAsUndefined } from 'vs/base/common/types';
export interface IUntitledTextEditorModel extends ITextEditorModel, IModeSupport, IEncodingSupport, IWorkingCopy { }
export class UntitledTextEditorModel extends BaseTextEditorModel implements IUntitledTextEditorModel {
private readonly _onDidChangeContent = this._register(new Emitter<void>());
readonly onDidChangeContent = this._onDidChangeContent.event;
private readonly _onDidChangeFirstLine = this._register(new Emitter<void>());
readonly onDidChangeFirstLine = this._onDidChangeFirstLine.event;
private readonly _onDidChangeDirty = this._register(new Emitter<void>());
readonly onDidChangeDirty = this._onDidChangeDirty.event;
private readonly _onDidChangeEncoding = this._register(new Emitter<void>());
readonly onDidChangeEncoding = this._onDidChangeEncoding.event;
readonly capabilities = WorkingCopyCapabilities.Untitled;
private dirty = false;
private versionId = 0;
private configuredEncoding: string | undefined;
constructor(
private readonly preferredMode: string | undefined,
public readonly resource: URI,
public readonly hasAssociatedFilePath: boolean,
private readonly initialValue: string | undefined,
private preferredEncoding: string | undefined,
@IModeService modeService: IModeService,
@IModelService modelService: IModelService,
@IBackupFileService private readonly backupFileService: IBackupFileService,
@ITextResourceConfigurationService private readonly configurationService: ITextResourceConfigurationService,
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
@ITextFileService private readonly textFileService: ITextFileService
) {
super(modelService, modeService);
// Make known to working copy service
this._register(this.workingCopyService.registerWorkingCopy(this));
this.registerListeners();
}
private registerListeners(): void {
// Config Changes
this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChange()));
}
private onConfigurationChange(): void {
const configuredEncoding = this.configurationService.getValue<string>(this.resource, 'files.encoding');
if (this.configuredEncoding !== configuredEncoding) {
this.configuredEncoding = configuredEncoding;
if (!this.preferredEncoding) {
this._onDidChangeEncoding.fire(); // do not fire event if we have a preferred encoding set
}
}
}
getVersionId(): number {
return this.versionId;
}
getMode(): string | undefined {
if (this.textEditorModel) {
return this.textEditorModel.getModeId();
}
return this.preferredMode;
}
getEncoding(): string | undefined {
return this.preferredEncoding || this.configuredEncoding;
}
setEncoding(encoding: string): void {
const oldEncoding = this.getEncoding();
this.preferredEncoding = encoding;
// Emit if it changed
if (oldEncoding !== this.preferredEncoding) {
this._onDidChangeEncoding.fire();
}
}
isDirty(): boolean {
return this.dirty;
}
private setDirty(dirty: boolean): void {
if (this.dirty === dirty) {
return;
}
this.dirty = dirty;
this._onDidChangeDirty.fire();
}
save(options?: ISaveOptions): Promise<boolean> {
return this.textFileService.save(this.resource, options);
}
async revert(): Promise<boolean> {
this.setDirty(false);
return true;
}
async backup(): Promise<IWorkingCopyBackup> {
return { content: withNullAsUndefined(this.createSnapshot()) };
}
async load(): Promise<UntitledTextEditorModel & IResolvedTextEditorModel> {
// Check for backups
const backup = await this.backupFileService.resolve(this.resource);
// untitled associated to file path are dirty right away as well as untitled with content
this.setDirty(this.hasAssociatedFilePath || !!backup || !!this.initialValue);
let untitledContents: ITextBufferFactory;
if (backup) {
untitledContents = backup.value;
} else {
untitledContents = createTextBufferFactory(this.initialValue || '');
}
// Create text editor model if not yet done
if (!this.textEditorModel) {
this.createTextEditorModel(untitledContents, this.resource, this.preferredMode);
}
// Otherwise update
else {
this.updateTextEditorModel(untitledContents, this.preferredMode);
}
// Encoding
this.configuredEncoding = this.configurationService.getValue<string>(this.resource, 'files.encoding');
// We know for a fact there is a text editor model here
const textEditorModel = this.textEditorModel!;
// Listen to content changes
this._register(textEditorModel.onDidChangeContent(e => this.onModelContentChanged(e)));
// Listen to mode changes
this._register(textEditorModel.onDidChangeLanguage(() => this.onConfigurationChange())); // mode change can have impact on config
// If we have initial contents, make sure to emit this
// as the appropiate events to the outside.
if (backup || this.initialValue) {
this._onDidChangeContent.fire();
this._onDidChangeFirstLine.fire();
}
return this as UntitledTextEditorModel & IResolvedTextEditorModel;
}
private onModelContentChanged(e: IModelContentChangedEvent): void {
if (!this.isResolved()) {
return;
}
this.versionId++;
// mark the untitled text editor as non-dirty once its content becomes empty and we do
// not have an associated path set. we never want dirty indicator in that case.
if (!this.hasAssociatedFilePath && this.textEditorModel.getLineCount() === 1 && this.textEditorModel.getLineContent(1) === '') {
this.setDirty(false);
}
// turn dirty otherwise
else {
this.setDirty(true);
}
// Emit as general content change event
this._onDidChangeContent.fire();
// Emit as first line change event depending on actual change
if (e.changes.some(change => change.range.startLineNumber === 1 || change.range.endLineNumber === 1)) {
this._onDidChangeFirstLine.fire();
}
}
isReadonly(): boolean {
return false;
}
}