Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

View File

@@ -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();
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);

View File

@@ -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();
});
});