Skip to content

Commit

Permalink
Add support for untitled file working copies (#124120)
Browse files Browse the repository at this point in the history
* untitled file working copy - first cut

* untitled file working copy - first cut manager

* untitled file working copy - tests

* untitled file working copy - 💄

* untitled file working copy - extract reusable interfaces

* untitled file working copy - extract common super type for manager

* untitled file working copy - add workingcopyservice#get

* untitled file working copy - wire in save support

* untitled file working copy - fix tests

* untitled file working copy - some code 💄

* untitled file working copy - set visibility

* untitled file working copy - poperly resolve target

* untitled file working copy - shared dispose handling

* untitled file working copy - add new manager that unifies file and untitled working copies

* untitled file working copy - tests for new unified manager

* untitled file working copy - test 💄
  • Loading branch information
bpasero authored May 19, 2021
2 parents a48180b + 13aff6a commit e878f5a
Show file tree
Hide file tree
Showing 21 changed files with 2,156 additions and 368 deletions.
23 changes: 19 additions & 4 deletions src/vs/platform/dialogs/test/common/testDialogService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,23 @@ export class TestDialogService implements IDialogService {

declare readonly _serviceBrand: undefined;

confirm(_confirmation: IConfirmation): Promise<IConfirmationResult> { return Promise.resolve({ confirmed: false }); }
show(_severity: Severity, _message: string, _buttons: string[], _options?: IDialogOptions): Promise<IShowResult> { return Promise.resolve({ choice: 0 }); }
input(): Promise<IInputResult> { { return Promise.resolve({ choice: 0, values: [] }); } }
about(): Promise<void> { return Promise.resolve(); }
private confirmResult: IConfirmationResult | undefined = undefined;
setConfirmResult(result: IConfirmationResult) {
this.confirmResult = result;
}

async confirm(confirmation: IConfirmation): Promise<IConfirmationResult> {
if (this.confirmResult) {
const confirmResult = this.confirmResult;
this.confirmResult = undefined;

return confirmResult;
}

return { confirmed: false };
}

async show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise<IShowResult> { return { choice: 0 }; }
async input(): Promise<IInputResult> { { return { choice: 0, values: [] }; } }
async about(): Promise<void> { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { canceled } from 'vs/base/common/errors';
import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IFileWorkingCopyManager, IFileWorkingCopySaveAsOptions } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager';
import { IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager';
import { filter } from 'vs/base/common/objects';

//#region --- complex content provider
Expand Down Expand Up @@ -491,8 +491,8 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE
return this;
}

async saveAs(target: URI, options?: IFileWorkingCopySaveAsOptions): Promise<IEditorInput | undefined> {
const newWorkingCopy = await this._workingCopyManager.saveAs(this.resource, target, options);
async saveAs(target: URI): Promise<IEditorInput | undefined> {
const newWorkingCopy = await this._workingCopyManager.saveAs(this.resource, target);
if (!newWorkingCopy) {
return undefined;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
// Otherwise try to suggest a path that can be saved
let suggestedFilename: string | undefined = undefined;
if (resource.scheme === Schemas.untitled) {
const model = this.untitledTextEditorService.get(resource);
const model = this.untitled.get(resource);
if (model) {

// Untitled with associated file path
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Event, Emitter } from 'vs/base/common/event';
import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService';
import { ITextModel } from 'vs/editor/common/model';
import { createTextBufferFactory, createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel';
import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel';
import { ITextEditorModel } from 'vs/editor/common/services/resolverService';
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { IWorkingCopy, WorkingCopyCapabilities, IWorkingCopyBackup, NO_TYPE_ID } from 'vs/workbench/services/workingCopy/common/workingCopy';
Expand Down Expand Up @@ -63,12 +63,6 @@ export interface IUntitledTextEditorModel extends ITextEditorModel, IModeSupport
* Resolves the untitled model.
*/
resolve(): Promise<void>;

/**
* Updates the value of the untitled model optionally allowing to ignore dirty.
* The model must be resolved for this method to work.
*/
setValue(value: string, ignoreDirty?: boolean): void;
}

export class UntitledTextEditorModel extends BaseTextEditorModel implements IUntitledTextEditorModel {
Expand Down Expand Up @@ -99,6 +93,10 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt

readonly capabilities = WorkingCopyCapabilities.Untitled;

//#region Name

private configuredLabelFormat: 'content' | 'name' = 'content';

private cachedModelFirstLineWords: string | undefined = undefined;
get name(): string {
// Take name from first line if present and only if
Expand All @@ -112,13 +110,8 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt
return this.labelService.getUriBasenameLabel(this.resource);
}

private dirty = this.hasAssociatedFilePath || !!this.initialValue;
private ignoreDirtyOnModelContentChange = false;

private versionId = 0;
//#endregion

private configuredEncoding: string | undefined;
private configuredLabelFormat: 'content' | 'name' = 'content';

constructor(
public readonly resource: URI,
Expand Down Expand Up @@ -153,7 +146,7 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt
private registerListeners(): void {

// Config Changes
this._register(this.textResourceConfigurationService.onDidChangeConfiguration(e => this.onConfigurationChange(true)));
this._register(this.textResourceConfigurationService.onDidChangeConfiguration(() => this.onConfigurationChange(true)));
}

private onConfigurationChange(fromEvent: boolean): void {
Expand All @@ -179,9 +172,8 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt
}
}

getVersionId(): number {
return this.versionId;
}

//#region Mode

private _hasModeSetExplicitly: boolean = false;
get hasModeSetExplicitly(): boolean { return this._hasModeSetExplicitly; }
Expand Down Expand Up @@ -216,6 +208,13 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt
return this.preferredMode;
}

//#endregion


//#region Encoding

private configuredEncoding: string | undefined;

getEncoding(): string | undefined {
return this.preferredEncoding || this.configuredEncoding;
}
Expand All @@ -230,25 +229,13 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt
}
}

setValue(value: string, ignoreDirty?: boolean): void {
if (ignoreDirty) {
this.ignoreDirtyOnModelContentChange = true;
}

try {
this.updateTextEditorModel(createTextBufferFactory(value));
} finally {
this.ignoreDirtyOnModelContentChange = false;
}
}

override isReadonly(): boolean {
return false;
}
//#endregion


//#region Dirty

private dirty = this.hasAssociatedFilePath || !!this.initialValue;

isDirty(): boolean {
return this.dirty;
}
Expand Down Expand Up @@ -360,19 +347,16 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt
}

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);
}
// 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);
}
// 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
Expand Down Expand Up @@ -421,4 +405,9 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt
}

