提交 379e732c 编写于 作者: J Jackson Kearl

Refactor to only use untitled search editors and transparently convert to disk...

Refactor to only use untitled search editors and transparently convert to disk format on save/load. Ref #89268.
上级 a8804400
......@@ -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<IEditorRegistry>(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;
......
......@@ -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;
}
......
......@@ -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<ISearchConfigurationProperties>('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);
};
......@@ -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<ITextModel>;
private readonly headerModel: Promise<ITextModel>;
private _cachedContentsModel: ITextModel | undefined;
private _cachedConfig?: SearchConfiguration;
private model: Promise<ITextModel>;
private _cachedModel: ITextModel | undefined;
private readonly _onDidChangeContent = this._register(new Emitter<void>());
readonly onDidChangeContent: Event<void> = this._onDidChangeContent.event;
private oldDecorationsIDs: string[] = [];
private _config: Readonly<SearchConfiguration>;
public get config(): Readonly<SearchConfiguration> { return this._config; }
public set config(value: Readonly<SearchConfiguration>) { 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<SearchConfiguration>,
getModel: () => Promise<ITextModel>,
@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<IEditorInput | undefined> {
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<IEditorInput | undefined> {
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<IWorkingCopyBackup> {
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<URI> {
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<string, SearchEditorInput>();
export const getOrMakeSearchEditorInput = (
accessor: ServicesAccessor,
existingData:
{ uri: URI, config?: Partial<SearchConfiguration>, text?: never } |
{ text: string, uri?: never, config?: never } |
{ config: Partial<SearchConfiguration>, text?: never, uri?: never }
existingData: ({ config: Partial<SearchConfiguration>, 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;
};
......@@ -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<SearchConfiguration> => {
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<SearchConfiguration>): string => {
......@@ -139,11 +141,10 @@ export const serializeSearchConfiguration = (config: Partial<SearchConfiguration
]).join(lineDelimiter);
};
export const extractSearchQueryFromModel = (model: ITextModel): SearchConfiguration =>
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<SearchConfiguration> } => {
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') };
};
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册