From 379e732c79e6cb0241f95f0e5e45b1dc2255e7ad Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Wed, 8 Apr 2020 23:09:10 -0700 Subject: [PATCH] Refactor to only use untitled search editors and transparently convert to disk format on save/load. Ref #89268. --- .../browser/searchEditor.contribution.ts | 57 ++--- .../searchEditor/browser/searchEditor.ts | 39 ++-- .../browser/searchEditorActions.ts | 6 +- .../searchEditor/browser/searchEditorInput.ts | 210 ++++++++---------- .../browser/searchEditorSerialization.ts | 57 +++-- 5 files changed, 184 insertions(+), 185 deletions(-) diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index db326f5ed3e..d8483a7d5ec 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts @@ -7,9 +7,11 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import * as objects from 'vs/base/common/objects'; import { endsWith } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { ToggleCaseSensitiveKeybinding, ToggleRegexKeybinding, ToggleWholeWordKeybinding } from 'vs/editor/contrib/find/findModel'; import { localize } from 'vs/nls'; -import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { MenuId, MenuRegistry, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -20,16 +22,15 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { Extensions as EditorInputExtensions, IEditorInputFactory, IEditorInputFactoryRegistry, ActiveEditorContext } from 'vs/workbench/common/editor'; +import { ActiveEditorContext, Extensions as EditorInputExtensions, IEditorInputFactory, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; import * as SearchConstants from 'vs/workbench/contrib/search/common/constants'; import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants'; import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; -import { OpenResultsInEditorAction, OpenSearchEditorAction, toggleSearchEditorCaseSensitiveCommand, toggleSearchEditorContextLinesCommand, toggleSearchEditorRegexCommand, toggleSearchEditorWholeWordCommand, selectAllSearchEditorMatchesCommand, RerunSearchEditorSearchAction, OpenSearchEditorToSideAction, modifySearchEditorContextLinesCommand } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions'; -import { getOrMakeSearchEditorInput, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; +import { modifySearchEditorContextLinesCommand, OpenResultsInEditorAction, OpenSearchEditorAction, OpenSearchEditorToSideAction, RerunSearchEditorSearchAction, selectAllSearchEditorMatchesCommand, toggleSearchEditorCaseSensitiveCommand, toggleSearchEditorContextLinesCommand, toggleSearchEditorRegexCommand, toggleSearchEditorWholeWordCommand } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions'; +import { getOrMakeSearchEditorInput, SearchEditorInput, SearchConfiguration } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { parseSavedSearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; +import { Range } from 'vs/editor/common/core/range'; //#region Editor Descriptior Registry.as(EditorExtensions.Editors).registerEditor( @@ -54,15 +55,10 @@ class SearchEditorContribution implements IWorkbenchContribution { ) { this.editorService.overrideOpenEditor((editor, options, group) => { - let resource = editor.resource; + const resource = editor.resource; if (!resource) { return undefined; } - if (resource.scheme === SearchEditorConstants.SearchEditorBodyScheme) { - resource = resource.with({ scheme: SearchEditorConstants.SearchEditorScheme }); - } - - if (resource.scheme !== SearchEditorConstants.SearchEditorScheme - && !(endsWith(resource.path, '.code-search') && editor instanceof FileEditorInput)) { + if (!endsWith(resource.path, '.code-search')) { return undefined; } @@ -70,13 +66,15 @@ class SearchEditorContribution implements IWorkbenchContribution { return undefined; } - if (endsWith(resource.path, '.code-search')) { - this.telemetryService.publicLog2('searchEditor/openSavedSearchEditor'); - } + this.telemetryService.publicLog2('searchEditor/openSavedSearchEditor'); - const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { uri: resource }); - const opened = editorService.openEditor(input, { ...options, pinned: resource.scheme === SearchEditorConstants.SearchEditorScheme, ignoreOverrides: true }, group); - return { override: Promise.resolve(opened) }; + return { + override: (async () => { + const { config } = await instantiationService.invokeFunction(parseSavedSearchEditor, resource); + const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { backingUri: resource, config }); + return editorService.openEditor(input, { ...options, ignoreOverrides: true }, group); + })() + }; }); } } @@ -86,27 +84,30 @@ workbenchContributionsRegistry.registerWorkbenchContribution(SearchEditorContrib //#endregion //#region Input Factory +type SerializedSearchEditor = { modelUri: string, dirty: boolean, config: SearchConfiguration, name: string, matchRanges: Range[], backingUri: string }; class SearchEditorInputFactory implements IEditorInputFactory { canSerialize() { return true; } serialize(input: SearchEditorInput) { - let resource = undefined; - if (input.resource.path || input.resource.fragment) { - resource = input.resource.toString(); + let modelUri = undefined; + if (input.modelUri.path || input.modelUri.fragment) { + modelUri = input.modelUri.toString(); } + if (!modelUri) { return undefined; } - const config = input.getConfigSync(); + const config = input.config; const dirty = input.isDirty(); const matchRanges = input.getMatchRanges(); + const backingUri = input.backingUri; - return JSON.stringify({ resource, dirty, config, name: input.getName(), matchRanges }); + return JSON.stringify({ modelUri: modelUri.toString(), dirty, config, name: input.getName(), matchRanges, backingUri: backingUri?.toString() } as SerializedSearchEditor); } deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): SearchEditorInput | undefined { - const { resource, dirty, config, matchRanges } = JSON.parse(serializedEditorInput); - if (config && (config.query !== undefined)) { - const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { config, uri: URI.parse(resource) }); + const { modelUri, dirty, config, matchRanges, backingUri } = JSON.parse(serializedEditorInput) as SerializedSearchEditor; + if (config && (config.query !== undefined) && (modelUri !== undefined)) { + const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { config, modelUri: URI.parse(modelUri), backingUri: backingUri ? URI.parse(backingUri) : undefined }); input.setDirty(dirty); input.setMatchRanges(matchRanges); return input; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 5af24dbdcfb..a258f9d9b39 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -5,21 +5,29 @@ import * as DOM from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { alert } from 'vs/base/browser/ui/aria/aria'; import { Delayer } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { assertIsDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/searchEditor'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; +import { ICodeEditorViewState } from 'vs/editor/common/editorCommon'; import { IModelService } from 'vs/editor/common/services/modelService'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; import { ReferencesController } from 'vs/editor/contrib/gotoSymbol/peek/referencesController'; import { localize } from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ILabelService } from 'vs/platform/label/common/label'; import { IEditorProgressService, LongRunningOperation } from 'vs/platform/progress/common/progress'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -28,6 +36,7 @@ import { inputBorder, registerColor, searchEditorFindMatch, searchEditorFindMatc import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; import { EditorOptions } from 'vs/workbench/common/editor'; import { ExcludePatternInputWidget, PatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget'; import { SearchWidget } from 'vs/workbench/contrib/search/browser/searchWidget'; @@ -37,19 +46,10 @@ import { getOutOfWorkspaceEditorResources } from 'vs/workbench/contrib/search/co import { SearchModel } from 'vs/workbench/contrib/search/common/searchModel'; import { InSearchEditor, SearchEditorFindMatchClass, SearchEditorID } from 'vs/workbench/contrib/searchEditor/browser/constants'; import type { SearchConfiguration, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; -import { extractSearchQuery, serializeSearchConfiguration, serializeSearchResultForEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; -import { IPatternInfo, ISearchConfigurationProperties, ITextQuery } from 'vs/workbench/services/search/common/search'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { ICodeEditorViewState } from 'vs/editor/common/editorCommon'; +import { serializeSearchResultForEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; -import { assertIsDefined } from 'vs/base/common/types'; -import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { Position } from 'vs/editor/common/core/position'; -import { Selection } from 'vs/editor/common/core/selection'; -import { alert } from 'vs/base/browser/ui/aria/aria'; +import { IPatternInfo, ISearchConfigurationProperties, ITextQuery } from 'vs/workbench/services/search/common/search'; const RESULT_LINE_REGEX = /^(\s+)(\d+)(:| )(\s+)(.*)$/; const FILE_LINE_REGEX = /^(\S.*):$/; @@ -498,12 +498,12 @@ export class SearchEditor extends BaseTextEditor { const controller = ReferencesController.get(this.searchResultEditor); controller.closeWidget(false); const labelFormatter = (uri: URI): string => this.labelService.getUriLabel(uri, { relative: true }); - const results = serializeSearchResultForEditor(this.searchModel.searchResult, config.includes, config.excludes, config.contextLines, labelFormatter, false); - const { header, body } = await input.getModels(); + const results = serializeSearchResultForEditor(this.searchModel.searchResult, config.includes, config.excludes, config.contextLines, labelFormatter); + const { body } = await input.getModels(); this.modelService.updateModel(body, results.text); - header.setValue(serializeSearchConfiguration(config)); + input.config = config; - input.setDirty(input.resource.scheme !== 'search-editor'); + input.setDirty(!input.isUntitled()); input.setMatchRanges(results.matchRanges); } @@ -538,12 +538,11 @@ export class SearchEditor extends BaseTextEditor { await super.setInput(newInput, options, token); - const { body, header } = await newInput.getModels(); + const { body, config } = await newInput.getModels(); this.searchResultEditor.setModel(body); this.pauseSearching = true; - const config = extractSearchQuery(header); this.toggleRunAgainMessage(body.getLineCount() === 1 && body.getValue() === '' && config.query !== ''); this.queryEditorWidget.setValue(config.query); @@ -588,7 +587,7 @@ export class SearchEditor extends BaseTextEditor { } private saveViewState() { - const resource = this.getInput()?.resource; + const resource = this.getInput()?.modelUri; if (resource) { this.saveTextEditorViewState(resource); } } @@ -596,13 +595,13 @@ export class SearchEditor extends BaseTextEditor { const control = this.getControl(); const editorViewState = control.saveViewState(); if (!editorViewState) { return null; } - if (resource.toString() !== this.getInput()?.resource.toString()) { return null; } + if (resource.toString() !== this.getInput()?.modelUri.toString()) { return null; } return { ...editorViewState, focused: this.searchResultEditor.hasWidgetFocus() ? 'editor' : 'input' }; } private loadViewState() { - const resource = assertIsDefined(this.input?.resource); + const resource = assertIsDefined(this.getInput()?.modelUri); return this.loadTextEditorViewState(resource) as SearchEditorViewState; } diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts index 29d36746319..2932ccfe49a 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts @@ -189,7 +189,7 @@ const openNewSearchEditor = telemetryService.publicLog2('searchEditor/openNewSearchEditor'); - const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { config: { query: selected } }); + const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { config: { query: selected }, text: '' }); const editor = await editorService.openEditor(input, { pinned: true }, toSide ? SIDE_GROUP : ACTIVE_GROUP) as SearchEditor; if (selected && configurationService.getValue('search').searchOnType) { @@ -214,9 +214,9 @@ export const createEditorFromSearchResult = const labelFormatter = (uri: URI): string => labelService.getUriLabel(uri, { relative: true }); - const { text, matchRanges } = serializeSearchResultForEditor(searchResult, rawIncludePattern, rawExcludePattern, 0, labelFormatter, true); + const { text, matchRanges, config } = serializeSearchResultForEditor(searchResult, rawIncludePattern, rawExcludePattern, 0, labelFormatter); - const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { text }); + const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { text, config }); await editorService.openEditor(input, { pinned: true }); input.setMatchRanges(matchRanges); }; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts index b5b76186eee..df18c61d5e9 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts @@ -6,7 +6,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import * as network from 'vs/base/common/network'; import { basename } from 'vs/base/common/path'; -import { isEqual, joinPath, extname } from 'vs/base/common/resources'; +import { extname, isEqual, joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/searchEditor'; import { Range } from 'vs/editor/common/core/range'; @@ -17,9 +17,10 @@ import { localize } from 'vs/nls'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { EditorInput, GroupIdentifier, IEditorInput, IRevertOptions, ISaveOptions, IMoveResult } from 'vs/workbench/common/editor'; -import { SearchEditorFindMatchClass, SearchEditorScheme, SearchEditorBodyScheme } from 'vs/workbench/contrib/searchEditor/browser/constants'; -import { extractSearchQuery, serializeSearchConfiguration } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; +import { EditorInput, GroupIdentifier, IEditorInput, IMoveResult, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; +import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { SearchEditorFindMatchClass, SearchEditorScheme } from 'vs/workbench/contrib/searchEditor/browser/constants'; +import { extractSearchQueryFromModel, parseSavedSearchEditor, serializeSearchConfiguration } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -48,19 +49,27 @@ export class SearchEditorInput extends EditorInput { static readonly ID: string = 'workbench.editorinputs.searchEditorInput'; private dirty: boolean = false; - private readonly contentsModel: Promise; - private readonly headerModel: Promise; - private _cachedContentsModel: ITextModel | undefined; - private _cachedConfig?: SearchConfiguration; + private model: Promise; + private _cachedModel: ITextModel | undefined; private readonly _onDidChangeContent = this._register(new Emitter()); readonly onDidChangeContent: Event = this._onDidChangeContent.event; private oldDecorationsIDs: string[] = []; + private _config: Readonly; + public get config(): Readonly { return this._config; } + public set config(value: Readonly) { this._config = value; this._onDidChangeLabel.fire(); } + + get resource() { + return this.backingUri || this.modelUri; + } + constructor( - public readonly resource: URI, - getModel: () => Promise<{ contentsModel: ITextModel, headerModel: ITextModel }>, + public readonly modelUri: URI, + public readonly backingUri: URI | undefined, + config: Readonly, + getModel: () => Promise, @IModelService private readonly modelService: IModelService, @IEditorService protected readonly editorService: IEditorService, @IEditorGroupsService protected readonly editorGroupService: IEditorGroupsService, @@ -76,34 +85,22 @@ export class SearchEditorInput extends EditorInput { ) { super(); - // Dummy model to set file icon - this._register(modelService.createModel('', modeService.create('search-result'), this.resource)); - - const modelLoader = getModel() - .then(({ contentsModel, headerModel }) => { - this._register(contentsModel.onDidChangeContent(() => this._onDidChangeContent.fire())); - this._register(headerModel.onDidChangeContent(() => { - this._cachedConfig = extractSearchQuery(headerModel); - this._onDidChangeContent.fire(); - this._onDidChangeLabel.fire(); - })); - - this._cachedConfig = extractSearchQuery(headerModel); - this._cachedContentsModel = contentsModel; - - this._register(contentsModel); - this._register(headerModel); - this._onDidChangeLabel.fire(); - - return { contentsModel, headerModel }; + this._config = config; + this.model = getModel() + .then(model => { + this._register(model.onDidChangeContent(() => this._onDidChangeContent.fire())); + this._register(model); + this._cachedModel = model; + return model; }); - this.contentsModel = modelLoader.then(({ contentsModel }) => contentsModel); - this.headerModel = modelLoader.then(({ headerModel }) => headerModel); + if (this.modelUri.scheme !== SearchEditorScheme) { + throw Error('SearchEditorInput must be invoked with a SearchEditorScheme uri'); + } const input = this; const workingCopyAdapter = new class implements IWorkingCopy { - readonly resource = input.resource; + readonly resource = input.modelUri; get name() { return input.getName(); } readonly capabilities = input.isUntitled() ? WorkingCopyCapabilities.Untitled : 0; readonly onDidChangeDirty = input.onDidChangeDirty; @@ -118,35 +115,34 @@ export class SearchEditorInput extends EditorInput { } async save(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { - if ((await this.headerModel).isDisposed() || (await this.contentsModel).isDisposed()) { return; } + if ((await this.model).isDisposed()) { return; } - if (this.isUntitled()) { - return this.saveAs(group, options); - } else { - await this.textFileService.write(this.resource, await this.serializeForDisk(), options); + if (this.backingUri) { + await this.textFileService.write(this.backingUri, await this.serializeForDisk(), options); this.setDirty(false); return this; + } else { + return this.saveAs(group, options); } } private async serializeForDisk() { - return (await this.headerModel).getValue() + '\n' + (await this.contentsModel).getValue(); + return serializeSearchConfiguration(this.config) + '\n' + (await this.model).getValue(); } async getModels() { - const header = await this.headerModel; - const body = await this.contentsModel; - return { header, body }; + return { config: this.config, body: await this.model }; } async saveAs(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { const path = await this.fileDialogService.pickFileToSave(await this.suggestFileName(), options?.availableFileSystems); if (path) { this.telemetryService.publicLog2('searchEditor/saveSearchResults'); - if (await this.textFileService.create(path, await this.serializeForDisk())) { + const toWrite = await this.serializeForDisk(); + if (await this.textFileService.create(path, toWrite, { overwrite: true })) { this.setDirty(false); - if (!isEqual(path, this.resource)) { - const input = this.instantiationService.invokeFunction(getOrMakeSearchEditorInput, { uri: path }); + if (!isEqual(path, this.modelUri)) { + const input = this.instantiationService.invokeFunction(getOrMakeSearchEditorInput, { config: this.config, backingUri: path }); input.setMatchRanges(this.getMatchRanges()); return input; } @@ -163,19 +159,15 @@ export class SearchEditorInput extends EditorInput { getName(maxLength = 12): string { const trimToMax = (label: string) => (label.length < maxLength ? label : `${label.slice(0, maxLength - 3)}...`); - if (this.isUntitled()) { - const query = this._cachedConfig?.query?.trim(); - if (query) { - return localize('searchTitle.withQuery', "Search: {0}", trimToMax(query)); - } - return localize('searchTitle', "Search"); + if (this.backingUri) { + return localize('searchTitle.withQuery', "Search: {0}", basename(this.backingUri?.path, SEARCH_EDITOR_EXT)); } - return localize('searchTitle.withQuery', "Search: {0}", basename(this.resource.path, SEARCH_EDITOR_EXT)); - } - - getConfigSync() { - return this._cachedConfig; + const query = this.config.query?.trim(); + if (query) { + return localize('searchTitle.withQuery', "Search: {0}", trimToMax(query)); + } + return localize('searchTitle', "Search"); } async resolve() { @@ -212,22 +204,21 @@ export class SearchEditorInput extends EditorInput { } isUntitled() { - return this.resource.scheme === SearchEditorScheme; + return !this.backingUri; } move(group: GroupIdentifier, target: URI): IMoveResult | undefined { - if (extname(target) === SEARCH_EDITOR_EXT) { + if (this._cachedModel && extname(target) === SEARCH_EDITOR_EXT) { return { - editor: this.instantiationService.invokeFunction(getOrMakeSearchEditorInput, { uri: target }) + editor: this.instantiationService.invokeFunction(getOrMakeSearchEditorInput, { config: this.config, text: this._cachedModel.getValue(), backingUri: target }) }; } - // Ignore move if editor was renamed to a different file extension return undefined; } dispose() { - this.modelService.destroyModel(this.resource); + this.modelService.destroyModel(this.modelUri); super.dispose(); } @@ -235,43 +226,49 @@ export class SearchEditorInput extends EditorInput { if (this === other) { return true; } if (other instanceof SearchEditorInput) { - if ( - (other.resource.path && other.resource.path === this.resource.path) || - (other.resource.fragment && other.resource.fragment === this.resource.fragment) - ) { - return true; - } + return !!(other.modelUri.fragment && other.modelUri.fragment === this.modelUri.fragment); + } else if (other instanceof FileEditorInput) { + return other.resource?.toString() === this.backingUri?.toString(); } return false; } - public getMatchRanges(): Range[] { - return (this._cachedContentsModel?.getAllDecorations() ?? []) + getMatchRanges(): Range[] { + return (this._cachedModel?.getAllDecorations() ?? []) .filter(decoration => decoration.options.className === SearchEditorFindMatchClass) .filter(({ range }) => !(range.startColumn === 1 && range.endColumn === 1)) .map(({ range }) => range); } - public async setMatchRanges(ranges: Range[]) { - this.oldDecorationsIDs = (await this.contentsModel).deltaDecorations(this.oldDecorationsIDs, ranges.map(range => + async setMatchRanges(ranges: Range[]) { + this.oldDecorationsIDs = (await this.model).deltaDecorations(this.oldDecorationsIDs, ranges.map(range => ({ range, options: { className: SearchEditorFindMatchClass, stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } }))); } async revert(group: GroupIdentifier, options?: IRevertOptions) { // TODO: this should actually revert the contents. But it needs to set dirty false. + if (this.backingUri) { + const { config, text } = await this.instantiationService.invokeFunction(parseSavedSearchEditor, this.backingUri); + (await this.model).setValue(text); + this.config = config; + } else { + (await this.model).setValue(''); + } super.revert(group, options); this.setDirty(false); } + supportsSplitEditor() { + return false; + } + private async backup(): Promise { - const content = stringToSnapshot(await this.serializeForDisk()); + const content = stringToSnapshot((await this.model).getValue()); return { content }; } - // Bringing this over from textFileService because it only suggests for untitled scheme. - // In the future I may just use the untitled scheme. I dont get particular benefit from using search-editor... private async suggestFileName(): Promise { - const query = extractSearchQuery(await this.headerModel).query; + const query = extractSearchQueryFromModel(await this.model).query; const searchFileName = (query.replace(/[^\w \-_]+/g, '_') || 'Search') + SEARCH_EDITOR_EXT; @@ -285,21 +282,23 @@ export class SearchEditorInput extends EditorInput { const inputs = new Map(); export const getOrMakeSearchEditorInput = ( accessor: ServicesAccessor, - existingData: - { uri: URI, config?: Partial, text?: never } | - { text: string, uri?: never, config?: never } | - { config: Partial, text?: never, uri?: never } + existingData: ({ config: Partial, backingUri?: URI } & + ({ modelUri: URI, text?: never, } | + { text: string, modelUri?: never, } | + { backingUri: URI, text?: never, modelUri?: never })) ): SearchEditorInput => { - const uri = existingData.uri ?? URI.from({ scheme: SearchEditorScheme, fragment: `${Math.random()}` }); + const defaultConfig: SearchConfiguration = { caseSensitive: false, contextLines: 0, excludes: '', includes: '', query: '', regexp: false, showIncludesExcludes: false, useIgnores: true, wholeWord: false }; + let config = { ...defaultConfig, ...existingData.config }; + + const modelUri = existingData.modelUri ?? URI.from({ scheme: SearchEditorScheme, fragment: `${Math.random()}` }); const instantiationService = accessor.get(IInstantiationService); const modelService = accessor.get(IModelService); - const textFileService = accessor.get(ITextFileService); const backupService = accessor.get(IBackupFileService); const modeService = accessor.get(IModeService); - const existing = inputs.get(uri.toString()); + const existing = inputs.get(modelUri.toString() + existingData.backingUri?.toString()); if (existing) { return existing; } @@ -307,52 +306,29 @@ export const getOrMakeSearchEditorInput = ( const getModel = async () => { let contents: string; - const backup = await backupService.resolve(uri); + const backup = await backupService.resolve(modelUri); if (backup) { // this way of stringifying a TextBufferFactory seems needlessly complicated... contents = snapshotToString(backup.value.create(DefaultEndOfLine.LF).createSnapshot(true)); - } else if (uri.scheme !== SearchEditorScheme) { - contents = (await textFileService.read(uri)).value; - } else if (existingData.text) { + } else if (existingData.text !== undefined) { contents = existingData.text; - } else if (existingData.config) { - contents = serializeSearchConfiguration(existingData.config); + } else if (existingData.backingUri !== undefined) { + const { text } = await instantiationService.invokeFunction(parseSavedSearchEditor, existingData.backingUri); + contents = text; + } else if (config !== undefined) { + contents = ''; } else { throw new Error('no initial contents for search editor'); } - backupService.discardBackup(uri); - - const lines = contents.split(/\r?\n/); - - const headerlines = []; - const bodylines = []; - let inHeader = true; - for (const line of lines) { - if (inHeader) { - headerlines.push(line); - if (line === '') { - inHeader = false; - } - } else { - bodylines.push(line); - } - } - - const contentsModelURI = uri.with({ scheme: SearchEditorBodyScheme }); - const headerModelURI = uri.with({ scheme: 'search-editor-header' }); - const contentsModel = modelService.getModel(contentsModelURI) ?? modelService.createModel('', modeService.create('search-result'), contentsModelURI); - const headerModel = modelService.getModel(headerModelURI) ?? modelService.createModel('', modeService.create('search-result'), headerModelURI); - - contentsModel.setValue(bodylines.join('\n')); - headerModel.setValue(headerlines.join('\n')); + backupService.discardBackup(modelUri); - return { contentsModel, headerModel }; + return modelService.getModel(modelUri) ?? modelService.createModel(contents, modeService.create('search-result'), modelUri); }; - const input = instantiationService.createInstance(SearchEditorInput, uri, getModel); + const input = instantiationService.createInstance(SearchEditorInput, modelUri, existingData.backingUri, config, getModel); - inputs.set(uri.toString(), input); - input.onDispose(() => inputs.delete(uri.toString())); + inputs.set(modelUri.toString(), input); + input.onDispose(() => inputs.delete(modelUri.toString())); return input; }; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts index 91d818eff12..8874be3fb67 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts @@ -3,16 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/searchEditor'; import { coalesce, flatten } from 'vs/base/common/arrays'; import { repeat } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; +import 'vs/css!./media/searchEditor'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { Range } from 'vs/editor/common/core/range'; -import { FileMatch, Match, searchMatchComparer, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; -import { ITextQuery } from 'vs/workbench/services/search/common/search'; -import { localize } from 'vs/nls'; import type { ITextModel } from 'vs/editor/common/model'; +import { localize } from 'vs/nls'; +import { FileMatch, Match, searchMatchComparer, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; import type { SearchConfiguration } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; +import { ITextQuery } from 'vs/workbench/services/search/common/search'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; // Using \r\n on Windows inserts an extra newline between results. const lineDelimiter = '\n'; @@ -104,8 +106,8 @@ function fileMatchToSearchResultFormat(fileMatch: FileMatch, labelFormatter: (x: return { text, matchRanges }; } -const contentPatternToSearchResultHeader = (pattern: ITextQuery | null, includes: string, excludes: string, contextLines: number): string[] => { - return serializeSearchConfiguration({ +const contentPatternToSearchConfiguration = (pattern: ITextQuery | null, includes: string, excludes: string, contextLines: number): Partial => { + return { query: pattern?.contentPattern.pattern, regexp: pattern?.contentPattern.isRegExp, caseSensitive: pattern?.contentPattern.isCaseSensitive, @@ -114,7 +116,7 @@ const contentPatternToSearchResultHeader = (pattern: ITextQuery | null, includes showIncludesExcludes: !!(includes || excludes || pattern?.userDisabledExcludesAndIgnoreFiles), useIgnores: pattern?.userDisabledExcludesAndIgnoreFiles === undefined ? undefined : !pattern.userDisabledExcludesAndIgnoreFiles, contextLines, - }).split(lineDelimiter); + }; }; export const serializeSearchConfiguration = (config: Partial): string => { @@ -139,11 +141,10 @@ export const serializeSearchConfiguration = (config: Partial + extractSearchQueryFromLines(model.getValueInRange(new Range(1, 1, 6, 1)).split(lineDelimiter)); -export const extractSearchQuery = (model: ITextModel | string): SearchConfiguration => { - const header = (typeof model === 'string') - ? model - : model.getValueInRange(new Range(1, 1, 6, 1)).split(lineDelimiter); +export const extractSearchQueryFromLines = (lines: string[]): SearchConfiguration => { const query: SearchConfiguration = { query: '', @@ -181,7 +182,7 @@ export const extractSearchQuery = (model: ITextModel | string): SearchConfigurat }; const parseYML = /^# ([^:]*): (.*)$/; - for (const line of header) { + for (const line of lines) { const parsed = parseYML.exec(line); if (!parsed) { continue; } const [, key, value] = parsed; @@ -205,10 +206,8 @@ export const extractSearchQuery = (model: ITextModel | string): SearchConfigurat }; export const serializeSearchResultForEditor = - (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, contextLines: number, labelFormatter: (x: URI) => string, includeHeader: boolean): { matchRanges: Range[], text: string } => { - const header = includeHeader - ? contentPatternToSearchResultHeader(searchResult.query, rawIncludePattern, rawExcludePattern, contextLines) - : []; + (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, contextLines: number, labelFormatter: (x: URI) => string): { matchRanges: Range[], text: string, config: Partial } => { + const config = contentPatternToSearchConfiguration(searchResult.query, rawIncludePattern, rawExcludePattern, contextLines); const filecount = searchResult.fileCount() > 1 ? localize('numFiles', "{0} files", searchResult.fileCount()) : localize('oneFile', "1 file"); const resultcount = searchResult.count() > 1 ? localize('numResults', "{0} results", searchResult.count()) : localize('oneResult', "1 result"); @@ -228,7 +227,8 @@ export const serializeSearchResultForEditor = return { matchRanges: allResults.matchRanges.map(translateRangeLines(info.length)), - text: header.concat(info).concat(allResults.text).join(lineDelimiter) + text: info.concat(allResults.text).join(lineDelimiter), + config }; }; @@ -244,3 +244,26 @@ const flattenSearchResultSerializations = (serializations: SearchResultSerializa return { text, matchRanges }; }; + +export const parseSavedSearchEditor = async (accessor: ServicesAccessor, resource: URI) => { + const textFileService = accessor.get(ITextFileService); + + const text = (await textFileService.read(resource)).value; + + const headerlines = []; + const bodylines = []; + + let inHeader = true; + for (const line of text.split(/\r?\n/g)) { + if (inHeader) { + headerlines.push(line); + if (line === '') { + inHeader = false; + } + } else { + bodylines.push(line); + } + } + + return { config: extractSearchQueryFromLines(headerlines), text: bodylines.join('\n') }; +}; -- GitLab