searchEditorInput.ts 11.3 KB
Newer Older
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6
import { Emitter, Event } from 'vs/base/common/event';
7
import * as network from 'vs/base/common/network';
8 9
import { basename } from 'vs/base/common/path';
import { isEqual, joinPath, toLocalResource } from 'vs/base/common/resources';
10 11
import { URI } from 'vs/base/common/uri';
import 'vs/css!./media/searchEditor';
12 13
import type { ICodeEditorViewState } from 'vs/editor/common/editorCommon';
import { IModelDeltaDecoration, ITextBufferFactory, ITextModel } from 'vs/editor/common/model';
14 15
import { IModelService } from 'vs/editor/common/services/modelService';
import { IModeService } from 'vs/editor/common/services/modeService';
16
import { localize } from 'vs/nls';
17
import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
18
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
19
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
20 21 22 23 24 25 26 27 28 29
import { EditorInput, GroupIdentifier, IEditorInput, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor';
import { extractSearchQuery, 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';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { ITextFileSaveOptions, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { IWorkingCopy, IWorkingCopyBackup, IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { SearchEditorScheme } from 'vs/workbench/contrib/searchEditor/browser/constants';
30 31 32 33 34 35 36 37 38 39 40 41 42

export type SearchConfiguration = {
	query: string,
	includes: string,
	excludes: string
	contextLines: number,
	wholeWord: boolean,
	caseSensitive: boolean,
	regexp: boolean,
	useIgnores: boolean,
	showIncludesExcludes: boolean,
};

J
Jackson Kearl 已提交
43 44 45 46
type SearchEditorViewState =
	| { focused: 'input' }
	| { focused: 'editor', state: ICodeEditorViewState };

47 48 49 50 51
export class SearchEditorInput extends EditorInput {
	static readonly ID: string = 'workbench.editorinputs.searchEditorInput';

	private dirty: boolean = false;
	private readonly model: Promise<ITextModel>;
52 53
	private query: Partial<SearchConfiguration> | undefined;

54 55 56
	private readonly _onDidChangeContent = new Emitter<void>();
	readonly onDidChangeContent: Event<void> = this._onDidChangeContent.event;

J
Jackson Kearl 已提交
57
	viewState: SearchEditorViewState = { focused: 'input' };
58

59 60
	private _highlights: IModelDeltaDecoration[] | undefined;

61 62 63
	constructor(
		public readonly resource: URI,
		getModel: () => Promise<ITextModel>,
64
		startingConfig: Partial<SearchConfiguration> | undefined,
65 66 67 68 69 70 71 72
		@IModelService private readonly modelService: IModelService,
		@IEditorService protected readonly editorService: IEditorService,
		@IEditorGroupsService protected readonly editorGroupService: IEditorGroupsService,
		@ITextFileService protected readonly textFileService: ITextFileService,
		@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
		@IFileDialogService private readonly fileDialogService: IFileDialogService,
		@IInstantiationService private readonly instantiationService: IInstantiationService,
		@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
73 74
		@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService,
		@ITelemetryService private readonly telemetryService: ITelemetryService,
75 76 77
	) {
		super();

78 79 80 81 82
		this.model = getModel()
			.then(model => {
				this._register(model.onDidChangeContent(() => this._onDidChangeContent.fire()));
				return model;
			});
83

B
Benjamin Pasero 已提交
84 85 86 87 88 89
		const input = this;
		const workingCopyAdapter = new class implements IWorkingCopy {
			readonly resource = input.getResource();
			get name() { return input.getName(); }
			readonly capabilities = input.isUntitled() ? WorkingCopyCapabilities.Untitled : 0;
			readonly onDidChangeDirty = input.onDidChangeDirty;
90
			readonly onDidChangeContent = input.onDidChangeContent;
B
Benjamin Pasero 已提交
91 92 93 94
			isDirty(): boolean { return input.isDirty(); }
			backup(): Promise<IWorkingCopyBackup> { return input.backup(); }
			save(options?: ISaveOptions): Promise<boolean> { return input.save(0, options).then(editor => !!editor); }
			revert(options?: IRevertOptions): Promise<boolean> { return input.revert(0, options); }
95 96 97
		};

		this.workingCopyService.registerWorkingCopy(workingCopyAdapter);
98 99

		this.query = startingConfig;
100 101
	}

102 103 104 105
	getResource() {
		return this.resource;
	}

106
	async save(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise<IEditorInput | undefined> {
107 108
		if ((await this.model).isDisposed()) { return; }

B
Benjamin Pasero 已提交
109 110
		if (this.isUntitled()) {
			return this.saveAs(group, options);
111
		} else {
J
Jackson Kearl 已提交
112
			await this.textFileService.write(this.resource, (await this.model).getValue(), options);
113
			this.setDirty(false);
J
Jackson Kearl 已提交
114
			return this;
115 116 117
		}
	}

B
Benjamin Pasero 已提交
118 119 120
	async saveAs(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise<IEditorInput | undefined> {
		const path = await this.fileDialogService.pickFileToSave(await this.suggestFileName(), options?.availableFileSystems);
		if (path) {
121
			this.telemetryService.publicLog2('searchEditor/saveSearchResults');
B
Benjamin Pasero 已提交
122 123 124
			if (await this.textFileService.saveAs(this.resource, path, options)) {
				this.setDirty(false);
				if (!isEqual(path, this.resource)) {
125 126 127
					const input = this.instantiationService.invokeFunction(getOrMakeSearchEditorInput, { uri: path });
					input.setHighlights(this.highlights);
					return input;
B
Benjamin Pasero 已提交
128 129 130 131 132 133 134
				}
				return this;
			}
		}
		return undefined;
	}

135 136 137 138
	getTypeId(): string {
		return SearchEditorInput.ID;
	}

139 140 141
	getName(maxLength = 12): string {
		const trimToMax = (label: string) => (label.length < maxLength ? label : `${label.slice(0, maxLength - 3)}...`);

B
Benjamin Pasero 已提交
142
		if (this.isUntitled()) {
143 144 145 146 147
			const query = this.query?.query?.trim();
			if (query) {
				return localize('searchTitle.withQuery', "Search: {0}", trimToMax(query));
			}
			return localize('searchTitle', "Search");
148 149 150 151 152 153 154 155
		}

		return localize('searchTitle.withQuery', "Search: {0}", basename(this.resource.path, '.code-search'));
	}

	async reloadModel() {
		const model = await this.model;
		const query = extractSearchQuery(model);
156
		this.query = query;
157 158
		this._highlights = model.getAllDecorations();

159 160 161 162 163
		this._onDidChangeLabel.fire();
		return { model, query };
	}

	getConfigSync() {
164
		return this.query;
165 166 167 168 169 170
	}

	async resolve() {
		return null;
	}

171
	setDirty(dirty: boolean) {
172 173 174 175 176 177 178 179
		this.dirty = dirty;
		this._onDidChangeDirty.fire();
	}

	isDirty() {
		return this.dirty;
	}

B
Benjamin Pasero 已提交
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
	isSaving(): boolean {
		if (!this.isDirty()) {
			return false; // the editor needs to be dirty for being saved
		}

		if (this.isUntitled()) {
			return false; // untitled are not saving automatically
		}

		if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) {
			return true; // a short auto save is configured, treat this as being saved
		}

		return false;
	}

	isReadonly() {
		return false;
	}

	isUntitled() {
201
		return this.resource.scheme === SearchEditorScheme;
B
Benjamin Pasero 已提交
202 203
	}

204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
	dispose() {
		this.modelService.destroyModel(this.resource);
		super.dispose();
	}

	matches(other: unknown) {
		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 false;
	}

223 224 225 226 227 228 229 230 231 232 233
	public get highlights(): IModelDeltaDecoration[] {
		return (this._highlights ?? []).map(({ range, options }) => ({ range, options }));
	}

	public async setHighlights(value: IModelDeltaDecoration[]) {
		if (!value) { return; }
		const model = await this.model;
		model.deltaDecorations([], value);
		this._highlights = value;
	}

B
Benjamin Pasero 已提交
234
	async revert(group: GroupIdentifier, options?: IRevertOptions) {
235
		// TODO: this should actually revert the contents. But it needs to set dirty false.
B
Benjamin Pasero 已提交
236
		super.revert(group, options);
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
		this.setDirty(false);
		return true;
	}

	private async backup(): Promise<IWorkingCopyBackup> {
		const content = (await this.model).createSnapshot();
		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 = (await this.reloadModel()).query.query;

		const searchFileName = (query.replace(/[^\w \-_]+/g, '_') || 'Search') + '.code-search';

		const remoteAuthority = this.environmentService.configuration.remoteAuthority;
		const schemeFilter = remoteAuthority ? network.Schemas.vscodeRemote : network.Schemas.file;

256 257 258
		const defaultFilePath = this.fileDialogService.defaultFilePath(schemeFilter);
		if (defaultFilePath) {
			return joinPath(defaultFilePath, searchFileName);
259 260
		}

B
Benjamin Pasero 已提交
261
		return toLocalResource(URI.from({ scheme: schemeFilter, path: searchFileName }), remoteAuthority);
262 263 264 265 266 267
	}
}

const inputs = new Map<string, SearchEditorInput>();
export const getOrMakeSearchEditorInput = (
	accessor: ServicesAccessor,
268 269 270 271
	existingData:
		{ uri: URI, config?: Partial<SearchConfiguration>, text?: never } |
		{ text: string, uri?: never, config?: never } |
		{ config: Partial<SearchConfiguration>, text?: never, uri?: never }
272 273
): SearchEditorInput => {

274
	const uri = existingData.uri ?? URI.from({ scheme: SearchEditorScheme, fragment: `${Math.random()}` });
275 276 277 278 279 280 281 282 283 284 285 286

	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());
	if (existing) {
		return existing;
	}

287
	const config = existingData.config ?? (existingData.text ? extractSearchQuery(existingData.text) : {});
288 289 290 291 292

	const getModel = async () => {
		const existing = modelService.getModel(uri);
		if (existing) { return existing; }

293 294
		const backup = await backupService.resolve(uri);
		backupService.discardBackup(uri);
295 296

		let contents: string | ITextBufferFactory;
297 298 299

		if (backup) {
			contents = backup.value;
300
		} else if (uri.scheme !== SearchEditorScheme) {
301
			contents = (await textFileService.read(uri)).value;
302 303 304 305
		} else if (existingData.text) {
			contents = existingData.text;
		} else if (existingData.config) {
			contents = serializeSearchConfiguration(existingData.config);
306
		} else {
307
			throw new Error('no initial contents for search editor');
308
		}
309

310 311 312
		return modelService.createModel(contents, modeService.create('search-result'), uri);
	};

313
	const input = instantiationService.createInstance(SearchEditorInput, uri, getModel, config);
314 315 316 317 318 319

	inputs.set(uri.toString(), input);
	input.onDispose(() => inputs.delete(uri.toString()));

	return input;
};