提交 1762d508 编写于 作者: B Benjamin Pasero

working copies - add name property

上级 eda792e0
......@@ -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<void>());
readonly onDidModelChangeEncoding = this._onDidModelChangeEncoding.event;
private cachedModel: UntitledTextEditorModel | null = null;
private cachedModelFirstWords: string | undefined = undefined;
private modelResolve: Promise<UntitledTextEditorModel & IResolvedTextEditorModel> | 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 {
......
......@@ -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<void>());
readonly onDidChangeContent = this._onDidChangeContent.event;
private readonly _onDidChangeFirstLine = this._register(new Emitter<void>());
readonly onDidChangeFirstLine = this._onDidChangeFirstLine.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;
......@@ -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<string>(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();
}
}
......
......@@ -165,7 +165,7 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont
private async confirmBeforeShutdown(workingCopies: IWorkingCopy[]): Promise<boolean> {
// 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) {
......
......@@ -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);
......
......@@ -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<IWorkingCopyBackup> {
// TODO@matt implement
return {};
}
}
......@@ -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<string, { readonly model: CustomEditorModel, readonly disposables: DisposableStore }>();
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 });
......
......@@ -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,
......
......@@ -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)));
}
});
}
......
......@@ -44,6 +44,8 @@ export interface IWorkingCopy {
readonly resource: URI;
readonly name: string;
readonly capabilities: WorkingCopyCapabilities;
......
......@@ -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) {
......
......@@ -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();
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册