From 1762d50880e0acf1733bf81230f329d0867e606a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 23 Jan 2020 10:01:47 +0100 Subject: [PATCH] working copies - add name property --- .../common/editor/untitledTextEditorInput.ts | 40 +------------ .../common/editor/untitledTextEditorModel.ts | 56 +++++++++++++++++-- .../backup/electron-browser/backupTracker.ts | 3 +- .../customEditor/browser/customEditors.ts | 5 +- .../customEditor/common/customEditorModel.ts | 14 +++-- .../common/customEditorModelManager.ts | 4 +- .../search/browser/searchEditorInput.ts | 1 + .../textfile/common/textFileEditorModel.ts | 12 ++-- .../workingCopy/common/workingCopyService.ts | 2 + .../test/common/workingCopyService.test.ts | 3 + .../common/editor/untitledTextEditor.test.ts | 10 ++-- 11 files changed, 88 insertions(+), 62 deletions(-) diff --git a/src/vs/workbench/common/editor/untitledTextEditorInput.ts b/src/vs/workbench/common/editor/untitledTextEditorInput.ts index 874f2fea8e3..1ef371be66f 100644 --- a/src/vs/workbench/common/editor/untitledTextEditorInput.ts +++ b/src/vs/workbench/common/editor/untitledTextEditorInput.ts @@ -18,7 +18,6 @@ import { IResolvedTextEditorModel } from 'vs/editor/common/services/resolverServ import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { ensureValidWordDefinition } from 'vs/editor/common/model/wordHelper'; /** * An editor input to be used for untitled text buffers. @@ -29,13 +28,10 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin private static readonly MEMOIZER = createMemoizer(); - private static readonly FIRST_LINE_MAX_TITLE_LENGTH = 50; - private readonly _onDidModelChangeEncoding = this._register(new Emitter()); readonly onDidModelChangeEncoding = this._onDidModelChangeEncoding.event; private cachedModel: UntitledTextEditorModel | null = null; - private cachedModelFirstWords: string | undefined = undefined; private modelResolve: Promise | null = null; @@ -76,15 +72,10 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin } getName(): string { - - // Take name from first words if present and only if - // we have no associated file path. In that case we - // prefer the file name as title. - if (!this._hasAssociatedFilePath && this.cachedModelFirstWords) { - return this.cachedModelFirstWords; + if (this.cachedModel) { + return this.cachedModel.name; } - // Otherwise fallback to resource return this.hasAssociatedFilePath ? basenameOrAuthority(this.resource) : this.resource.path; } @@ -294,32 +285,7 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin // re-emit some events from the model this._register(model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); this._register(model.onDidChangeEncoding(() => this._onDidModelChangeEncoding.fire())); - - // listen for first line change events if we use it for the label - // by checking the contents of the first line has changed - if (!this._hasAssociatedFilePath) { - this._register(model.onDidChangeFirstLine(() => this.onDidChangeFirstLine(model))); - } - } - - private onDidChangeFirstLine(model: UntitledTextEditorModel): void { - - // 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 - - let modelFirstWordsCandidate: string | undefined = undefined; - - const firstLineText = model.textEditorModel?.getValueInRange({ startLineNumber: 1, endLineNumber: 1, startColumn: 1, endColumn: UntitledTextEditorInput.FIRST_LINE_MAX_TITLE_LENGTH }).trim(); - if (firstLineText && ensureValidWordDefinition().exec(firstLineText)) { - modelFirstWordsCandidate = firstLineText; - } - - if (modelFirstWordsCandidate !== this.cachedModelFirstWords) { - this.cachedModelFirstWords = modelFirstWordsCandidate; - this._onDidChangeLabel.fire(); - } + this._register(model.onDidChangeName(() => this._onDidChangeLabel.fire())); } matches(otherInput: unknown): boolean { diff --git a/src/vs/workbench/common/editor/untitledTextEditorModel.ts b/src/vs/workbench/common/editor/untitledTextEditorModel.ts index 8029f739fc9..b7773dd818c 100644 --- a/src/vs/workbench/common/editor/untitledTextEditorModel.ts +++ b/src/vs/workbench/common/editor/untitledTextEditorModel.ts @@ -18,16 +18,20 @@ import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities, IWorkingCop import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { basenameOrAuthority } from 'vs/base/common/resources'; +import { ensureValidWordDefinition } from 'vs/editor/common/model/wordHelper'; export interface IUntitledTextEditorModel extends ITextEditorModel, IModeSupport, IEncodingSupport, IWorkingCopy { } export class UntitledTextEditorModel extends BaseTextEditorModel implements IUntitledTextEditorModel { + private static readonly FIRST_LINE_NAME_MAX_LENGTH = 50; + private readonly _onDidChangeContent = this._register(new Emitter()); readonly onDidChangeContent = this._onDidChangeContent.event; - private readonly _onDidChangeFirstLine = this._register(new Emitter()); - readonly onDidChangeFirstLine = this._onDidChangeFirstLine.event; + private readonly _onDidChangeName = this._register(new Emitter()); + readonly onDidChangeName = this._onDidChangeName.event; private readonly _onDidChangeDirty = this._register(new Emitter()); readonly onDidChangeDirty = this._onDidChangeDirty.event; @@ -37,6 +41,19 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt 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.hasAssociatedFilePath && this.cachedModelFirstLineWords) { + return this.cachedModelFirstLineWords; + } + + // Otherwise fallback to resource + return this.hasAssociatedFilePath ? basenameOrAuthority(this.resource) : this.resource.path; + } + private dirty = false; private versionId = 0; private configuredEncoding: string | undefined; @@ -158,6 +175,11 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt this.updateTextEditorModel(untitledContents, this.preferredMode); } + // Name + if (backup || this.initialValue) { + this.updateNameFromFirstLine(); + } + // Encoding this.configuredEncoding = this.configurationService.getValue(this.resource, 'files.encoding'); @@ -174,7 +196,6 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt // as the appropiate events to the outside. if (backup || this.initialValue) { this._onDidChangeContent.fire(); - this._onDidChangeFirstLine.fire(); } return this as UntitledTextEditorModel & IResolvedTextEditorModel; @@ -198,12 +219,35 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt this.setDirty(true); } + // Check for name change if first line changed + if (e.changes.some(change => change.range.startLineNumber === 1 || change.range.endLineNumber === 1)) { + this.updateNameFromFirstLine(); + } + // Emit as general content change event this._onDidChangeContent.fire(); + } - // Emit as first line change event depending on actual change - if (e.changes.some(change => change.range.startLineNumber === 1 || change.range.endLineNumber === 1)) { - this._onDidChangeFirstLine.fire(); + private updateNameFromFirstLine(): 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 + + let modelFirstWordsCandidate: string | undefined = undefined; + + const firstLineText = this.textEditorModel?.getValueInRange({ startLineNumber: 1, endLineNumber: 1, startColumn: 1, endColumn: UntitledTextEditorModel.FIRST_LINE_NAME_MAX_LENGTH }).trim(); + if (firstLineText && ensureValidWordDefinition().exec(firstLineText)) { + modelFirstWordsCandidate = firstLineText; + } + + if (modelFirstWordsCandidate !== this.cachedModelFirstLineWords) { + this.cachedModelFirstLineWords = modelFirstWordsCandidate; + this._onDidChangeName.fire(); } } diff --git a/src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts b/src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts index a792723f895..87e049d9704 100644 --- a/src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts +++ b/src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts @@ -165,7 +165,7 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont private async confirmBeforeShutdown(workingCopies: IWorkingCopy[]): Promise { // Show confirm dialog for all dirty working copies - const confirm = await this.fileDialogService.showSaveConfirm(workingCopies.map(workingCopy => workingCopy.resource)); + const confirm = await this.fileDialogService.showSaveConfirm(workingCopies.map(workingCopy => workingCopy.name)); // Save if (confirm === ConfirmResult.SAVE) { @@ -214,7 +214,6 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont result = await this.editorService.saveAll({ includeUntitled: typeof arg1 === 'boolean' ? arg1 : true, ...saveOptions }); } - // If we still have dirty working copies, save those directly // unless the save was not successful (e.g. cancelled) if (result !== false) { diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index 2afd16f5f63..2049d25b6ba 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -31,6 +31,8 @@ import { IEditorService, IOpenEditorOverride } from 'vs/workbench/services/edito import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { CustomFileEditorInput } from './customEditorInput'; import { Emitter } from 'vs/base/common/event'; +import { ILabelService } from 'vs/platform/label/common/label'; + export const defaultEditorId = 'default'; const defaultEditorInfo = new CustomEditorInfo({ @@ -110,10 +112,11 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ @IInstantiationService private readonly instantiationService: IInstantiationService, @IQuickInputService private readonly quickInputService: IQuickInputService, @IWebviewService private readonly webviewService: IWebviewService, + @ILabelService labelService: ILabelService ) { super(); - this._models = new CustomEditorModelManager(workingCopyService); + this._models = new CustomEditorModelManager(workingCopyService, labelService); this._hasCustomEditor = CONTEXT_HAS_CUSTOM_EDITORS.bindTo(contextKeyService); this._focusedCustomEditorIsEditable = CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE.bindTo(contextKeyService); diff --git a/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts b/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts index 66e29755f62..ff103cb3fdc 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts @@ -7,8 +7,10 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ICustomEditorModel, CustomEditorEdit, CustomEditorSaveAsEvent, CustomEditorSaveEvent } from 'vs/workbench/contrib/customEditor/common/customEditor'; -import { WorkingCopyCapabilities, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { basename } from 'vs/base/common/path'; export class CustomEditorModel extends Disposable implements ICustomEditorModel { @@ -19,6 +21,7 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel constructor( public readonly viewType: string, private readonly _resource: URI, + private readonly labelService: ILabelService ) { super(); } @@ -34,6 +37,10 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel return this._resource; } + public get name() { + return basename(this.labelService.getUriLabel(this._resource)); + } + public get capabilities(): WorkingCopyCapabilities { return 0; } @@ -189,9 +196,4 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel this.updateDirty(); this.updateContentChanged(); } - - public async backup(): Promise { - // TODO@matt implement - return {}; - } } diff --git a/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts b/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts index 15331ad5497..e6cbfb4be4d 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts @@ -8,12 +8,14 @@ import { URI } from 'vs/base/common/uri'; import { ICustomEditorModel, ICustomEditorModelManager } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { CustomEditorModel } from 'vs/workbench/contrib/customEditor/common/customEditorModel'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { ILabelService } from 'vs/platform/label/common/label'; export class CustomEditorModelManager implements ICustomEditorModelManager { private readonly _models = new Map(); constructor( @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, + @ILabelService private readonly _labelService: ILabelService ) { } @@ -27,7 +29,7 @@ export class CustomEditorModelManager implements ICustomEditorModelManager { return existing; } - const model = new CustomEditorModel(viewType, resource); + const model = new CustomEditorModel(viewType, resource, this._labelService); const disposables = new DisposableStore(); disposables.add(this._workingCopyService.registerWorkingCopy(model)); this._models.set(this.key(resource, viewType), { model, disposables }); diff --git a/src/vs/workbench/contrib/search/browser/searchEditorInput.ts b/src/vs/workbench/contrib/search/browser/searchEditorInput.ts index 59fab059caf..df93929877b 100644 --- a/src/vs/workbench/contrib/search/browser/searchEditorInput.ts +++ b/src/vs/workbench/contrib/search/browser/searchEditorInput.ts @@ -66,6 +66,7 @@ export class SearchEditorInput extends EditorInput { const workingCopyAdapter: IWorkingCopy = { resource: this.resource, + name: basename(this.resource.path), capabilities: this.resource.scheme === 'search-editor' ? WorkingCopyCapabilities.Untitled : 0, onDidChangeDirty: this.onDidChangeDirty, onDidChangeContent: this.onDidChangeDirty, diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index fd3613f8c5c..d0f22c9fb60 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -19,11 +19,12 @@ import { timeout } from 'vs/base/common/async'; import { ITextBufferFactory } from 'vs/editor/common/model'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ILogService } from 'vs/platform/log/common/log'; -import { basename } from 'vs/base/common/resources'; +import { basename } from 'vs/base/common/path'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IWorkingCopyService, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { SaveSequentializer } from 'vs/workbench/services/textfile/common/saveSequenzializer'; +import { ILabelService } from 'vs/platform/label/common/label'; interface IBackupMetaData { mtime: number; @@ -74,6 +75,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil readonly capabilities = 0; + readonly name = basename(this.labelService.getUriLabel(this.resource)); + private contentEncoding: string | undefined; // encoding as reported from disk private versionId = 0; @@ -103,7 +106,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil @IBackupFileService private readonly backupFileService: IBackupFileService, @ILogService private readonly logService: ILogService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, - @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, + @ILabelService private readonly labelService: ILabelService ) { super(modelService, modeService); @@ -286,7 +290,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Load with backup this.loadFromContent({ resource: this.resource, - name: basename(this.resource), + name: this.name, mtime: backup.meta ? backup.meta.mtime : Date.now(), ctime: backup.meta ? backup.meta.ctime : Date.now(), size: backup.meta ? backup.meta.size : 0, @@ -784,7 +788,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil const notificationService = this.notificationService; TextFileEditorModel.setSaveErrorHandler({ onSaveError(error: Error, model: TextFileEditorModel): void { - notificationService.error(nls.localize('genericSaveError', "Failed to save '{0}': {1}", basename(model.resource), toErrorMessage(error, false))); + notificationService.error(nls.localize('genericSaveError', "Failed to save '{0}': {1}", model.name, toErrorMessage(error, false))); } }); } diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts index 0edfe63168d..7be4f335d23 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts @@ -44,6 +44,8 @@ export interface IWorkingCopy { readonly resource: URI; + readonly name: string; + readonly capabilities: WorkingCopyCapabilities; diff --git a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts index 38eabd06a6d..d20c91df91d 100644 --- a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts @@ -10,6 +10,7 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { TestWorkingCopyService } from 'vs/workbench/test/workbenchTestServices'; import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; +import { basename } from 'vs/base/common/resources'; suite('WorkingCopyService', () => { @@ -26,6 +27,8 @@ suite('WorkingCopyService', () => { readonly capabilities = 0; + readonly name = basename(this.resource); + private dirty = false; constructor(public readonly resource: URI, isDirty = false) { diff --git a/src/vs/workbench/test/common/editor/untitledTextEditor.test.ts b/src/vs/workbench/test/common/editor/untitledTextEditor.test.ts index 7332d97af5a..c5b76cba3b2 100644 --- a/src/vs/workbench/test/common/editor/untitledTextEditor.test.ts +++ b/src/vs/workbench/test/common/editor/untitledTextEditor.test.ts @@ -367,14 +367,14 @@ suite('Workbench untitled text editors', () => { model.dispose(); }); - test('model#onDidChangeFirstLine and input name', async function () { + test('model#onDidChangeName and input name', async function () { const service = accessor.untitledTextEditorService; const input = service.create(); let counter = 0; let model = await input.resolve(); - model.onDidChangeFirstLine(() => counter++); + model.onDidChangeName(() => counter++); model.textEditorModel.setValue('foo'); assert.equal(input.getName(), 'foo'); @@ -396,10 +396,10 @@ suite('Workbench untitled text editors', () => { model.textEditorModel.setValue('([]}hello '); // require actual words assert.equal(input.getName(), '([]}hello'); - assert.equal(counter, 6); + assert.equal(counter, 4); model.textEditorModel.setValue('Hello\nWorld'); - assert.equal(counter, 7); + assert.equal(counter, 5); function createSingleEditOp(text: string, positionLineNumber: number, positionColumn: number, selectionLineNumber: number = positionLineNumber, selectionColumn: number = positionColumn): IIdentifiedSingleEditOperation { let range = new Range( @@ -418,7 +418,7 @@ suite('Workbench untitled text editors', () => { } model.textEditorModel.applyEdits([createSingleEditOp('hello', 2, 2)]); - assert.equal(counter, 7); // change was not on first line + assert.equal(counter, 5); // change was not on first line input.dispose(); model.dispose(); -- GitLab