mirror of
https://github.com/coder/code-server.git
synced 2026-05-09 05:47:26 +02:00
Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IEncodingSupport, EncodingMode, Verbosity, IModeSupport } from 'vs/workbench/common/editor';
|
||||
import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput';
|
||||
import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel';
|
||||
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
|
||||
import { isEqual } from 'vs/base/common/resources';
|
||||
|
||||
/**
|
||||
* An editor input to be used for untitled text buffers.
|
||||
*/
|
||||
export class UntitledTextEditorInput extends AbstractTextResourceEditorInput implements IEncodingSupport, IModeSupport {
|
||||
|
||||
static readonly ID: string = 'workbench.editors.untitledEditorInput';
|
||||
|
||||
private modelResolve: Promise<IUntitledTextEditorModel & IResolvedTextEditorModel> | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
public readonly model: IUntitledTextEditorModel,
|
||||
@ITextFileService textFileService: ITextFileService,
|
||||
@ILabelService labelService: ILabelService,
|
||||
@IEditorService editorService: IEditorService,
|
||||
@IEditorGroupsService editorGroupService: IEditorGroupsService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IFilesConfigurationService filesConfigurationService: IFilesConfigurationService
|
||||
) {
|
||||
super(model.resource, undefined, editorService, editorGroupService, textFileService, labelService, fileService, filesConfigurationService);
|
||||
|
||||
this.registerModelListeners(model);
|
||||
}
|
||||
|
||||
private registerModelListeners(model: IUntitledTextEditorModel): void {
|
||||
|
||||
// re-emit some events from the model
|
||||
this._register(model.onDidChangeDirty(() => this._onDidChangeDirty.fire()));
|
||||
this._register(model.onDidChangeName(() => this._onDidChangeLabel.fire()));
|
||||
|
||||
// a reverted untitled text editor model renders this input disposed
|
||||
this._register(model.onDidRevert(() => this.dispose()));
|
||||
}
|
||||
|
||||
getTypeId(): string {
|
||||
return UntitledTextEditorInput.ID;
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.model.name;
|
||||
}
|
||||
|
||||
getDescription(verbosity: Verbosity = Verbosity.MEDIUM): string | undefined {
|
||||
|
||||
// Without associated path: only use if name and description differ
|
||||
if (!this.model.hasAssociatedFilePath) {
|
||||
const descriptionCandidate = this.resource.path;
|
||||
if (descriptionCandidate !== this.getName()) {
|
||||
return descriptionCandidate;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// With associated path: delegate to parent
|
||||
return super.getDescription(verbosity);
|
||||
}
|
||||
|
||||
getTitle(verbosity: Verbosity): string {
|
||||
|
||||
// Without associated path: check if name and description differ to decide
|
||||
// if description should appear besides the name to distinguish better
|
||||
if (!this.model.hasAssociatedFilePath) {
|
||||
const name = this.getName();
|
||||
const description = this.getDescription();
|
||||
if (description && description !== name) {
|
||||
return `${name} • ${description}`;
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
// With associated path: delegate to parent
|
||||
return super.getTitle(verbosity);
|
||||
}
|
||||
|
||||
isDirty(): boolean {
|
||||
return this.model.isDirty();
|
||||
}
|
||||
|
||||
getEncoding(): string | undefined {
|
||||
return this.model.getEncoding();
|
||||
}
|
||||
|
||||
setEncoding(encoding: string, mode: EncodingMode /* ignored, we only have Encode */): void {
|
||||
this.model.setEncoding(encoding);
|
||||
}
|
||||
|
||||
setMode(mode: string): void {
|
||||
this.model.setMode(mode);
|
||||
}
|
||||
|
||||
getMode(): string | undefined {
|
||||
return this.model.getMode();
|
||||
}
|
||||
|
||||
resolve(): Promise<IUntitledTextEditorModel & IResolvedTextEditorModel> {
|
||||
|
||||
// Join a model resolve if we have had one before
|
||||
if (this.modelResolve) {
|
||||
return this.modelResolve;
|
||||
}
|
||||
|
||||
this.modelResolve = this.model.load();
|
||||
|
||||
return this.modelResolve;
|
||||
}
|
||||
|
||||
matches(otherInput: unknown): boolean {
|
||||
if (otherInput === this) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (otherInput instanceof UntitledTextEditorInput) {
|
||||
return isEqual(otherInput.resource, this.resource);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.modelResolve = undefined;
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { Event, 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, ITextModel } 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, assertIsDefined } from 'vs/base/common/types';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { ensureValidWordDefinition } from 'vs/editor/common/model/wordHelper';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
export interface IUntitledTextEditorModel extends ITextEditorModel, IModeSupport, IEncodingSupport, IWorkingCopy {
|
||||
|
||||
/**
|
||||
* Emits an event when the encoding of this untitled model changes.
|
||||
*/
|
||||
readonly onDidChangeEncoding: Event<void>;
|
||||
|
||||
/**
|
||||
* Emits an event when the name of this untitled model changes.
|
||||
*/
|
||||
readonly onDidChangeName: Event<void>;
|
||||
|
||||
/**
|
||||
* Emits an event when this untitled model is reverted.
|
||||
*/
|
||||
readonly onDidRevert: Event<void>;
|
||||
|
||||
/**
|
||||
* Whether this untitled text model has an associated file path.
|
||||
*/
|
||||
readonly hasAssociatedFilePath: boolean;
|
||||
|
||||
/**
|
||||
* Whether this model has an explicit language mode or not.
|
||||
*/
|
||||
readonly hasModeSetExplicitly: boolean;
|
||||
|
||||
/**
|
||||
* Sets the encoding to use for this untitled model.
|
||||
*/
|
||||
setEncoding(encoding: string): void;
|
||||
|
||||
/**
|
||||
* Load the untitled model.
|
||||
*/
|
||||
load(): Promise<IUntitledTextEditorModel & IResolvedTextEditorModel>;
|
||||
|
||||
/**
|
||||
* Updates the value of the untitled model optionally allowing to ignore dirty.
|
||||
* The model must be resolved for this method to work.
|
||||
*/
|
||||
setValue(this: IResolvedTextEditorModel, value: string, ignoreDirty?: boolean): void;
|
||||
}
|
||||
|
||||
export class UntitledTextEditorModel extends BaseTextEditorModel implements IUntitledTextEditorModel {
|
||||
|
||||
private static readonly FIRST_LINE_NAME_MAX_LENGTH = 40;
|
||||
private static readonly FIRST_LINE_NAME_CANDIDATE_MAX_LENGTH = UntitledTextEditorModel.FIRST_LINE_NAME_MAX_LENGTH * 10;
|
||||
|
||||
private readonly _onDidChangeContent = this._register(new Emitter<void>());
|
||||
readonly onDidChangeContent = this._onDidChangeContent.event;
|
||||
|
||||
private readonly _onDidChangeName = this._register(new Emitter<void>());
|
||||
readonly onDidChangeName = this._onDidChangeName.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;
|
||||
|
||||
private readonly _onDidRevert = this._register(new Emitter<void>());
|
||||
readonly onDidRevert = this._onDidRevert.event;
|
||||
|
||||
readonly capabilities = WorkingCopyCapabilities.Untitled;
|
||||
|
||||
private cachedModelFirstLineWords: string | undefined = undefined;
|
||||
get name(): string {
|
||||
// Take name from first line if present and only if
|
||||
// we have no associated file path. In that case we
|
||||
// prefer the file name as title.
|
||||
if (this.configuredLabelFormat === 'content' && !this.hasAssociatedFilePath && this.cachedModelFirstLineWords) {
|
||||
return this.cachedModelFirstLineWords;
|
||||
}
|
||||
|
||||
// Otherwise fallback to resource
|
||||
return this.labelService.getUriBasenameLabel(this.resource);
|
||||
}
|
||||
|
||||
private dirty = this.hasAssociatedFilePath || !!this.initialValue;
|
||||
private ignoreDirtyOnModelContentChange = false;
|
||||
|
||||
private versionId = 0;
|
||||
|
||||
private configuredEncoding: string | undefined;
|
||||
private configuredLabelFormat: 'content' | 'name' = 'content';
|
||||
|
||||
constructor(
|
||||
public readonly resource: URI,
|
||||
public readonly hasAssociatedFilePath: boolean,
|
||||
private readonly initialValue: string | undefined,
|
||||
private preferredMode: string | undefined,
|
||||
private preferredEncoding: string | undefined,
|
||||
@IModeService modeService: IModeService,
|
||||
@IModelService modelService: IModelService,
|
||||
@IBackupFileService private readonly backupFileService: IBackupFileService,
|
||||
@ITextResourceConfigurationService private readonly textResourceConfigurationService: ITextResourceConfigurationService,
|
||||
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
|
||||
@ITextFileService private readonly textFileService: ITextFileService,
|
||||
@ILabelService private readonly labelService: ILabelService,
|
||||
@IEditorService private readonly editorService: IEditorService
|
||||
) {
|
||||
super(modelService, modeService);
|
||||
|
||||
// Make known to working copy service
|
||||
this._register(this.workingCopyService.registerWorkingCopy(this));
|
||||
|
||||
if (preferredMode) {
|
||||
this.setMode(preferredMode);
|
||||
}
|
||||
|
||||
// Fetch config
|
||||
this.onConfigurationChange(false);
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
|
||||
// Config Changes
|
||||
this._register(this.textResourceConfigurationService.onDidChangeConfiguration(e => this.onConfigurationChange(true)));
|
||||
}
|
||||
|
||||
private onConfigurationChange(fromEvent: boolean): void {
|
||||
|
||||
// Encoding
|
||||
const configuredEncoding = this.textResourceConfigurationService.getValue<string>(this.resource, 'files.encoding');
|
||||
if (this.configuredEncoding !== configuredEncoding) {
|
||||
this.configuredEncoding = configuredEncoding;
|
||||
|
||||
if (fromEvent && !this.preferredEncoding) {
|
||||
this._onDidChangeEncoding.fire(); // do not fire event if we have a preferred encoding set
|
||||
}
|
||||
}
|
||||
|
||||
// Label Format
|
||||
const configuredLabelFormat = this.textResourceConfigurationService.getValue<string>(this.resource, 'workbench.editor.untitled.labelFormat');
|
||||
if (this.configuredLabelFormat !== configuredLabelFormat && (configuredLabelFormat === 'content' || configuredLabelFormat === 'name')) {
|
||||
this.configuredLabelFormat = configuredLabelFormat;
|
||||
|
||||
if (fromEvent) {
|
||||
this._onDidChangeName.fire();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getVersionId(): number {
|
||||
return this.versionId;
|
||||
}
|
||||
|
||||
private _hasModeSetExplicitly: boolean = false;
|
||||
get hasModeSetExplicitly(): boolean { return this._hasModeSetExplicitly; }
|
||||
|
||||
setMode(mode: string): void {
|
||||
|
||||
// Remember that an explicit mode was set
|
||||
this._hasModeSetExplicitly = true;
|
||||
|
||||
let actualMode: string | undefined = undefined;
|
||||
if (mode === '${activeEditorLanguage}') {
|
||||
// support the special '${activeEditorLanguage}' mode by
|
||||
// looking up the language mode from the currently
|
||||
// active text editor if any
|
||||
actualMode = this.editorService.activeTextEditorMode;
|
||||
} else {
|
||||
actualMode = mode;
|
||||
}
|
||||
|
||||
this.preferredMode = actualMode;
|
||||
|
||||
if (actualMode) {
|
||||
super.setMode(actualMode);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
setValue(value: string, ignoreDirty?: boolean): void {
|
||||
if (ignoreDirty) {
|
||||
this.ignoreDirtyOnModelContentChange = true;
|
||||
}
|
||||
|
||||
try {
|
||||
this.updateTextEditorModel(createTextBufferFactory(value));
|
||||
} finally {
|
||||
this.ignoreDirtyOnModelContentChange = false;
|
||||
}
|
||||
}
|
||||
|
||||
isReadonly(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isDirty(): boolean {
|
||||
return this.dirty;
|
||||
}
|
||||
|
||||
private setDirty(dirty: boolean): void {
|
||||
if (this.dirty === dirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dirty = dirty;
|
||||
this._onDidChangeDirty.fire();
|
||||
}
|
||||
|
||||
async save(options?: ISaveOptions): Promise<boolean> {
|
||||
const target = await this.textFileService.save(this.resource, options);
|
||||
|
||||
return !!target;
|
||||
}
|
||||
|
||||
async revert(): Promise<void> {
|
||||
this.setDirty(false);
|
||||
|
||||
// Emit as event
|
||||
this._onDidRevert.fire();
|
||||
|
||||
// A reverted untitled model is invalid because it has
|
||||
// no actual source on disk to revert to. As such we
|
||||
// dispose the model.
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
async backup(token: CancellationToken): Promise<IWorkingCopyBackup> {
|
||||
return { content: withNullAsUndefined(this.createSnapshot()) };
|
||||
}
|
||||
|
||||
async load(): Promise<UntitledTextEditorModel & IResolvedTextEditorModel> {
|
||||
|
||||
// Check for backups
|
||||
const backup = await this.backupFileService.resolve(this.resource);
|
||||
|
||||
let untitledContents: ITextBufferFactory;
|
||||
if (backup) {
|
||||
untitledContents = backup.value;
|
||||
} else {
|
||||
untitledContents = createTextBufferFactory(this.initialValue || '');
|
||||
}
|
||||
|
||||
// Create text editor model if not yet done
|
||||
let createdUntitledModel = false;
|
||||
if (!this.textEditorModel) {
|
||||
this.createTextEditorModel(untitledContents, this.resource, this.preferredMode);
|
||||
createdUntitledModel = true;
|
||||
}
|
||||
|
||||
// Otherwise: the untitled model already exists and we must assume
|
||||
// that the value of the model was changed by the user. As such we
|
||||
// do not update the contents, only the mode if configured.
|
||||
else {
|
||||
this.updateTextEditorModel(undefined, this.preferredMode);
|
||||
}
|
||||
|
||||
// Listen to text model events
|
||||
const textEditorModel = assertIsDefined(this.textEditorModel);
|
||||
this._register(textEditorModel.onDidChangeContent(e => this.onModelContentChanged(textEditorModel, e)));
|
||||
this._register(textEditorModel.onDidChangeLanguage(() => this.onConfigurationChange(true))); // mode change can have impact on config
|
||||
|
||||
// Only adjust name and dirty state etc. if we
|
||||
// actually created the untitled model
|
||||
if (createdUntitledModel) {
|
||||
|
||||
// Name
|
||||
if (backup || this.initialValue) {
|
||||
this.updateNameFromFirstLine(textEditorModel);
|
||||
}
|
||||
|
||||
// Untitled associated to file path are dirty right away as well as untitled with content
|
||||
this.setDirty(this.hasAssociatedFilePath || !!backup || !!this.initialValue);
|
||||
|
||||
// If we have initial contents, make sure to emit this
|
||||
// as the appropiate events to the outside.
|
||||
if (backup || this.initialValue) {
|
||||
this._onDidChangeContent.fire();
|
||||
}
|
||||
}
|
||||
|
||||
return this as UntitledTextEditorModel & IResolvedTextEditorModel;
|
||||
}
|
||||
|
||||
private onModelContentChanged(textEditorModel: ITextModel, e: IModelContentChangedEvent): void {
|
||||
this.versionId++;
|
||||
|
||||
if (!this.ignoreDirtyOnModelContentChange) {
|
||||
// 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 && textEditorModel.getLineCount() === 1 && textEditorModel.getLineContent(1) === '') {
|
||||
this.setDirty(false);
|
||||
}
|
||||
|
||||
// turn dirty otherwise
|
||||
else {
|
||||
this.setDirty(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for name change if first line changed in the range of 0-FIRST_LINE_NAME_CANDIDATE_MAX_LENGTH columns
|
||||
if (e.changes.some(change => (change.range.startLineNumber === 1 || change.range.endLineNumber === 1) && change.range.startColumn <= UntitledTextEditorModel.FIRST_LINE_NAME_CANDIDATE_MAX_LENGTH)) {
|
||||
this.updateNameFromFirstLine(textEditorModel);
|
||||
}
|
||||
|
||||
// Emit as general content change event
|
||||
this._onDidChangeContent.fire();
|
||||
}
|
||||
|
||||
private updateNameFromFirstLine(textEditorModel: ITextModel): void {
|
||||
if (this.hasAssociatedFilePath) {
|
||||
return; // not in case of an associated file path
|
||||
}
|
||||
|
||||
// Determine the first words of the model following these rules:
|
||||
// - cannot be only whitespace (so we trim())
|
||||
// - cannot be only non-alphanumeric characters (so we run word definition regex over it)
|
||||
// - cannot be longer than FIRST_LINE_MAX_TITLE_LENGTH
|
||||
// - normalize multiple whitespaces to a single whitespace
|
||||
|
||||
let modelFirstWordsCandidate: string | undefined = undefined;
|
||||
|
||||
const firstLineText = textEditorModel
|
||||
.getValueInRange({
|
||||
startLineNumber: 1,
|
||||
endLineNumber: 1,
|
||||
startColumn: 1,
|
||||
endColumn: UntitledTextEditorModel.FIRST_LINE_NAME_CANDIDATE_MAX_LENGTH + 1 // first cap at FIRST_LINE_NAME_CANDIDATE_MAX_LENGTH
|
||||
})
|
||||
.trim().replace(/\s+/g, ' ') // normalize whitespaces
|
||||
.substr(0, UntitledTextEditorModel.FIRST_LINE_NAME_MAX_LENGTH); // finally cap at FIRST_LINE_NAME_MAX_LENGTH
|
||||
|
||||
if (firstLineText && ensureValidWordDefinition().exec(firstLineText)) {
|
||||
modelFirstWordsCandidate = firstLineText;
|
||||
}
|
||||
|
||||
if (modelFirstWordsCandidate !== this.cachedModelFirstLineWords) {
|
||||
this.cachedModelFirstLineWords = modelFirstWordsCandidate;
|
||||
this._onDidChangeName.fire();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { UntitledTextEditorModel, IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel';
|
||||
import { IFilesConfiguration } from 'vs/platform/files/common/files';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { ResourceMap } from 'vs/base/common/map';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService';
|
||||
|
||||
export const IUntitledTextEditorService = createDecorator<IUntitledTextEditorService>('untitledTextEditorService');
|
||||
|
||||
export interface INewUntitledTextEditorOptions {
|
||||
|
||||
/**
|
||||
* Initial value of the untitled editor. An untitled editor with initial
|
||||
* value is dirty right from the beginning.
|
||||
*/
|
||||
initialValue?: string;
|
||||
|
||||
/**
|
||||
* Preferred language mode to use when saving the untitled editor.
|
||||
*/
|
||||
mode?: string;
|
||||
|
||||
/**
|
||||
* Preferred encoding to use when saving the untitled editor.
|
||||
*/
|
||||
encoding?: string;
|
||||
}
|
||||
|
||||
export interface IExistingUntitledTextEditorOptions extends INewUntitledTextEditorOptions {
|
||||
|
||||
/**
|
||||
* A resource to identify the untitled editor to create or return
|
||||
* if already existing.
|
||||
*
|
||||
* Note: the resource will not be used unless the scheme is `untitled`.
|
||||
*/
|
||||
untitledResource?: URI;
|
||||
}
|
||||
|
||||
export interface INewUntitledTextEditorWithAssociatedResourceOptions extends INewUntitledTextEditorOptions {
|
||||
|
||||
/**
|
||||
* Resource components to associate with the untitled editor. When saving
|
||||
* the untitled editor, the associated components will be used and the user
|
||||
* is not being asked to provide a file path.
|
||||
*
|
||||
* Note: currently it is not possible to specify the `scheme` to use. The
|
||||
* untitled editor will saved to the default local or remote resource.
|
||||
*/
|
||||
associatedResource?: { authority: string; path: string; query: string; fragment: string; }
|
||||
}
|
||||
|
||||
type IInternalUntitledTextEditorOptions = IExistingUntitledTextEditorOptions & INewUntitledTextEditorWithAssociatedResourceOptions;
|
||||
|
||||
export interface IUntitledTextEditorModelManager {
|
||||
|
||||
/**
|
||||
* Events for when untitled text editors change (e.g. getting dirty, saved or reverted).
|
||||
*/
|
||||
readonly onDidChangeDirty: Event<IUntitledTextEditorModel>;
|
||||
|
||||
/**
|
||||
* Events for when untitled text editor encodings change.
|
||||
*/
|
||||
readonly onDidChangeEncoding: Event<IUntitledTextEditorModel>;
|
||||
|
||||
/**
|
||||
* Events for when untitled text editor labels change.
|
||||
*/
|
||||
readonly onDidChangeLabel: Event<IUntitledTextEditorModel>;
|
||||
|
||||
/**
|
||||
* Events for when untitled text editors are disposed.
|
||||
*/
|
||||
readonly onDidDispose: Event<IUntitledTextEditorModel>;
|
||||
|
||||
/**
|
||||
* Creates a new untitled editor model with the provided options. If the `untitledResource`
|
||||
* property is provided and the untitled editor exists, it will return that existing
|
||||
* instance instead of creating a new one.
|
||||
*/
|
||||
create(options?: INewUntitledTextEditorOptions): IUntitledTextEditorModel;
|
||||
create(options?: INewUntitledTextEditorWithAssociatedResourceOptions): IUntitledTextEditorModel;
|
||||
create(options?: IExistingUntitledTextEditorOptions): IUntitledTextEditorModel;
|
||||
|
||||
/**
|
||||
* Returns an existing untitled editor model if already created before.
|
||||
*/
|
||||
get(resource: URI): IUntitledTextEditorModel | undefined;
|
||||
|
||||
/**
|
||||
* Resolves an untitled editor model from the provided options. If the `untitledResource`
|
||||
* property is provided and the untitled editor exists, it will return that existing
|
||||
* instance instead of creating a new one.
|
||||
*/
|
||||
resolve(options?: INewUntitledTextEditorOptions): Promise<IUntitledTextEditorModel & IResolvedTextEditorModel>;
|
||||
resolve(options?: INewUntitledTextEditorWithAssociatedResourceOptions): Promise<IUntitledTextEditorModel & IResolvedTextEditorModel>;
|
||||
resolve(options?: IExistingUntitledTextEditorOptions): Promise<IUntitledTextEditorModel & IResolvedTextEditorModel>;
|
||||
}
|
||||
|
||||
export interface IUntitledTextEditorService extends IUntitledTextEditorModelManager {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
export class UntitledTextEditorService extends Disposable implements IUntitledTextEditorService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidChangeDirty = this._register(new Emitter<IUntitledTextEditorModel>());
|
||||
readonly onDidChangeDirty = this._onDidChangeDirty.event;
|
||||
|
||||
private readonly _onDidChangeEncoding = this._register(new Emitter<IUntitledTextEditorModel>());
|
||||
readonly onDidChangeEncoding = this._onDidChangeEncoding.event;
|
||||
|
||||
private readonly _onDidDispose = this._register(new Emitter<IUntitledTextEditorModel>());
|
||||
readonly onDidDispose = this._onDidDispose.event;
|
||||
|
||||
private readonly _onDidChangeLabel = this._register(new Emitter<IUntitledTextEditorModel>());
|
||||
readonly onDidChangeLabel = this._onDidChangeLabel.event;
|
||||
|
||||
private readonly mapResourceToModel = new ResourceMap<UntitledTextEditorModel>();
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
get(resource: URI): UntitledTextEditorModel | undefined {
|
||||
return this.mapResourceToModel.get(resource);
|
||||
}
|
||||
|
||||
resolve(options?: IInternalUntitledTextEditorOptions): Promise<UntitledTextEditorModel & IResolvedTextEditorModel> {
|
||||
return this.doCreateOrGet(options).load();
|
||||
}
|
||||
|
||||
create(options?: IInternalUntitledTextEditorOptions): UntitledTextEditorModel {
|
||||
return this.doCreateOrGet(options);
|
||||
}
|
||||
|
||||
private doCreateOrGet(options: IInternalUntitledTextEditorOptions = Object.create(null)): UntitledTextEditorModel {
|
||||
const massagedOptions = this.massageOptions(options);
|
||||
|
||||
// Return existing instance if asked for it
|
||||
if (massagedOptions.untitledResource && this.mapResourceToModel.has(massagedOptions.untitledResource)) {
|
||||
return this.mapResourceToModel.get(massagedOptions.untitledResource)!;
|
||||
}
|
||||
|
||||
// Create new instance otherwise
|
||||
return this.doCreate(massagedOptions);
|
||||
}
|
||||
|
||||
private massageOptions(options: IInternalUntitledTextEditorOptions): IInternalUntitledTextEditorOptions {
|
||||
const massagedOptions: IInternalUntitledTextEditorOptions = Object.create(null);
|
||||
|
||||
// Figure out associated and untitled resource
|
||||
if (options.associatedResource) {
|
||||
massagedOptions.untitledResource = URI.from({
|
||||
scheme: Schemas.untitled,
|
||||
authority: options.associatedResource.authority,
|
||||
fragment: options.associatedResource.fragment,
|
||||
path: options.associatedResource.path,
|
||||
query: options.associatedResource.query
|
||||
});
|
||||
massagedOptions.associatedResource = options.associatedResource;
|
||||
} else {
|
||||
if (options.untitledResource?.scheme === Schemas.untitled) {
|
||||
massagedOptions.untitledResource = options.untitledResource;
|
||||
}
|
||||
}
|
||||
|
||||
// Language mode
|
||||
if (options.mode) {
|
||||
massagedOptions.mode = options.mode;
|
||||
} else if (!massagedOptions.associatedResource) {
|
||||
const configuration = this.configurationService.getValue<IFilesConfiguration>();
|
||||
if (configuration.files?.defaultLanguage) {
|
||||
massagedOptions.mode = configuration.files.defaultLanguage;
|
||||
}
|
||||
}
|
||||
|
||||
// Take over encoding and initial value
|
||||
massagedOptions.encoding = options.encoding;
|
||||
massagedOptions.initialValue = options.initialValue;
|
||||
|
||||
return massagedOptions;
|
||||
}
|
||||
|
||||
private doCreate(options: IInternalUntitledTextEditorOptions): UntitledTextEditorModel {
|
||||
|
||||
// Create a new untitled resource if none is provided
|
||||
let untitledResource = options.untitledResource;
|
||||
if (!untitledResource) {
|
||||
let counter = 1;
|
||||
do {
|
||||
untitledResource = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter}` });
|
||||
counter++;
|
||||
} while (this.mapResourceToModel.has(untitledResource));
|
||||
}
|
||||
|
||||
// Create new model with provided options
|
||||
const model = this._register(this.instantiationService.createInstance(UntitledTextEditorModel, untitledResource, !!options.associatedResource, options.initialValue, options.mode, options.encoding));
|
||||
|
||||
this.registerModel(model);
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
private registerModel(model: UntitledTextEditorModel): void {
|
||||
|
||||
// Install model listeners
|
||||
const modelListeners = new DisposableStore();
|
||||
modelListeners.add(model.onDidChangeDirty(() => this._onDidChangeDirty.fire(model)));
|
||||
modelListeners.add(model.onDidChangeName(() => this._onDidChangeLabel.fire(model)));
|
||||
modelListeners.add(model.onDidChangeEncoding(() => this._onDidChangeEncoding.fire(model)));
|
||||
modelListeners.add(model.onDispose(() => this._onDidDispose.fire(model)));
|
||||
|
||||
// Remove from cache on dispose
|
||||
Event.once(model.onDispose)(() => {
|
||||
|
||||
// Registry
|
||||
this.mapResourceToModel.delete(model.resource);
|
||||
|
||||
// Listeners
|
||||
modelListeners.dispose();
|
||||
});
|
||||
|
||||
// Add to cache
|
||||
this.mapResourceToModel.set(model.resource, model);
|
||||
|
||||
// If the model is dirty right from the beginning,
|
||||
// make sure to emit this as an event
|
||||
if (model.isDirty()) {
|
||||
this._onDidChangeDirty.fire(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IUntitledTextEditorService, UntitledTextEditorService, true);
|
||||
@@ -0,0 +1,524 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import * as assert from 'assert';
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IUntitledTextEditorService, UntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
|
||||
import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices';
|
||||
import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
|
||||
import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput';
|
||||
import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel';
|
||||
|
||||
suite('Untitled text editors', () => {
|
||||
|
||||
let instantiationService: IInstantiationService;
|
||||
let accessor: TestServiceAccessor;
|
||||
|
||||
setup(() => {
|
||||
instantiationService = workbenchInstantiationService();
|
||||
accessor = instantiationService.createInstance(TestServiceAccessor);
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
(accessor.untitledTextEditorService as UntitledTextEditorService).dispose();
|
||||
});
|
||||
|
||||
test('basics', async () => {
|
||||
const service = accessor.untitledTextEditorService;
|
||||
const workingCopyService = accessor.workingCopyService;
|
||||
|
||||
const input1 = instantiationService.createInstance(UntitledTextEditorInput, service.create());
|
||||
await input1.resolve();
|
||||
assert.equal(service.get(input1.resource), input1.model);
|
||||
|
||||
assert.ok(service.get(input1.resource));
|
||||
assert.ok(!service.get(URI.file('testing')));
|
||||
|
||||
const input2 = instantiationService.createInstance(UntitledTextEditorInput, service.create());
|
||||
assert.equal(service.get(input2.resource), input2.model);
|
||||
|
||||
// get()
|
||||
assert.equal(service.get(input1.resource), input1.model);
|
||||
assert.equal(service.get(input2.resource), input2.model);
|
||||
|
||||
// revert()
|
||||
await input1.revert(0);
|
||||
assert.ok(input1.isDisposed());
|
||||
assert.ok(!service.get(input1.resource));
|
||||
|
||||
// dirty
|
||||
const model = await input2.resolve();
|
||||
assert.equal(await service.resolve({ untitledResource: input2.resource }), model);
|
||||
assert.ok(service.get(model.resource));
|
||||
|
||||
assert.ok(!input2.isDirty());
|
||||
|
||||
const resourcePromise = awaitDidChangeDirty(accessor.untitledTextEditorService);
|
||||
|
||||
model.textEditorModel.setValue('foo bar');
|
||||
|
||||
const resource = await resourcePromise;
|
||||
|
||||
assert.equal(resource.toString(), input2.resource.toString());
|
||||
|
||||
assert.ok(input2.isDirty());
|
||||
|
||||
assert.ok(workingCopyService.isDirty(input2.resource));
|
||||
assert.equal(workingCopyService.dirtyCount, 1);
|
||||
|
||||
await input1.revert(0);
|
||||
await input2.revert(0);
|
||||
assert.ok(!service.get(input1.resource));
|
||||
assert.ok(!service.get(input2.resource));
|
||||
assert.ok(!input2.isDirty());
|
||||
assert.ok(!model.isDirty());
|
||||
|
||||
assert.ok(!workingCopyService.isDirty(input2.resource));
|
||||
assert.equal(workingCopyService.dirtyCount, 0);
|
||||
|
||||
await input1.revert(0);
|
||||
assert.ok(input1.isDisposed());
|
||||
assert.ok(!service.get(input1.resource));
|
||||
|
||||
input2.dispose();
|
||||
assert.ok(!service.get(input2.resource));
|
||||
});
|
||||
|
||||
function awaitDidChangeDirty(service: IUntitledTextEditorService): Promise<URI> {
|
||||
return new Promise(resolve => {
|
||||
const listener = service.onDidChangeDirty(async model => {
|
||||
listener.dispose();
|
||||
|
||||
resolve(model.resource);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test('setValue()', async () => {
|
||||
const service = accessor.untitledTextEditorService;
|
||||
const untitled = instantiationService.createInstance(UntitledTextEditorInput, service.create());
|
||||
|
||||
const model = await untitled.resolve();
|
||||
|
||||
model.setValue('not dirty', true);
|
||||
assert.ok(!model.isDirty());
|
||||
|
||||
model.setValue('dirty');
|
||||
assert.ok(model.isDirty());
|
||||
|
||||
untitled.dispose();
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('associated resource is dirty', async () => {
|
||||
const service = accessor.untitledTextEditorService;
|
||||
const file = URI.file(join('C:\\', '/foo/file.txt'));
|
||||
|
||||
let onDidChangeDirtyModel: IUntitledTextEditorModel | undefined = undefined;
|
||||
const listener = service.onDidChangeDirty(model => {
|
||||
onDidChangeDirtyModel = model;
|
||||
});
|
||||
|
||||
const model = service.create({ associatedResource: file });
|
||||
const untitled = instantiationService.createInstance(UntitledTextEditorInput, model);
|
||||
assert.ok(untitled.isDirty());
|
||||
assert.equal(model, onDidChangeDirtyModel);
|
||||
|
||||
const resolvedModel = await untitled.resolve();
|
||||
|
||||
assert.ok(resolvedModel.hasAssociatedFilePath);
|
||||
assert.equal(untitled.isDirty(), true);
|
||||
|
||||
untitled.dispose();
|
||||
listener.dispose();
|
||||
});
|
||||
|
||||
test('no longer dirty when content gets empty (not with associated resource)', async () => {
|
||||
const service = accessor.untitledTextEditorService;
|
||||
const workingCopyService = accessor.workingCopyService;
|
||||
const input = instantiationService.createInstance(UntitledTextEditorInput, service.create());
|
||||
|
||||
// dirty
|
||||
const model = await input.resolve();
|
||||
model.textEditorModel.setValue('foo bar');
|
||||
assert.ok(model.isDirty());
|
||||
assert.ok(workingCopyService.isDirty(model.resource));
|
||||
model.textEditorModel.setValue('');
|
||||
assert.ok(!model.isDirty());
|
||||
assert.ok(!workingCopyService.isDirty(model.resource));
|
||||
input.dispose();
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('via create options', async () => {
|
||||
const service = accessor.untitledTextEditorService;
|
||||
|
||||
const model1 = await instantiationService.createInstance(UntitledTextEditorInput, service.create()).resolve();
|
||||
|
||||
model1.textEditorModel!.setValue('foo bar');
|
||||
assert.ok(model1.isDirty());
|
||||
|
||||
model1.textEditorModel!.setValue('');
|
||||
assert.ok(!model1.isDirty());
|
||||
|
||||
const model2 = await instantiationService.createInstance(UntitledTextEditorInput, service.create({ initialValue: 'Hello World' })).resolve();
|
||||
assert.equal(snapshotToString(model2.createSnapshot()!), 'Hello World');
|
||||
|
||||
const input = instantiationService.createInstance(UntitledTextEditorInput, service.create());
|
||||
|
||||
const model3 = await instantiationService.createInstance(UntitledTextEditorInput, service.create({ untitledResource: input.resource })).resolve();
|
||||
|
||||
assert.equal(model3.resource.toString(), input.resource.toString());
|
||||
|
||||
const file = URI.file(join('C:\\', '/foo/file44.txt'));
|
||||
const model4 = await instantiationService.createInstance(UntitledTextEditorInput, service.create({ associatedResource: file })).resolve();
|
||||
assert.ok(model4.hasAssociatedFilePath);
|
||||
assert.ok(model4.isDirty());
|
||||
|
||||
model1.dispose();
|
||||
model2.dispose();
|
||||
model3.dispose();
|
||||
model4.dispose();
|
||||
input.dispose();
|
||||
});
|
||||
|
||||
test('associated path remains dirty when content gets empty', async () => {
|
||||
const service = accessor.untitledTextEditorService;
|
||||
const file = URI.file(join('C:\\', '/foo/file.txt'));
|
||||
const input = instantiationService.createInstance(UntitledTextEditorInput, service.create({ associatedResource: file }));
|
||||
|
||||
// dirty
|
||||
const model = await input.resolve();
|
||||
model.textEditorModel.setValue('foo bar');
|
||||
assert.ok(model.isDirty());
|
||||
model.textEditorModel.setValue('');
|
||||
assert.ok(model.isDirty());
|
||||
input.dispose();
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('initial content is dirty', async () => {
|
||||
const service = accessor.untitledTextEditorService;
|
||||
const workingCopyService = accessor.workingCopyService;
|
||||
|
||||
const untitled = instantiationService.createInstance(UntitledTextEditorInput, service.create({ initialValue: 'Hello World' }));
|
||||
assert.ok(untitled.isDirty());
|
||||
|
||||
// dirty
|
||||
const model = await untitled.resolve();
|
||||
assert.ok(model.isDirty());
|
||||
assert.equal(workingCopyService.dirtyCount, 1);
|
||||
|
||||
untitled.dispose();
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('created with files.defaultLanguage setting', () => {
|
||||
const defaultLanguage = 'javascript';
|
||||
const config = accessor.testConfigurationService;
|
||||
config.setUserConfiguration('files', { 'defaultLanguage': defaultLanguage });
|
||||
|
||||
const service = accessor.untitledTextEditorService;
|
||||
const input = service.create();
|
||||
|
||||
assert.equal(input.getMode(), defaultLanguage);
|
||||
|
||||
config.setUserConfiguration('files', { 'defaultLanguage': undefined });
|
||||
|
||||
input.dispose();
|
||||
});
|
||||
|
||||
test('created with files.defaultLanguage setting (${activeEditorLanguage})', async () => {
|
||||
const config = accessor.testConfigurationService;
|
||||
config.setUserConfiguration('files', { 'defaultLanguage': '${activeEditorLanguage}' });
|
||||
|
||||
accessor.editorService.activeTextEditorMode = 'typescript';
|
||||
|
||||
const service = accessor.untitledTextEditorService;
|
||||
const model = service.create();
|
||||
|
||||
assert.equal(model.getMode(), 'typescript');
|
||||
|
||||
config.setUserConfiguration('files', { 'defaultLanguage': undefined });
|
||||
accessor.editorService.activeTextEditorMode = undefined;
|
||||
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('created with mode overrides files.defaultLanguage setting', () => {
|
||||
const mode = 'typescript';
|
||||
const defaultLanguage = 'javascript';
|
||||
const config = accessor.testConfigurationService;
|
||||
config.setUserConfiguration('files', { 'defaultLanguage': defaultLanguage });
|
||||
|
||||
const service = accessor.untitledTextEditorService;
|
||||
const input = service.create({ mode });
|
||||
|
||||
assert.equal(input.getMode(), mode);
|
||||
|
||||
config.setUserConfiguration('files', { 'defaultLanguage': undefined });
|
||||
|
||||
input.dispose();
|
||||
});
|
||||
|
||||
test('can change mode afterwards', async () => {
|
||||
const mode = 'untitled-input-test';
|
||||
|
||||
ModesRegistry.registerLanguage({
|
||||
id: mode,
|
||||
});
|
||||
|
||||
const service = accessor.untitledTextEditorService;
|
||||
const input = instantiationService.createInstance(UntitledTextEditorInput, service.create({ mode }));
|
||||
|
||||
assert.ok(input.model.hasModeSetExplicitly);
|
||||
assert.equal(input.getMode(), mode);
|
||||
|
||||
const model = await input.resolve();
|
||||
assert.equal(model.getMode(), mode);
|
||||
|
||||
input.setMode('plaintext');
|
||||
|
||||
assert.equal(input.getMode(), PLAINTEXT_MODE_ID);
|
||||
|
||||
input.dispose();
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('remembers that mode was set explicitly', async () => {
|
||||
const mode = 'untitled-input-test';
|
||||
|
||||
ModesRegistry.registerLanguage({
|
||||
id: mode,
|
||||
});
|
||||
|
||||
const service = accessor.untitledTextEditorService;
|
||||
const model = service.create();
|
||||
const input = instantiationService.createInstance(UntitledTextEditorInput, model);
|
||||
|
||||
assert.ok(!input.model.hasModeSetExplicitly);
|
||||
input.setMode('plaintext');
|
||||
assert.ok(input.model.hasModeSetExplicitly);
|
||||
|
||||
assert.equal(input.getMode(), PLAINTEXT_MODE_ID);
|
||||
|
||||
input.dispose();
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('service#onDidChangeEncoding', async () => {
|
||||
const service = accessor.untitledTextEditorService;
|
||||
const input = instantiationService.createInstance(UntitledTextEditorInput, service.create());
|
||||
|
||||
let counter = 0;
|
||||
|
||||
service.onDidChangeEncoding(model => {
|
||||
counter++;
|
||||
assert.equal(model.resource.toString(), input.resource.toString());
|
||||
});
|
||||
|
||||
// encoding
|
||||
const model = await input.resolve();
|
||||
model.setEncoding('utf16');
|
||||
assert.equal(counter, 1);
|
||||
input.dispose();
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('service#onDidChangeLabel', async () => {
|
||||
const service = accessor.untitledTextEditorService;
|
||||
const input = instantiationService.createInstance(UntitledTextEditorInput, service.create());
|
||||
|
||||
let counter = 0;
|
||||
|
||||
service.onDidChangeLabel(model => {
|
||||
counter++;
|
||||
assert.equal(model.resource.toString(), input.resource.toString());
|
||||
});
|
||||
|
||||
// label
|
||||
const model = await input.resolve();
|
||||
model.textEditorModel.setValue('Foo Bar');
|
||||
assert.equal(counter, 1);
|
||||
input.dispose();
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('service#onDidDisposeModel', async () => {
|
||||
const service = accessor.untitledTextEditorService;
|
||||
const input = instantiationService.createInstance(UntitledTextEditorInput, service.create());
|
||||
|
||||
let counter = 0;
|
||||
|
||||
service.onDidDispose(model => {
|
||||
counter++;
|
||||
assert.equal(model.resource.toString(), input.resource.toString());
|
||||
});
|
||||
|
||||
const model = await input.resolve();
|
||||
assert.equal(counter, 0);
|
||||
model.dispose();
|
||||
assert.equal(counter, 1);
|
||||
});
|
||||
|
||||
test('model#onDidChangeContent', async function () {
|
||||
const service = accessor.untitledTextEditorService;
|
||||
const input = instantiationService.createInstance(UntitledTextEditorInput, service.create());
|
||||
|
||||
let counter = 0;
|
||||
|
||||
const model = await input.resolve();
|
||||
model.onDidChangeContent(() => counter++);
|
||||
|
||||
model.textEditorModel.setValue('foo');
|
||||
|
||||
assert.equal(counter, 1, 'Dirty model should trigger event');
|
||||
model.textEditorModel.setValue('bar');
|
||||
|
||||
assert.equal(counter, 2, 'Content change when dirty should trigger event');
|
||||
model.textEditorModel.setValue('');
|
||||
|
||||
assert.equal(counter, 3, 'Manual revert should trigger event');
|
||||
model.textEditorModel.setValue('foo');
|
||||
|
||||
assert.equal(counter, 4, 'Dirty model should trigger event');
|
||||
|
||||
input.dispose();
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('model#onDidRevert and input disposed when reverted', async function () {
|
||||
const service = accessor.untitledTextEditorService;
|
||||
const input = instantiationService.createInstance(UntitledTextEditorInput, service.create());
|
||||
|
||||
let counter = 0;
|
||||
|
||||
const model = await input.resolve();
|
||||
model.onDidRevert(() => counter++);
|
||||
|
||||
model.textEditorModel.setValue('foo');
|
||||
|
||||
await model.revert();
|
||||
|
||||
assert.ok(input.isDisposed());
|
||||
assert.ok(counter === 1);
|
||||
});
|
||||
|
||||
test('model#onDidChangeName and input name', async function () {
|
||||
const service = accessor.untitledTextEditorService;
|
||||
const input = instantiationService.createInstance(UntitledTextEditorInput, service.create());
|
||||
|
||||
let counter = 0;
|
||||
|
||||
let model = await input.resolve();
|
||||
model.onDidChangeName(() => counter++);
|
||||
|
||||
model.textEditorModel.setValue('foo');
|
||||
assert.equal(input.getName(), 'foo');
|
||||
assert.equal(model.name, 'foo');
|
||||
|
||||
assert.equal(counter, 1);
|
||||
model.textEditorModel.setValue('bar');
|
||||
assert.equal(input.getName(), 'bar');
|
||||
assert.equal(model.name, 'bar');
|
||||
|
||||
assert.equal(counter, 2);
|
||||
model.textEditorModel.setValue('');
|
||||
assert.equal(input.getName(), 'Untitled-1');
|
||||
assert.equal(model.name, 'Untitled-1');
|
||||
|
||||
model.textEditorModel.setValue(' ');
|
||||
assert.equal(input.getName(), 'Untitled-1');
|
||||
assert.equal(model.name, 'Untitled-1');
|
||||
|
||||
model.textEditorModel.setValue('([]}'); // require actual words
|
||||
assert.equal(input.getName(), 'Untitled-1');
|
||||
assert.equal(model.name, 'Untitled-1');
|
||||
|
||||
model.textEditorModel.setValue('([]}hello '); // require actual words
|
||||
assert.equal(input.getName(), '([]}hello');
|
||||
assert.equal(model.name, '([]}hello');
|
||||
|
||||
assert.equal(counter, 4);
|
||||
|
||||
model.textEditorModel.setValue('Hello\nWorld');
|
||||
assert.equal(counter, 5);
|
||||
|
||||
function createSingleEditOp(text: string, positionLineNumber: number, positionColumn: number, selectionLineNumber: number = positionLineNumber, selectionColumn: number = positionColumn): IIdentifiedSingleEditOperation {
|
||||
let range = new Range(
|
||||
selectionLineNumber,
|
||||
selectionColumn,
|
||||
positionLineNumber,
|
||||
positionColumn
|
||||
);
|
||||
|
||||
return {
|
||||
identifier: null,
|
||||
range,
|
||||
text,
|
||||
forceMoveMarkers: false
|
||||
};
|
||||
}
|
||||
|
||||
model.textEditorModel.applyEdits([createSingleEditOp('hello', 2, 2)]);
|
||||
assert.equal(counter, 5); // change was not on first line
|
||||
|
||||
input.dispose();
|
||||
model.dispose();
|
||||
|
||||
const inputWithContents = instantiationService.createInstance(UntitledTextEditorInput, service.create({ initialValue: 'Foo' }));
|
||||
model = await inputWithContents.resolve();
|
||||
|
||||
assert.equal(inputWithContents.getName(), 'Foo');
|
||||
|
||||
inputWithContents.dispose();
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('model#onDidChangeDirty', async function () {
|
||||
const service = accessor.untitledTextEditorService;
|
||||
const input = instantiationService.createInstance(UntitledTextEditorInput, service.create());
|
||||
|
||||
let counter = 0;
|
||||
|
||||
const model = await input.resolve();
|
||||
model.onDidChangeDirty(() => counter++);
|
||||
|
||||
model.textEditorModel.setValue('foo');
|
||||
|
||||
assert.equal(counter, 1, 'Dirty model should trigger event');
|
||||
model.textEditorModel.setValue('bar');
|
||||
|
||||
assert.equal(counter, 1, 'Another change does not fire event');
|
||||
|
||||
input.dispose();
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('model#onDidChangeEncoding', async function () {
|
||||
const service = accessor.untitledTextEditorService;
|
||||
const input = instantiationService.createInstance(UntitledTextEditorInput, service.create());
|
||||
|
||||
let counter = 0;
|
||||
|
||||
const model = await input.resolve();
|
||||
model.onDidChangeEncoding(() => counter++);
|
||||
|
||||
model.setEncoding('utf16');
|
||||
|
||||
assert.equal(counter, 1, 'Dirty model should trigger event');
|
||||
model.setEncoding('utf16');
|
||||
|
||||
assert.equal(counter, 1, 'Another change to same encoding does not fire event');
|
||||
|
||||
input.dispose();
|
||||
model.dispose();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user