//#endregion


override isReadonly(): boolean {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,22 +102,6 @@ suite('Untitled text editors', () => {
});
}

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'));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { IDisposable } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';
import { CancellationToken } from 'vs/base/common/cancellation';
import { VSBufferReadableStream } from 'vs/base/common/buffer';
import { URI } from 'vs/base/common/uri';
import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopy';

export interface IBaseFileWorkingCopyModelFactory<T extends IBaseFileWorkingCopyModel> {

/**
* Create a model from the given content under the provided resource.
*
* @param resource the `URI` of the model
* @param contents the content of the model to create it
* @param token support for cancellation
*/
createModel(resource: URI, contents: VSBufferReadableStream, token: CancellationToken): Promise<T>;
}

/**
* A generic file working copy model to be reused by untitled
* and existing file working copies.
*/
export interface IBaseFileWorkingCopyModel extends IDisposable {

/**
* This event signals ANY changes to the contents, for example:
* - through the user typing into the editor
* - from API usage (e.g. bulk edits)
* - when `IBaseFileWorkingCopyModel#update` is invoked with contents
* that are different from the current contents
*
* The file working copy will listen to these changes and may mark
* the working copy as dirty whenever this event fires.
*
* Note: ONLY report changes to the model but not the underlying
* file. The file working copy is tracking changes to the file
* automatically.
*/
readonly onDidChangeContent: Event<unknown>;

/**
* An event emitted right before disposing the model.
*/
readonly onWillDispose: Event<void>;

/**
* Snapshots the model's current content for writing. This must include
* any changes that were made to the model that are in memory.
*
* @param token support for cancellation
*/
snapshot(token: CancellationToken): Promise<VSBufferReadableStream>;

/**
* Updates the model with the provided contents. The implementation should
* behave in a similar fashion as `IBaseFileWorkingCopyModelFactory#createModel`
* except that here the model already exists and just needs to update to
* the provided contents.
*
* Note: it is expected that the model fires a `onDidChangeContent` event
* as part of the update.
*
* @param the contents to use for the model
* @param token support for cancellation
*/
update(contents: VSBufferReadableStream, token: CancellationToken): Promise<void>;
}

export interface IBaseFileWorkingCopy<T extends IBaseFileWorkingCopyModel> extends IWorkingCopy, IDisposable {

/**
* An event for when the file working copy has been reverted.
*/
readonly onDidRevert: Event<void>;

/**
* An event for when the file working copy has been disposed.
*/
readonly onWillDispose: Event<void>;

/**
* Provides access to the underlying model of this file
* based working copy. As long as the file working copy
* has not been resolved, the model is `undefined`.
*/
readonly model: T | undefined;

/**
* Resolves the file working copy and thus makes the `model`
* available.
*/
resolve(): Promise<void>;

/**
* Whether we have a resolved model or not.
*/
isResolved(): this is IBaseResolvedFileWorkingCopy<T>;
}

export interface IBaseResolvedFileWorkingCopy<T extends IBaseFileWorkingCopyModel> extends IBaseFileWorkingCopy<T> {

/**
* A resolved file working copy has a resolved model.
*/
readonly model: T;
}
Loading

0 comments on commit e878f5a

Please sign in to comment.