@theia/search-in-workspace 1.45.1 → 1.46.0-next.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -40
- package/lib/browser/components/search-in-workspace-input.d.ts +39 -39
- package/lib/browser/components/search-in-workspace-input.js +123 -123
- package/lib/browser/components/search-in-workspace-textarea.d.ts +39 -39
- package/lib/browser/components/search-in-workspace-textarea.js +130 -130
- package/lib/browser/search-in-workspace-context-key-service.d.ts +23 -23
- package/lib/browser/search-in-workspace-context-key-service.js +90 -90
- package/lib/browser/search-in-workspace-factory.d.ts +10 -10
- package/lib/browser/search-in-workspace-factory.js +68 -68
- package/lib/browser/search-in-workspace-frontend-contribution.d.ts +57 -55
- package/lib/browser/search-in-workspace-frontend-contribution.d.ts.map +1 -1
- package/lib/browser/search-in-workspace-frontend-contribution.js +516 -482
- package/lib/browser/search-in-workspace-frontend-contribution.js.map +1 -1
- package/lib/browser/search-in-workspace-frontend-module.d.ts +6 -6
- package/lib/browser/search-in-workspace-frontend-module.js +71 -71
- package/lib/browser/search-in-workspace-label-provider.d.ts +9 -9
- package/lib/browser/search-in-workspace-label-provider.js +57 -57
- package/lib/browser/search-in-workspace-preferences.d.ts +17 -17
- package/lib/browser/search-in-workspace-preferences.js +87 -87
- package/lib/browser/search-in-workspace-result-tree-widget.d.ts +259 -255
- package/lib/browser/search-in-workspace-result-tree-widget.d.ts.map +1 -1
- package/lib/browser/search-in-workspace-result-tree-widget.js +1172 -1099
- package/lib/browser/search-in-workspace-result-tree-widget.js.map +1 -1
- package/lib/browser/search-in-workspace-service.d.ts +35 -35
- package/lib/browser/search-in-workspace-service.js +158 -158
- package/lib/browser/search-in-workspace-widget.d.ts +121 -121
- package/lib/browser/search-in-workspace-widget.js +629 -629
- package/lib/browser/search-layout-migrations.d.ts +5 -5
- package/lib/browser/search-layout-migrations.js +64 -64
- package/lib/common/search-in-workspace-interface.d.ts +116 -116
- package/lib/common/search-in-workspace-interface.js +35 -35
- package/lib/node/ripgrep-search-in-workspace-server.d.ts +94 -94
- package/lib/node/ripgrep-search-in-workspace-server.js +430 -430
- package/lib/node/ripgrep-search-in-workspace-server.js.map +1 -1
- package/lib/node/ripgrep-search-in-workspace-server.slow-spec.d.ts +1 -1
- package/lib/node/ripgrep-search-in-workspace-server.slow-spec.js +899 -899
- package/lib/node/ripgrep-search-in-workspace-server.slow-spec.js.map +1 -1
- package/lib/node/search-in-workspace-backend-module.d.ts +3 -3
- package/lib/node/search-in-workspace-backend-module.js +32 -32
- package/package.json +9 -9
- package/src/browser/components/search-in-workspace-input.tsx +139 -139
- package/src/browser/components/search-in-workspace-textarea.tsx +153 -153
- package/src/browser/search-in-workspace-context-key-service.ts +93 -93
- package/src/browser/search-in-workspace-factory.ts +59 -59
- package/src/browser/search-in-workspace-frontend-contribution.ts +510 -474
- package/src/browser/search-in-workspace-frontend-module.ts +83 -83
- package/src/browser/search-in-workspace-label-provider.ts +48 -48
- package/src/browser/search-in-workspace-preferences.ts +96 -96
- package/src/browser/search-in-workspace-result-tree-widget.tsx +1318 -1245
- package/src/browser/search-in-workspace-service.ts +152 -152
- package/src/browser/search-in-workspace-widget.tsx +727 -727
- package/src/browser/search-layout-migrations.ts +53 -53
- package/src/browser/styles/index.css +400 -400
- package/src/browser/styles/search.svg +6 -6
- package/src/common/search-in-workspace-interface.ts +153 -153
- package/src/node/ripgrep-search-in-workspace-server.slow-spec.ts +1073 -1073
- package/src/node/ripgrep-search-in-workspace-server.ts +490 -490
- package/src/node/search-in-workspace-backend-module.ts +33 -33
|
@@ -1,727 +1,727 @@
|
|
|
1
|
-
// *****************************************************************************
|
|
2
|
-
// Copyright (C) 2018 TypeFox and others.
|
|
3
|
-
//
|
|
4
|
-
// This program and the accompanying materials are made available under the
|
|
5
|
-
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
-
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
-
//
|
|
8
|
-
// This Source Code may also be made available under the following Secondary
|
|
9
|
-
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
-
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
-
// with the GNU Classpath Exception which is available at
|
|
12
|
-
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
-
//
|
|
14
|
-
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
-
// *****************************************************************************
|
|
16
|
-
|
|
17
|
-
import { Widget, Message, BaseWidget, Key, StatefulWidget, MessageLoop, KeyCode, codicon } from '@theia/core/lib/browser';
|
|
18
|
-
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
|
19
|
-
import { SearchInWorkspaceResultTreeWidget } from './search-in-workspace-result-tree-widget';
|
|
20
|
-
import { SearchInWorkspaceOptions } from '../common/search-in-workspace-interface';
|
|
21
|
-
import * as React from '@theia/core/shared/react';
|
|
22
|
-
import { createRoot, Root } from '@theia/core/shared/react-dom/client';
|
|
23
|
-
import { Event, Emitter, Disposable } from '@theia/core/lib/common';
|
|
24
|
-
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
|
25
|
-
import { SearchInWorkspaceContextKeyService } from './search-in-workspace-context-key-service';
|
|
26
|
-
import { CancellationTokenSource } from '@theia/core';
|
|
27
|
-
import { ProgressBarFactory } from '@theia/core/lib/browser/progress-bar-factory';
|
|
28
|
-
import { EditorManager } from '@theia/editor/lib/browser';
|
|
29
|
-
import { SearchInWorkspacePreferences } from './search-in-workspace-preferences';
|
|
30
|
-
import { SearchInWorkspaceInput } from './components/search-in-workspace-input';
|
|
31
|
-
import { SearchInWorkspaceTextArea } from './components/search-in-workspace-textarea';
|
|
32
|
-
import { nls } from '@theia/core/lib/common/nls';
|
|
33
|
-
|
|
34
|
-
export interface SearchFieldState {
|
|
35
|
-
className: string;
|
|
36
|
-
enabled: boolean;
|
|
37
|
-
title: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
@injectable()
|
|
41
|
-
export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidget {
|
|
42
|
-
|
|
43
|
-
static ID = 'search-in-workspace';
|
|
44
|
-
static LABEL = nls.localizeByDefault('Search');
|
|
45
|
-
|
|
46
|
-
protected matchCaseState: SearchFieldState;
|
|
47
|
-
protected wholeWordState: SearchFieldState;
|
|
48
|
-
protected regExpState: SearchFieldState;
|
|
49
|
-
protected includeIgnoredState: SearchFieldState;
|
|
50
|
-
|
|
51
|
-
protected showSearchDetails = false;
|
|
52
|
-
protected _hasResults = false;
|
|
53
|
-
protected get hasResults(): boolean {
|
|
54
|
-
return this._hasResults;
|
|
55
|
-
}
|
|
56
|
-
protected set hasResults(hasResults: boolean) {
|
|
57
|
-
this.contextKeyService.hasSearchResult.set(hasResults);
|
|
58
|
-
this._hasResults = hasResults;
|
|
59
|
-
}
|
|
60
|
-
protected resultNumber = 0;
|
|
61
|
-
|
|
62
|
-
protected searchFieldContainerIsFocused = false;
|
|
63
|
-
|
|
64
|
-
protected searchInWorkspaceOptions: SearchInWorkspaceOptions;
|
|
65
|
-
|
|
66
|
-
protected searchTerm = '';
|
|
67
|
-
protected replaceTerm = '';
|
|
68
|
-
|
|
69
|
-
private searchRef = React.createRef<SearchInWorkspaceTextArea>();
|
|
70
|
-
private replaceRef = React.createRef<SearchInWorkspaceTextArea>();
|
|
71
|
-
private includeRef = React.createRef<SearchInWorkspaceInput>();
|
|
72
|
-
private excludeRef = React.createRef<SearchInWorkspaceInput>();
|
|
73
|
-
|
|
74
|
-
protected _showReplaceField = false;
|
|
75
|
-
protected get showReplaceField(): boolean {
|
|
76
|
-
return this._showReplaceField;
|
|
77
|
-
}
|
|
78
|
-
protected set showReplaceField(showReplaceField: boolean) {
|
|
79
|
-
this.contextKeyService.replaceActive.set(showReplaceField);
|
|
80
|
-
this._showReplaceField = showReplaceField;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
protected contentNode: HTMLElement;
|
|
84
|
-
protected searchFormContainer: HTMLElement;
|
|
85
|
-
protected resultContainer: HTMLElement;
|
|
86
|
-
|
|
87
|
-
protected readonly onDidUpdateEmitter = new Emitter<void>();
|
|
88
|
-
readonly onDidUpdate: Event<void> = this.onDidUpdateEmitter.event;
|
|
89
|
-
|
|
90
|
-
@inject(SearchInWorkspaceResultTreeWidget) readonly resultTreeWidget: SearchInWorkspaceResultTreeWidget;
|
|
91
|
-
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
|
|
92
|
-
|
|
93
|
-
@inject(SearchInWorkspaceContextKeyService)
|
|
94
|
-
protected readonly contextKeyService: SearchInWorkspaceContextKeyService;
|
|
95
|
-
|
|
96
|
-
@inject(ProgressBarFactory)
|
|
97
|
-
protected readonly progressBarFactory: ProgressBarFactory;
|
|
98
|
-
|
|
99
|
-
@inject(EditorManager) protected readonly editorManager: EditorManager;
|
|
100
|
-
|
|
101
|
-
@inject(SearchInWorkspacePreferences)
|
|
102
|
-
protected readonly searchInWorkspacePreferences: SearchInWorkspacePreferences;
|
|
103
|
-
|
|
104
|
-
protected searchFormContainerRoot: Root;
|
|
105
|
-
|
|
106
|
-
@postConstruct()
|
|
107
|
-
protected init(): void {
|
|
108
|
-
this.id = SearchInWorkspaceWidget.ID;
|
|
109
|
-
this.title.label = SearchInWorkspaceWidget.LABEL;
|
|
110
|
-
this.title.caption = SearchInWorkspaceWidget.LABEL;
|
|
111
|
-
this.title.iconClass = codicon('search');
|
|
112
|
-
this.title.closable = true;
|
|
113
|
-
this.contentNode = document.createElement('div');
|
|
114
|
-
this.contentNode.classList.add('t-siw-search-container');
|
|
115
|
-
this.searchFormContainer = document.createElement('div');
|
|
116
|
-
this.searchFormContainer.classList.add('searchHeader');
|
|
117
|
-
this.contentNode.appendChild(this.searchFormContainer);
|
|
118
|
-
this.searchFormContainerRoot = createRoot(this.searchFormContainer);
|
|
119
|
-
this.node.tabIndex = 0;
|
|
120
|
-
this.node.appendChild(this.contentNode);
|
|
121
|
-
|
|
122
|
-
this.matchCaseState = {
|
|
123
|
-
className: codicon('case-sensitive'),
|
|
124
|
-
enabled: false,
|
|
125
|
-
title: nls.localizeByDefault('Match Case')
|
|
126
|
-
};
|
|
127
|
-
this.wholeWordState = {
|
|
128
|
-
className: codicon('whole-word'),
|
|
129
|
-
enabled: false,
|
|
130
|
-
title: nls.localizeByDefault('Match Whole Word')
|
|
131
|
-
};
|
|
132
|
-
this.regExpState = {
|
|
133
|
-
className: codicon('regex'),
|
|
134
|
-
enabled: false,
|
|
135
|
-
title: nls.localizeByDefault('Use Regular Expression')
|
|
136
|
-
};
|
|
137
|
-
this.includeIgnoredState = {
|
|
138
|
-
className: codicon('eye'),
|
|
139
|
-
enabled: false,
|
|
140
|
-
title: nls.localize('theia/search-in-workspace/includeIgnoredFiles', 'Include Ignored Files')
|
|
141
|
-
};
|
|
142
|
-
this.searchInWorkspaceOptions = {
|
|
143
|
-
matchCase: false,
|
|
144
|
-
matchWholeWord: false,
|
|
145
|
-
useRegExp: false,
|
|
146
|
-
multiline: false,
|
|
147
|
-
includeIgnored: false,
|
|
148
|
-
include: [],
|
|
149
|
-
exclude: [],
|
|
150
|
-
maxResults: 2000
|
|
151
|
-
};
|
|
152
|
-
this.toDispose.push(this.resultTreeWidget.onChange(r => {
|
|
153
|
-
this.hasResults = r.size > 0;
|
|
154
|
-
this.resultNumber = 0;
|
|
155
|
-
const results = Array.from(r.values());
|
|
156
|
-
results.forEach(rootFolder =>
|
|
157
|
-
rootFolder.children.forEach(file => this.resultNumber += file.children.length)
|
|
158
|
-
);
|
|
159
|
-
this.update();
|
|
160
|
-
}));
|
|
161
|
-
|
|
162
|
-
this.toDispose.push(this.resultTreeWidget.onFocusInput(b => {
|
|
163
|
-
this.focusInputField();
|
|
164
|
-
}));
|
|
165
|
-
|
|
166
|
-
this.toDispose.push(this.searchInWorkspacePreferences.onPreferenceChanged(e => {
|
|
167
|
-
if (e.preferenceName === 'search.smartCase') {
|
|
168
|
-
this.performSearch();
|
|
169
|
-
}
|
|
170
|
-
}));
|
|
171
|
-
|
|
172
|
-
this.toDispose.push(this.resultTreeWidget);
|
|
173
|
-
this.toDispose.push(this.resultTreeWidget.onExpansionChanged(() => {
|
|
174
|
-
this.onDidUpdateEmitter.fire();
|
|
175
|
-
}));
|
|
176
|
-
|
|
177
|
-
this.toDispose.push(this.progressBarFactory({ container: this.node, insertMode: 'prepend', locationId: 'search' }));
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
storeState(): object {
|
|
181
|
-
return {
|
|
182
|
-
matchCaseState: this.matchCaseState,
|
|
183
|
-
wholeWordState: this.wholeWordState,
|
|
184
|
-
regExpState: this.regExpState,
|
|
185
|
-
includeIgnoredState: this.includeIgnoredState,
|
|
186
|
-
showSearchDetails: this.showSearchDetails,
|
|
187
|
-
searchInWorkspaceOptions: this.searchInWorkspaceOptions,
|
|
188
|
-
searchTerm: this.searchTerm,
|
|
189
|
-
replaceTerm: this.replaceTerm,
|
|
190
|
-
showReplaceField: this.showReplaceField,
|
|
191
|
-
searchHistoryState: this.searchRef.current?.state,
|
|
192
|
-
replaceHistoryState: this.replaceRef.current?.state,
|
|
193
|
-
includeHistoryState: this.includeRef.current?.state,
|
|
194
|
-
excludeHistoryState: this.excludeRef.current?.state,
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
199
|
-
restoreState(oldState: any): void {
|
|
200
|
-
this.matchCaseState = oldState.matchCaseState;
|
|
201
|
-
this.wholeWordState = oldState.wholeWordState;
|
|
202
|
-
this.regExpState = oldState.regExpState;
|
|
203
|
-
this.includeIgnoredState = oldState.includeIgnoredState;
|
|
204
|
-
// Override the title of the restored state, as we could have changed languages in between
|
|
205
|
-
this.matchCaseState.title = nls.localizeByDefault('Match Case');
|
|
206
|
-
this.wholeWordState.title = nls.localizeByDefault('Match Whole Word');
|
|
207
|
-
this.regExpState.title = nls.localizeByDefault('Use Regular Expression');
|
|
208
|
-
this.includeIgnoredState.title = nls.localize('theia/search-in-workspace/includeIgnoredFiles', 'Include Ignored Files');
|
|
209
|
-
this.showSearchDetails = oldState.showSearchDetails;
|
|
210
|
-
this.searchInWorkspaceOptions = oldState.searchInWorkspaceOptions;
|
|
211
|
-
this.searchTerm = oldState.searchTerm;
|
|
212
|
-
this.replaceTerm = oldState.replaceTerm;
|
|
213
|
-
this.showReplaceField = oldState.showReplaceField;
|
|
214
|
-
this.resultTreeWidget.replaceTerm = this.replaceTerm;
|
|
215
|
-
this.resultTreeWidget.showReplaceButtons = this.showReplaceField;
|
|
216
|
-
this.searchRef.current?.setState(oldState.searchHistoryState);
|
|
217
|
-
this.replaceRef.current?.setState(oldState.replaceHistoryState);
|
|
218
|
-
this.includeRef.current?.setState(oldState.includeHistoryState);
|
|
219
|
-
this.excludeRef.current?.setState(oldState.excludeHistoryState);
|
|
220
|
-
this.refresh();
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
findInFolder(uris: string[]): void {
|
|
224
|
-
this.showSearchDetails = true;
|
|
225
|
-
const values = Array.from(new Set(uris.map(uri => `${uri}/**`)));
|
|
226
|
-
const value = values.join(', ');
|
|
227
|
-
this.searchInWorkspaceOptions.include = values;
|
|
228
|
-
if (this.includeRef.current) {
|
|
229
|
-
this.includeRef.current.value = value;
|
|
230
|
-
this.includeRef.current.addToHistory();
|
|
231
|
-
}
|
|
232
|
-
this.update();
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Update the search term and input field.
|
|
237
|
-
* @param term the search term.
|
|
238
|
-
* @param showReplaceField controls if the replace field should be displayed.
|
|
239
|
-
*/
|
|
240
|
-
updateSearchTerm(term: string, showReplaceField?: boolean): void {
|
|
241
|
-
this.searchTerm = term;
|
|
242
|
-
if (this.searchRef.current) {
|
|
243
|
-
this.searchRef.current.value = term;
|
|
244
|
-
this.searchRef.current.addToHistory();
|
|
245
|
-
}
|
|
246
|
-
if (showReplaceField) {
|
|
247
|
-
this.showReplaceField = true;
|
|
248
|
-
}
|
|
249
|
-
this.refresh();
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
hasResultList(): boolean {
|
|
253
|
-
return this.hasResults;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
hasSearchTerm(): boolean {
|
|
257
|
-
return this.searchTerm !== '';
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
refresh(): void {
|
|
261
|
-
this.performSearch();
|
|
262
|
-
this.update();
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
getCancelIndicator(): CancellationTokenSource | undefined {
|
|
266
|
-
return this.resultTreeWidget.cancelIndicator;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
collapseAll(): void {
|
|
270
|
-
this.resultTreeWidget.collapseAll();
|
|
271
|
-
this.update();
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
expandAll(): void {
|
|
275
|
-
this.resultTreeWidget.expandAll();
|
|
276
|
-
this.update();
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
areResultsCollapsed(): boolean {
|
|
280
|
-
return this.resultTreeWidget.areResultsCollapsed();
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
clear(): void {
|
|
284
|
-
this.searchTerm = '';
|
|
285
|
-
this.replaceTerm = '';
|
|
286
|
-
this.searchInWorkspaceOptions.include = [];
|
|
287
|
-
this.searchInWorkspaceOptions.exclude = [];
|
|
288
|
-
this.includeIgnoredState.enabled = false;
|
|
289
|
-
this.matchCaseState.enabled = false;
|
|
290
|
-
this.wholeWordState.enabled = false;
|
|
291
|
-
this.regExpState.enabled = false;
|
|
292
|
-
if (this.searchRef.current) {
|
|
293
|
-
this.searchRef.current.value = '';
|
|
294
|
-
}
|
|
295
|
-
if (this.replaceRef.current) {
|
|
296
|
-
this.replaceRef.current.value = '';
|
|
297
|
-
}
|
|
298
|
-
if (this.includeRef.current) {
|
|
299
|
-
this.includeRef.current.value = '';
|
|
300
|
-
}
|
|
301
|
-
if (this.excludeRef.current) {
|
|
302
|
-
this.excludeRef.current.value = '';
|
|
303
|
-
}
|
|
304
|
-
this.performSearch();
|
|
305
|
-
this.update();
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
protected override onAfterAttach(msg: Message): void {
|
|
309
|
-
super.onAfterAttach(msg);
|
|
310
|
-
this.searchFormContainerRoot.render(<React.Fragment>{this.renderSearchHeader()}{this.renderSearchInfo()}</React.Fragment>);
|
|
311
|
-
Widget.attach(this.resultTreeWidget, this.contentNode);
|
|
312
|
-
this.toDisposeOnDetach.push(Disposable.create(() => {
|
|
313
|
-
Widget.detach(this.resultTreeWidget);
|
|
314
|
-
}));
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
protected override onUpdateRequest(msg: Message): void {
|
|
318
|
-
super.onUpdateRequest(msg);
|
|
319
|
-
const searchInfo = this.renderSearchInfo();
|
|
320
|
-
if (searchInfo) {
|
|
321
|
-
this.searchFormContainerRoot.render(<React.Fragment>{this.renderSearchHeader()}{searchInfo}</React.Fragment>);
|
|
322
|
-
this.onDidUpdateEmitter.fire(undefined);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
protected override onResize(msg: Widget.ResizeMessage): void {
|
|
327
|
-
super.onResize(msg);
|
|
328
|
-
this.searchRef.current?.forceUpdate();
|
|
329
|
-
this.replaceRef.current?.forceUpdate();
|
|
330
|
-
MessageLoop.sendMessage(this.resultTreeWidget, Widget.ResizeMessage.UnknownSize);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
protected override onAfterShow(msg: Message): void {
|
|
334
|
-
super.onAfterShow(msg);
|
|
335
|
-
this.focusInputField();
|
|
336
|
-
this.contextKeyService.searchViewletVisible.set(true);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
protected override onAfterHide(msg: Message): void {
|
|
340
|
-
super.onAfterHide(msg);
|
|
341
|
-
this.contextKeyService.searchViewletVisible.set(false);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
protected override onActivateRequest(msg: Message): void {
|
|
345
|
-
super.onActivateRequest(msg);
|
|
346
|
-
this.focusInputField();
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
protected focusInputField(): void {
|
|
350
|
-
const f = document.getElementById('search-input-field');
|
|
351
|
-
if (f) {
|
|
352
|
-
(f as HTMLInputElement).focus();
|
|
353
|
-
(f as HTMLInputElement).select();
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
protected renderSearchHeader(): React.ReactNode {
|
|
358
|
-
const searchAndReplaceContainer = this.renderSearchAndReplace();
|
|
359
|
-
const searchDetails = this.renderSearchDetails();
|
|
360
|
-
return <div>{searchAndReplaceContainer}{searchDetails}</div>;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
protected renderSearchAndReplace(): React.ReactNode {
|
|
364
|
-
const toggleContainer = this.renderReplaceFieldToggle();
|
|
365
|
-
const searchField = this.renderSearchField();
|
|
366
|
-
const replaceField = this.renderReplaceField();
|
|
367
|
-
return <div className='search-and-replace-container'>
|
|
368
|
-
{toggleContainer}
|
|
369
|
-
<div className='search-and-replace-fields'>
|
|
370
|
-
{searchField}
|
|
371
|
-
{replaceField}
|
|
372
|
-
</div>
|
|
373
|
-
</div>;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
protected renderReplaceFieldToggle(): React.ReactNode {
|
|
377
|
-
const toggle = <span className={codicon(this.showReplaceField ? 'chevron-down' : 'chevron-right')}></span>;
|
|
378
|
-
return <div
|
|
379
|
-
title={nls.localizeByDefault('Toggle Replace')}
|
|
380
|
-
className='replace-toggle'
|
|
381
|
-
tabIndex={0}
|
|
382
|
-
onClick={e => {
|
|
383
|
-
const elArr = document.getElementsByClassName('replace-toggle');
|
|
384
|
-
if (elArr && elArr.length > 0) {
|
|
385
|
-
(elArr[0] as HTMLElement).focus();
|
|
386
|
-
}
|
|
387
|
-
this.showReplaceField = !this.showReplaceField;
|
|
388
|
-
this.resultTreeWidget.showReplaceButtons = this.showReplaceField;
|
|
389
|
-
this.update();
|
|
390
|
-
}}>
|
|
391
|
-
{toggle}
|
|
392
|
-
</div>;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
protected renderNotification(): React.ReactNode {
|
|
396
|
-
if (this.workspaceService.tryGetRoots().length <= 0 && this.editorManager.all.length <= 0) {
|
|
397
|
-
return <div className='search-notification show'>
|
|
398
|
-
<div>{nls.localize('theia/search-in-workspace/noFolderSpecified', 'You have not opened or specified a folder. Only open files are currently searched.')}</div>
|
|
399
|
-
</div>;
|
|
400
|
-
}
|
|
401
|
-
return <div
|
|
402
|
-
className={`search-notification ${this.searchInWorkspaceOptions.maxResults && this.resultNumber >= this.searchInWorkspaceOptions.maxResults ? 'show' : ''}`}>
|
|
403
|
-
<div>{nls.localize('theia/search-in-workspace/resultSubset',
|
|
404
|
-
'This is only a subset of all results. Use a more specific search term to narrow down the result list.')}</div>
|
|
405
|
-
</div>;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
protected readonly focusSearchFieldContainer = () => this.doFocusSearchFieldContainer();
|
|
409
|
-
protected doFocusSearchFieldContainer(): void {
|
|
410
|
-
this.searchFieldContainerIsFocused = true;
|
|
411
|
-
this.update();
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
protected readonly blurSearchFieldContainer = () => this.doBlurSearchFieldContainer();
|
|
415
|
-
protected doBlurSearchFieldContainer(): void {
|
|
416
|
-
this.searchFieldContainerIsFocused = false;
|
|
417
|
-
this.update();
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
private _searchTimeout: number;
|
|
421
|
-
protected readonly search = (e: React.KeyboardEvent) => {
|
|
422
|
-
e.persist();
|
|
423
|
-
const searchOnType = this.searchInWorkspacePreferences['search.searchOnType'];
|
|
424
|
-
if (searchOnType) {
|
|
425
|
-
const delay = this.searchInWorkspacePreferences['search.searchOnTypeDebouncePeriod'] || 0;
|
|
426
|
-
window.clearTimeout(this._searchTimeout);
|
|
427
|
-
this._searchTimeout = window.setTimeout(() => this.doSearch(e), delay);
|
|
428
|
-
}
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
protected readonly onKeyDownSearch = (e: React.KeyboardEvent) => {
|
|
432
|
-
if (Key.ENTER.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode) {
|
|
433
|
-
this.searchTerm = (e.target as HTMLInputElement).value;
|
|
434
|
-
this.performSearch();
|
|
435
|
-
}
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
protected doSearch(e: React.KeyboardEvent): void {
|
|
439
|
-
if (e.target) {
|
|
440
|
-
const searchValue = (e.target as HTMLInputElement).value;
|
|
441
|
-
if (this.searchTerm === searchValue && Key.ENTER.keyCode !== KeyCode.createKeyCode(e.nativeEvent).key?.keyCode) {
|
|
442
|
-
return;
|
|
443
|
-
} else {
|
|
444
|
-
this.searchTerm = searchValue;
|
|
445
|
-
this.performSearch();
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
protected performSearch(): void {
|
|
451
|
-
const searchOptions: SearchInWorkspaceOptions = {
|
|
452
|
-
...this.searchInWorkspaceOptions,
|
|
453
|
-
followSymlinks: this.shouldFollowSymlinks(),
|
|
454
|
-
matchCase: this.shouldMatchCase(),
|
|
455
|
-
multiline: this.searchTerm.includes('\n')
|
|
456
|
-
};
|
|
457
|
-
this.resultTreeWidget.search(this.searchTerm, searchOptions);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
protected shouldFollowSymlinks(): boolean {
|
|
461
|
-
return this.searchInWorkspacePreferences['search.followSymlinks'];
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Determine if search should be case sensitive.
|
|
466
|
-
*/
|
|
467
|
-
protected shouldMatchCase(): boolean {
|
|
468
|
-
if (this.matchCaseState.enabled) {
|
|
469
|
-
return this.matchCaseState.enabled;
|
|
470
|
-
}
|
|
471
|
-
// search.smartCase makes siw search case-sensitive if the search term contains uppercase letter(s).
|
|
472
|
-
return (
|
|
473
|
-
!!this.searchInWorkspacePreferences['search.smartCase']
|
|
474
|
-
&& this.searchTerm !== this.searchTerm.toLowerCase()
|
|
475
|
-
);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
protected renderSearchField(): React.ReactNode {
|
|
479
|
-
const input = <SearchInWorkspaceTextArea
|
|
480
|
-
id='search-input-field'
|
|
481
|
-
className='theia-input'
|
|
482
|
-
title={SearchInWorkspaceWidget.LABEL}
|
|
483
|
-
placeholder={SearchInWorkspaceWidget.LABEL}
|
|
484
|
-
defaultValue={this.searchTerm}
|
|
485
|
-
autoComplete='off'
|
|
486
|
-
onKeyUp={this.search}
|
|
487
|
-
onKeyDown={this.onKeyDownSearch}
|
|
488
|
-
onFocus={this.handleFocusSearchInputBox}
|
|
489
|
-
onBlur={this.handleBlurSearchInputBox}
|
|
490
|
-
ref={this.searchRef}
|
|
491
|
-
/>;
|
|
492
|
-
const notification = this.renderNotification();
|
|
493
|
-
const optionContainer = this.renderOptionContainer();
|
|
494
|
-
const tooMany = this.searchInWorkspaceOptions.maxResults && this.resultNumber >= this.searchInWorkspaceOptions.maxResults ? 'tooManyResults' : '';
|
|
495
|
-
const className = `search-field-container ${tooMany} ${this.searchFieldContainerIsFocused ? 'focused' : ''}`;
|
|
496
|
-
return <div className={className}>
|
|
497
|
-
<div className='search-field' tabIndex={-1} onFocus={this.focusSearchFieldContainer} onBlur={this.blurSearchFieldContainer}>
|
|
498
|
-
{input}
|
|
499
|
-
{optionContainer}
|
|
500
|
-
</div>
|
|
501
|
-
{notification}
|
|
502
|
-
</div>;
|
|
503
|
-
}
|
|
504
|
-
protected handleFocusSearchInputBox = (event: React.FocusEvent<HTMLTextAreaElement>) => {
|
|
505
|
-
event.target.placeholder = `${SearchInWorkspaceWidget.LABEL} (⇅ ${nls.localizeByDefault('for history')})`;
|
|
506
|
-
this.contextKeyService.setSearchInputBoxFocus(true);
|
|
507
|
-
};
|
|
508
|
-
protected handleBlurSearchInputBox = (event: React.FocusEvent<HTMLTextAreaElement>) => {
|
|
509
|
-
event.target.placeholder = SearchInWorkspaceWidget.LABEL;
|
|
510
|
-
this.contextKeyService.setSearchInputBoxFocus(false);
|
|
511
|
-
};
|
|
512
|
-
|
|
513
|
-
protected readonly updateReplaceTerm = (e: React.KeyboardEvent) => this.doUpdateReplaceTerm(e);
|
|
514
|
-
protected doUpdateReplaceTerm(e: React.KeyboardEvent): void {
|
|
515
|
-
if (e.target) {
|
|
516
|
-
this.replaceTerm = (e.target as HTMLInputElement).value;
|
|
517
|
-
this.resultTreeWidget.replaceTerm = this.replaceTerm;
|
|
518
|
-
if (KeyCode.createKeyCode(e.nativeEvent).key?.keyCode === Key.ENTER.keyCode) { this.performSearch(); }
|
|
519
|
-
this.update();
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
protected renderReplaceField(): React.ReactNode {
|
|
524
|
-
const replaceAllButtonContainer = this.renderReplaceAllButtonContainer();
|
|
525
|
-
const replace = nls.localizeByDefault('Replace');
|
|
526
|
-
return <div className={`replace-field${this.showReplaceField ? '' : ' hidden'}`}>
|
|
527
|
-
<SearchInWorkspaceTextArea
|
|
528
|
-
id='replace-input-field'
|
|
529
|
-
className='theia-input'
|
|
530
|
-
title={replace}
|
|
531
|
-
placeholder={replace}
|
|
532
|
-
defaultValue={this.replaceTerm}
|
|
533
|
-
autoComplete='off'
|
|
534
|
-
onKeyUp={this.updateReplaceTerm}
|
|
535
|
-
onFocus={this.handleFocusReplaceInputBox}
|
|
536
|
-
onBlur={this.handleBlurReplaceInputBox}
|
|
537
|
-
ref={this.replaceRef}
|
|
538
|
-
/>
|
|
539
|
-
{replaceAllButtonContainer}
|
|
540
|
-
</div>;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
protected handleFocusReplaceInputBox = (event: React.FocusEvent<HTMLTextAreaElement>) => {
|
|
544
|
-
event.target.placeholder = `${nls.localizeByDefault('Replace')} (⇅ ${nls.localizeByDefault('for history')})`;
|
|
545
|
-
this.contextKeyService.setReplaceInputBoxFocus(true);
|
|
546
|
-
};
|
|
547
|
-
protected handleBlurReplaceInputBox = (event: React.FocusEvent<HTMLTextAreaElement>) => {
|
|
548
|
-
event.target.placeholder = nls.localizeByDefault('Replace');
|
|
549
|
-
this.contextKeyService.setReplaceInputBoxFocus(false);
|
|
550
|
-
};
|
|
551
|
-
|
|
552
|
-
protected renderReplaceAllButtonContainer(): React.ReactNode {
|
|
553
|
-
// The `Replace All` button is enabled if there is a search term present with results.
|
|
554
|
-
const enabled: boolean = this.searchTerm !== '' && this.resultNumber > 0;
|
|
555
|
-
return <div className='replace-all-button-container'>
|
|
556
|
-
<span
|
|
557
|
-
title={nls.localizeByDefault('Replace All')}
|
|
558
|
-
className={`${codicon('replace-all', true)} ${enabled ? ' ' : ' disabled'}`}
|
|
559
|
-
onClick={() => {
|
|
560
|
-
if (enabled) {
|
|
561
|
-
this.resultTreeWidget.replace(undefined);
|
|
562
|
-
}
|
|
563
|
-
}}>
|
|
564
|
-
</span>
|
|
565
|
-
</div>;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
protected renderOptionContainer(): React.ReactNode {
|
|
569
|
-
const matchCaseOption = this.renderOptionElement(this.matchCaseState);
|
|
570
|
-
const wholeWordOption = this.renderOptionElement(this.wholeWordState);
|
|
571
|
-
const regexOption = this.renderOptionElement(this.regExpState);
|
|
572
|
-
const includeIgnoredOption = this.renderOptionElement(this.includeIgnoredState);
|
|
573
|
-
return <div className='option-buttons'>{matchCaseOption}{wholeWordOption}{regexOption}{includeIgnoredOption}</div>;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
protected renderOptionElement(opt: SearchFieldState): React.ReactNode {
|
|
577
|
-
return <span
|
|
578
|
-
className={`${opt.className} option action-label ${opt.enabled ? 'enabled' : ''}`}
|
|
579
|
-
title={opt.title}
|
|
580
|
-
onClick={() => this.handleOptionClick(opt)}></span>;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
protected handleOptionClick(option: SearchFieldState): void {
|
|
584
|
-
option.enabled = !option.enabled;
|
|
585
|
-
this.updateSearchOptions();
|
|
586
|
-
this.searchFieldContainerIsFocused = true;
|
|
587
|
-
this.performSearch();
|
|
588
|
-
this.update();
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
protected updateSearchOptions(): void {
|
|
592
|
-
this.searchInWorkspaceOptions.matchCase = this.matchCaseState.enabled;
|
|
593
|
-
this.searchInWorkspaceOptions.matchWholeWord = this.wholeWordState.enabled;
|
|
594
|
-
this.searchInWorkspaceOptions.useRegExp = this.regExpState.enabled;
|
|
595
|
-
this.searchInWorkspaceOptions.includeIgnored = this.includeIgnoredState.enabled;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
protected renderSearchDetails(): React.ReactNode {
|
|
599
|
-
const expandButton = this.renderExpandGlobFieldsButton();
|
|
600
|
-
const globFieldContainer = this.renderGlobFieldContainer();
|
|
601
|
-
return <div className='search-details'>{expandButton}{globFieldContainer}</div>;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
protected renderGlobFieldContainer(): React.ReactNode {
|
|
605
|
-
const includeField = this.renderGlobField('include');
|
|
606
|
-
const excludeField = this.renderGlobField('exclude');
|
|
607
|
-
return <div className={`glob-field-container${!this.showSearchDetails ? ' hidden' : ''}`}>{includeField}{excludeField}</div>;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
protected renderExpandGlobFieldsButton(): React.ReactNode {
|
|
611
|
-
return <div className='button-container'>
|
|
612
|
-
<span
|
|
613
|
-
title={nls.localizeByDefault('Toggle Search Details')}
|
|
614
|
-
className={codicon('ellipsis')}
|
|
615
|
-
onClick={() => {
|
|
616
|
-
this.showSearchDetails = !this.showSearchDetails;
|
|
617
|
-
this.update();
|
|
618
|
-
}}></span>
|
|
619
|
-
</div>;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
protected renderGlobField(kind: 'include' | 'exclude'): React.ReactNode {
|
|
623
|
-
const currentValue = this.searchInWorkspaceOptions[kind];
|
|
624
|
-
const value = currentValue && currentValue.join(', ') || '';
|
|
625
|
-
return <div className='glob-field'>
|
|
626
|
-
<div className='label'>{nls.localizeByDefault('files to ' + kind)}</div>
|
|
627
|
-
<SearchInWorkspaceInput
|
|
628
|
-
className='theia-input'
|
|
629
|
-
type='text'
|
|
630
|
-
size={1}
|
|
631
|
-
defaultValue={value}
|
|
632
|
-
autoComplete='off'
|
|
633
|
-
id={kind + '-glob-field'}
|
|
634
|
-
placeholder={kind === 'include'
|
|
635
|
-
? nls.localizeByDefault('e.g. *.ts, src/**/include')
|
|
636
|
-
: nls.localizeByDefault('e.g. *.ts, src/**/exclude')
|
|
637
|
-
}
|
|
638
|
-
onKeyUp={e => {
|
|
639
|
-
if (e.target) {
|
|
640
|
-
const targetValue = (e.target as HTMLInputElement).value || '';
|
|
641
|
-
let shouldSearch = Key.ENTER.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode;
|
|
642
|
-
const currentOptions = (this.searchInWorkspaceOptions[kind] || []).slice().map(s => s.trim()).sort();
|
|
643
|
-
const candidateOptions = this.splitOnComma(targetValue).map(s => s.trim()).sort();
|
|
644
|
-
const sameAs = (left: string[], right: string[]) => {
|
|
645
|
-
if (left.length !== right.length) {
|
|
646
|
-
return false;
|
|
647
|
-
}
|
|
648
|
-
for (let i = 0; i < left.length; i++) {
|
|
649
|
-
if (left[i] !== right[i]) {
|
|
650
|
-
return false;
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
return true;
|
|
654
|
-
};
|
|
655
|
-
if (!sameAs(currentOptions, candidateOptions)) {
|
|
656
|
-
this.searchInWorkspaceOptions[kind] = this.splitOnComma(targetValue);
|
|
657
|
-
shouldSearch = true;
|
|
658
|
-
}
|
|
659
|
-
if (shouldSearch) {
|
|
660
|
-
this.performSearch();
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
}}
|
|
664
|
-
onFocus={kind === 'include' ? this.handleFocusIncludesInputBox : this.handleFocusExcludesInputBox}
|
|
665
|
-
onBlur={kind === 'include' ? this.handleBlurIncludesInputBox : this.handleBlurExcludesInputBox}
|
|
666
|
-
ref={kind === 'include' ? this.includeRef : this.excludeRef}
|
|
667
|
-
/>
|
|
668
|
-
</div>;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
protected handleFocusIncludesInputBox = () => this.contextKeyService.setPatternIncludesInputBoxFocus(true);
|
|
672
|
-
protected handleBlurIncludesInputBox = () => this.contextKeyService.setPatternIncludesInputBoxFocus(false);
|
|
673
|
-
|
|
674
|
-
protected handleFocusExcludesInputBox = () => this.contextKeyService.setPatternExcludesInputBoxFocus(true);
|
|
675
|
-
protected handleBlurExcludesInputBox = () => this.contextKeyService.setPatternExcludesInputBoxFocus(false);
|
|
676
|
-
|
|
677
|
-
protected splitOnComma(patterns: string): string[] {
|
|
678
|
-
return patterns.length > 0 ? patterns.split(',').map(s => s.trim()) : [];
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
protected renderSearchInfo(): React.ReactNode {
|
|
682
|
-
const message = this.getSearchResultMessage() || '';
|
|
683
|
-
return <div className='search-info'>{message}</div>;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
protected getSearchResultMessage(): string | undefined {
|
|
687
|
-
|
|
688
|
-
if (!this.searchTerm) {
|
|
689
|
-
return undefined;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
if (this.resultNumber === 0) {
|
|
693
|
-
const isIncludesPresent = this.searchInWorkspaceOptions.include && this.searchInWorkspaceOptions.include.length > 0;
|
|
694
|
-
const isExcludesPresent = this.searchInWorkspaceOptions.exclude && this.searchInWorkspaceOptions.exclude.length > 0;
|
|
695
|
-
|
|
696
|
-
let message: string;
|
|
697
|
-
if (isIncludesPresent && isExcludesPresent) {
|
|
698
|
-
message = nls.localizeByDefault("No results found in '{0}' excluding '{1}' - ",
|
|
699
|
-
this.searchInWorkspaceOptions.include!.toString(), this.searchInWorkspaceOptions.exclude!.toString());
|
|
700
|
-
} else if (isIncludesPresent) {
|
|
701
|
-
message = nls.localizeByDefault("No results found in '{0}' - ",
|
|
702
|
-
this.searchInWorkspaceOptions.include!.toString());
|
|
703
|
-
} else if (isExcludesPresent) {
|
|
704
|
-
message = nls.localizeByDefault("No results found excluding '{0}' - ",
|
|
705
|
-
this.searchInWorkspaceOptions.exclude!.toString());
|
|
706
|
-
} else {
|
|
707
|
-
message = nls.localizeByDefault('No results found') + ' - ';
|
|
708
|
-
}
|
|
709
|
-
// We have to trim here as vscode will always add a trailing " - " string
|
|
710
|
-
return message.substring(0, message.length - 2).trim();
|
|
711
|
-
} else {
|
|
712
|
-
if (this.resultNumber === 1 && this.resultTreeWidget.fileNumber === 1) {
|
|
713
|
-
return nls.localizeByDefault('{0} result in {1} file',
|
|
714
|
-
this.resultNumber.toString(), this.resultTreeWidget.fileNumber.toString());
|
|
715
|
-
} else if (this.resultTreeWidget.fileNumber === 1) {
|
|
716
|
-
return nls.localizeByDefault('{0} results in {1} file',
|
|
717
|
-
this.resultNumber.toString(), this.resultTreeWidget.fileNumber.toString());
|
|
718
|
-
} else if (this.resultTreeWidget.fileNumber > 0) {
|
|
719
|
-
return nls.localizeByDefault('{0} results in {1} files',
|
|
720
|
-
this.resultNumber.toString(), this.resultTreeWidget.fileNumber.toString());
|
|
721
|
-
} else {
|
|
722
|
-
// if fileNumber === 0, return undefined so that `onUpdateRequest()` would not re-render component
|
|
723
|
-
return undefined;
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2018 TypeFox and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { Widget, Message, BaseWidget, Key, StatefulWidget, MessageLoop, KeyCode, codicon } from '@theia/core/lib/browser';
|
|
18
|
+
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
|
19
|
+
import { SearchInWorkspaceResultTreeWidget } from './search-in-workspace-result-tree-widget';
|
|
20
|
+
import { SearchInWorkspaceOptions } from '../common/search-in-workspace-interface';
|
|
21
|
+
import * as React from '@theia/core/shared/react';
|
|
22
|
+
import { createRoot, Root } from '@theia/core/shared/react-dom/client';
|
|
23
|
+
import { Event, Emitter, Disposable } from '@theia/core/lib/common';
|
|
24
|
+
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
|
25
|
+
import { SearchInWorkspaceContextKeyService } from './search-in-workspace-context-key-service';
|
|
26
|
+
import { CancellationTokenSource } from '@theia/core';
|
|
27
|
+
import { ProgressBarFactory } from '@theia/core/lib/browser/progress-bar-factory';
|
|
28
|
+
import { EditorManager } from '@theia/editor/lib/browser';
|
|
29
|
+
import { SearchInWorkspacePreferences } from './search-in-workspace-preferences';
|
|
30
|
+
import { SearchInWorkspaceInput } from './components/search-in-workspace-input';
|
|
31
|
+
import { SearchInWorkspaceTextArea } from './components/search-in-workspace-textarea';
|
|
32
|
+
import { nls } from '@theia/core/lib/common/nls';
|
|
33
|
+
|
|
34
|
+
export interface SearchFieldState {
|
|
35
|
+
className: string;
|
|
36
|
+
enabled: boolean;
|
|
37
|
+
title: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@injectable()
|
|
41
|
+
export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidget {
|
|
42
|
+
|
|
43
|
+
static ID = 'search-in-workspace';
|
|
44
|
+
static LABEL = nls.localizeByDefault('Search');
|
|
45
|
+
|
|
46
|
+
protected matchCaseState: SearchFieldState;
|
|
47
|
+
protected wholeWordState: SearchFieldState;
|
|
48
|
+
protected regExpState: SearchFieldState;
|
|
49
|
+
protected includeIgnoredState: SearchFieldState;
|
|
50
|
+
|
|
51
|
+
protected showSearchDetails = false;
|
|
52
|
+
protected _hasResults = false;
|
|
53
|
+
protected get hasResults(): boolean {
|
|
54
|
+
return this._hasResults;
|
|
55
|
+
}
|
|
56
|
+
protected set hasResults(hasResults: boolean) {
|
|
57
|
+
this.contextKeyService.hasSearchResult.set(hasResults);
|
|
58
|
+
this._hasResults = hasResults;
|
|
59
|
+
}
|
|
60
|
+
protected resultNumber = 0;
|
|
61
|
+
|
|
62
|
+
protected searchFieldContainerIsFocused = false;
|
|
63
|
+
|
|
64
|
+
protected searchInWorkspaceOptions: SearchInWorkspaceOptions;
|
|
65
|
+
|
|
66
|
+
protected searchTerm = '';
|
|
67
|
+
protected replaceTerm = '';
|
|
68
|
+
|
|
69
|
+
private searchRef = React.createRef<SearchInWorkspaceTextArea>();
|
|
70
|
+
private replaceRef = React.createRef<SearchInWorkspaceTextArea>();
|
|
71
|
+
private includeRef = React.createRef<SearchInWorkspaceInput>();
|
|
72
|
+
private excludeRef = React.createRef<SearchInWorkspaceInput>();
|
|
73
|
+
|
|
74
|
+
protected _showReplaceField = false;
|
|
75
|
+
protected get showReplaceField(): boolean {
|
|
76
|
+
return this._showReplaceField;
|
|
77
|
+
}
|
|
78
|
+
protected set showReplaceField(showReplaceField: boolean) {
|
|
79
|
+
this.contextKeyService.replaceActive.set(showReplaceField);
|
|
80
|
+
this._showReplaceField = showReplaceField;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
protected contentNode: HTMLElement;
|
|
84
|
+
protected searchFormContainer: HTMLElement;
|
|
85
|
+
protected resultContainer: HTMLElement;
|
|
86
|
+
|
|
87
|
+
protected readonly onDidUpdateEmitter = new Emitter<void>();
|
|
88
|
+
readonly onDidUpdate: Event<void> = this.onDidUpdateEmitter.event;
|
|
89
|
+
|
|
90
|
+
@inject(SearchInWorkspaceResultTreeWidget) readonly resultTreeWidget: SearchInWorkspaceResultTreeWidget;
|
|
91
|
+
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
|
|
92
|
+
|
|
93
|
+
@inject(SearchInWorkspaceContextKeyService)
|
|
94
|
+
protected readonly contextKeyService: SearchInWorkspaceContextKeyService;
|
|
95
|
+
|
|
96
|
+
@inject(ProgressBarFactory)
|
|
97
|
+
protected readonly progressBarFactory: ProgressBarFactory;
|
|
98
|
+
|
|
99
|
+
@inject(EditorManager) protected readonly editorManager: EditorManager;
|
|
100
|
+
|
|
101
|
+
@inject(SearchInWorkspacePreferences)
|
|
102
|
+
protected readonly searchInWorkspacePreferences: SearchInWorkspacePreferences;
|
|
103
|
+
|
|
104
|
+
protected searchFormContainerRoot: Root;
|
|
105
|
+
|
|
106
|
+
@postConstruct()
|
|
107
|
+
protected init(): void {
|
|
108
|
+
this.id = SearchInWorkspaceWidget.ID;
|
|
109
|
+
this.title.label = SearchInWorkspaceWidget.LABEL;
|
|
110
|
+
this.title.caption = SearchInWorkspaceWidget.LABEL;
|
|
111
|
+
this.title.iconClass = codicon('search');
|
|
112
|
+
this.title.closable = true;
|
|
113
|
+
this.contentNode = document.createElement('div');
|
|
114
|
+
this.contentNode.classList.add('t-siw-search-container');
|
|
115
|
+
this.searchFormContainer = document.createElement('div');
|
|
116
|
+
this.searchFormContainer.classList.add('searchHeader');
|
|
117
|
+
this.contentNode.appendChild(this.searchFormContainer);
|
|
118
|
+
this.searchFormContainerRoot = createRoot(this.searchFormContainer);
|
|
119
|
+
this.node.tabIndex = 0;
|
|
120
|
+
this.node.appendChild(this.contentNode);
|
|
121
|
+
|
|
122
|
+
this.matchCaseState = {
|
|
123
|
+
className: codicon('case-sensitive'),
|
|
124
|
+
enabled: false,
|
|
125
|
+
title: nls.localizeByDefault('Match Case')
|
|
126
|
+
};
|
|
127
|
+
this.wholeWordState = {
|
|
128
|
+
className: codicon('whole-word'),
|
|
129
|
+
enabled: false,
|
|
130
|
+
title: nls.localizeByDefault('Match Whole Word')
|
|
131
|
+
};
|
|
132
|
+
this.regExpState = {
|
|
133
|
+
className: codicon('regex'),
|
|
134
|
+
enabled: false,
|
|
135
|
+
title: nls.localizeByDefault('Use Regular Expression')
|
|
136
|
+
};
|
|
137
|
+
this.includeIgnoredState = {
|
|
138
|
+
className: codicon('eye'),
|
|
139
|
+
enabled: false,
|
|
140
|
+
title: nls.localize('theia/search-in-workspace/includeIgnoredFiles', 'Include Ignored Files')
|
|
141
|
+
};
|
|
142
|
+
this.searchInWorkspaceOptions = {
|
|
143
|
+
matchCase: false,
|
|
144
|
+
matchWholeWord: false,
|
|
145
|
+
useRegExp: false,
|
|
146
|
+
multiline: false,
|
|
147
|
+
includeIgnored: false,
|
|
148
|
+
include: [],
|
|
149
|
+
exclude: [],
|
|
150
|
+
maxResults: 2000
|
|
151
|
+
};
|
|
152
|
+
this.toDispose.push(this.resultTreeWidget.onChange(r => {
|
|
153
|
+
this.hasResults = r.size > 0;
|
|
154
|
+
this.resultNumber = 0;
|
|
155
|
+
const results = Array.from(r.values());
|
|
156
|
+
results.forEach(rootFolder =>
|
|
157
|
+
rootFolder.children.forEach(file => this.resultNumber += file.children.length)
|
|
158
|
+
);
|
|
159
|
+
this.update();
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
this.toDispose.push(this.resultTreeWidget.onFocusInput(b => {
|
|
163
|
+
this.focusInputField();
|
|
164
|
+
}));
|
|
165
|
+
|
|
166
|
+
this.toDispose.push(this.searchInWorkspacePreferences.onPreferenceChanged(e => {
|
|
167
|
+
if (e.preferenceName === 'search.smartCase') {
|
|
168
|
+
this.performSearch();
|
|
169
|
+
}
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
this.toDispose.push(this.resultTreeWidget);
|
|
173
|
+
this.toDispose.push(this.resultTreeWidget.onExpansionChanged(() => {
|
|
174
|
+
this.onDidUpdateEmitter.fire();
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
this.toDispose.push(this.progressBarFactory({ container: this.node, insertMode: 'prepend', locationId: 'search' }));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
storeState(): object {
|
|
181
|
+
return {
|
|
182
|
+
matchCaseState: this.matchCaseState,
|
|
183
|
+
wholeWordState: this.wholeWordState,
|
|
184
|
+
regExpState: this.regExpState,
|
|
185
|
+
includeIgnoredState: this.includeIgnoredState,
|
|
186
|
+
showSearchDetails: this.showSearchDetails,
|
|
187
|
+
searchInWorkspaceOptions: this.searchInWorkspaceOptions,
|
|
188
|
+
searchTerm: this.searchTerm,
|
|
189
|
+
replaceTerm: this.replaceTerm,
|
|
190
|
+
showReplaceField: this.showReplaceField,
|
|
191
|
+
searchHistoryState: this.searchRef.current?.state,
|
|
192
|
+
replaceHistoryState: this.replaceRef.current?.state,
|
|
193
|
+
includeHistoryState: this.includeRef.current?.state,
|
|
194
|
+
excludeHistoryState: this.excludeRef.current?.state,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
199
|
+
restoreState(oldState: any): void {
|
|
200
|
+
this.matchCaseState = oldState.matchCaseState;
|
|
201
|
+
this.wholeWordState = oldState.wholeWordState;
|
|
202
|
+
this.regExpState = oldState.regExpState;
|
|
203
|
+
this.includeIgnoredState = oldState.includeIgnoredState;
|
|
204
|
+
// Override the title of the restored state, as we could have changed languages in between
|
|
205
|
+
this.matchCaseState.title = nls.localizeByDefault('Match Case');
|
|
206
|
+
this.wholeWordState.title = nls.localizeByDefault('Match Whole Word');
|
|
207
|
+
this.regExpState.title = nls.localizeByDefault('Use Regular Expression');
|
|
208
|
+
this.includeIgnoredState.title = nls.localize('theia/search-in-workspace/includeIgnoredFiles', 'Include Ignored Files');
|
|
209
|
+
this.showSearchDetails = oldState.showSearchDetails;
|
|
210
|
+
this.searchInWorkspaceOptions = oldState.searchInWorkspaceOptions;
|
|
211
|
+
this.searchTerm = oldState.searchTerm;
|
|
212
|
+
this.replaceTerm = oldState.replaceTerm;
|
|
213
|
+
this.showReplaceField = oldState.showReplaceField;
|
|
214
|
+
this.resultTreeWidget.replaceTerm = this.replaceTerm;
|
|
215
|
+
this.resultTreeWidget.showReplaceButtons = this.showReplaceField;
|
|
216
|
+
this.searchRef.current?.setState(oldState.searchHistoryState);
|
|
217
|
+
this.replaceRef.current?.setState(oldState.replaceHistoryState);
|
|
218
|
+
this.includeRef.current?.setState(oldState.includeHistoryState);
|
|
219
|
+
this.excludeRef.current?.setState(oldState.excludeHistoryState);
|
|
220
|
+
this.refresh();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
findInFolder(uris: string[]): void {
|
|
224
|
+
this.showSearchDetails = true;
|
|
225
|
+
const values = Array.from(new Set(uris.map(uri => `${uri}/**`)));
|
|
226
|
+
const value = values.join(', ');
|
|
227
|
+
this.searchInWorkspaceOptions.include = values;
|
|
228
|
+
if (this.includeRef.current) {
|
|
229
|
+
this.includeRef.current.value = value;
|
|
230
|
+
this.includeRef.current.addToHistory();
|
|
231
|
+
}
|
|
232
|
+
this.update();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Update the search term and input field.
|
|
237
|
+
* @param term the search term.
|
|
238
|
+
* @param showReplaceField controls if the replace field should be displayed.
|
|
239
|
+
*/
|
|
240
|
+
updateSearchTerm(term: string, showReplaceField?: boolean): void {
|
|
241
|
+
this.searchTerm = term;
|
|
242
|
+
if (this.searchRef.current) {
|
|
243
|
+
this.searchRef.current.value = term;
|
|
244
|
+
this.searchRef.current.addToHistory();
|
|
245
|
+
}
|
|
246
|
+
if (showReplaceField) {
|
|
247
|
+
this.showReplaceField = true;
|
|
248
|
+
}
|
|
249
|
+
this.refresh();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
hasResultList(): boolean {
|
|
253
|
+
return this.hasResults;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
hasSearchTerm(): boolean {
|
|
257
|
+
return this.searchTerm !== '';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
refresh(): void {
|
|
261
|
+
this.performSearch();
|
|
262
|
+
this.update();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
getCancelIndicator(): CancellationTokenSource | undefined {
|
|
266
|
+
return this.resultTreeWidget.cancelIndicator;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
collapseAll(): void {
|
|
270
|
+
this.resultTreeWidget.collapseAll();
|
|
271
|
+
this.update();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
expandAll(): void {
|
|
275
|
+
this.resultTreeWidget.expandAll();
|
|
276
|
+
this.update();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
areResultsCollapsed(): boolean {
|
|
280
|
+
return this.resultTreeWidget.areResultsCollapsed();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
clear(): void {
|
|
284
|
+
this.searchTerm = '';
|
|
285
|
+
this.replaceTerm = '';
|
|
286
|
+
this.searchInWorkspaceOptions.include = [];
|
|
287
|
+
this.searchInWorkspaceOptions.exclude = [];
|
|
288
|
+
this.includeIgnoredState.enabled = false;
|
|
289
|
+
this.matchCaseState.enabled = false;
|
|
290
|
+
this.wholeWordState.enabled = false;
|
|
291
|
+
this.regExpState.enabled = false;
|
|
292
|
+
if (this.searchRef.current) {
|
|
293
|
+
this.searchRef.current.value = '';
|
|
294
|
+
}
|
|
295
|
+
if (this.replaceRef.current) {
|
|
296
|
+
this.replaceRef.current.value = '';
|
|
297
|
+
}
|
|
298
|
+
if (this.includeRef.current) {
|
|
299
|
+
this.includeRef.current.value = '';
|
|
300
|
+
}
|
|
301
|
+
if (this.excludeRef.current) {
|
|
302
|
+
this.excludeRef.current.value = '';
|
|
303
|
+
}
|
|
304
|
+
this.performSearch();
|
|
305
|
+
this.update();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
protected override onAfterAttach(msg: Message): void {
|
|
309
|
+
super.onAfterAttach(msg);
|
|
310
|
+
this.searchFormContainerRoot.render(<React.Fragment>{this.renderSearchHeader()}{this.renderSearchInfo()}</React.Fragment>);
|
|
311
|
+
Widget.attach(this.resultTreeWidget, this.contentNode);
|
|
312
|
+
this.toDisposeOnDetach.push(Disposable.create(() => {
|
|
313
|
+
Widget.detach(this.resultTreeWidget);
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
protected override onUpdateRequest(msg: Message): void {
|
|
318
|
+
super.onUpdateRequest(msg);
|
|
319
|
+
const searchInfo = this.renderSearchInfo();
|
|
320
|
+
if (searchInfo) {
|
|
321
|
+
this.searchFormContainerRoot.render(<React.Fragment>{this.renderSearchHeader()}{searchInfo}</React.Fragment>);
|
|
322
|
+
this.onDidUpdateEmitter.fire(undefined);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
protected override onResize(msg: Widget.ResizeMessage): void {
|
|
327
|
+
super.onResize(msg);
|
|
328
|
+
this.searchRef.current?.forceUpdate();
|
|
329
|
+
this.replaceRef.current?.forceUpdate();
|
|
330
|
+
MessageLoop.sendMessage(this.resultTreeWidget, Widget.ResizeMessage.UnknownSize);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
protected override onAfterShow(msg: Message): void {
|
|
334
|
+
super.onAfterShow(msg);
|
|
335
|
+
this.focusInputField();
|
|
336
|
+
this.contextKeyService.searchViewletVisible.set(true);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
protected override onAfterHide(msg: Message): void {
|
|
340
|
+
super.onAfterHide(msg);
|
|
341
|
+
this.contextKeyService.searchViewletVisible.set(false);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
protected override onActivateRequest(msg: Message): void {
|
|
345
|
+
super.onActivateRequest(msg);
|
|
346
|
+
this.focusInputField();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
protected focusInputField(): void {
|
|
350
|
+
const f = document.getElementById('search-input-field');
|
|
351
|
+
if (f) {
|
|
352
|
+
(f as HTMLInputElement).focus();
|
|
353
|
+
(f as HTMLInputElement).select();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
protected renderSearchHeader(): React.ReactNode {
|
|
358
|
+
const searchAndReplaceContainer = this.renderSearchAndReplace();
|
|
359
|
+
const searchDetails = this.renderSearchDetails();
|
|
360
|
+
return <div>{searchAndReplaceContainer}{searchDetails}</div>;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
protected renderSearchAndReplace(): React.ReactNode {
|
|
364
|
+
const toggleContainer = this.renderReplaceFieldToggle();
|
|
365
|
+
const searchField = this.renderSearchField();
|
|
366
|
+
const replaceField = this.renderReplaceField();
|
|
367
|
+
return <div className='search-and-replace-container'>
|
|
368
|
+
{toggleContainer}
|
|
369
|
+
<div className='search-and-replace-fields'>
|
|
370
|
+
{searchField}
|
|
371
|
+
{replaceField}
|
|
372
|
+
</div>
|
|
373
|
+
</div>;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
protected renderReplaceFieldToggle(): React.ReactNode {
|
|
377
|
+
const toggle = <span className={codicon(this.showReplaceField ? 'chevron-down' : 'chevron-right')}></span>;
|
|
378
|
+
return <div
|
|
379
|
+
title={nls.localizeByDefault('Toggle Replace')}
|
|
380
|
+
className='replace-toggle'
|
|
381
|
+
tabIndex={0}
|
|
382
|
+
onClick={e => {
|
|
383
|
+
const elArr = document.getElementsByClassName('replace-toggle');
|
|
384
|
+
if (elArr && elArr.length > 0) {
|
|
385
|
+
(elArr[0] as HTMLElement).focus();
|
|
386
|
+
}
|
|
387
|
+
this.showReplaceField = !this.showReplaceField;
|
|
388
|
+
this.resultTreeWidget.showReplaceButtons = this.showReplaceField;
|
|
389
|
+
this.update();
|
|
390
|
+
}}>
|
|
391
|
+
{toggle}
|
|
392
|
+
</div>;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
protected renderNotification(): React.ReactNode {
|
|
396
|
+
if (this.workspaceService.tryGetRoots().length <= 0 && this.editorManager.all.length <= 0) {
|
|
397
|
+
return <div className='search-notification show'>
|
|
398
|
+
<div>{nls.localize('theia/search-in-workspace/noFolderSpecified', 'You have not opened or specified a folder. Only open files are currently searched.')}</div>
|
|
399
|
+
</div>;
|
|
400
|
+
}
|
|
401
|
+
return <div
|
|
402
|
+
className={`search-notification ${this.searchInWorkspaceOptions.maxResults && this.resultNumber >= this.searchInWorkspaceOptions.maxResults ? 'show' : ''}`}>
|
|
403
|
+
<div>{nls.localize('theia/search-in-workspace/resultSubset',
|
|
404
|
+
'This is only a subset of all results. Use a more specific search term to narrow down the result list.')}</div>
|
|
405
|
+
</div>;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
protected readonly focusSearchFieldContainer = () => this.doFocusSearchFieldContainer();
|
|
409
|
+
protected doFocusSearchFieldContainer(): void {
|
|
410
|
+
this.searchFieldContainerIsFocused = true;
|
|
411
|
+
this.update();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
protected readonly blurSearchFieldContainer = () => this.doBlurSearchFieldContainer();
|
|
415
|
+
protected doBlurSearchFieldContainer(): void {
|
|
416
|
+
this.searchFieldContainerIsFocused = false;
|
|
417
|
+
this.update();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private _searchTimeout: number;
|
|
421
|
+
protected readonly search = (e: React.KeyboardEvent) => {
|
|
422
|
+
e.persist();
|
|
423
|
+
const searchOnType = this.searchInWorkspacePreferences['search.searchOnType'];
|
|
424
|
+
if (searchOnType) {
|
|
425
|
+
const delay = this.searchInWorkspacePreferences['search.searchOnTypeDebouncePeriod'] || 0;
|
|
426
|
+
window.clearTimeout(this._searchTimeout);
|
|
427
|
+
this._searchTimeout = window.setTimeout(() => this.doSearch(e), delay);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
protected readonly onKeyDownSearch = (e: React.KeyboardEvent) => {
|
|
432
|
+
if (Key.ENTER.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode) {
|
|
433
|
+
this.searchTerm = (e.target as HTMLInputElement).value;
|
|
434
|
+
this.performSearch();
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
protected doSearch(e: React.KeyboardEvent): void {
|
|
439
|
+
if (e.target) {
|
|
440
|
+
const searchValue = (e.target as HTMLInputElement).value;
|
|
441
|
+
if (this.searchTerm === searchValue && Key.ENTER.keyCode !== KeyCode.createKeyCode(e.nativeEvent).key?.keyCode) {
|
|
442
|
+
return;
|
|
443
|
+
} else {
|
|
444
|
+
this.searchTerm = searchValue;
|
|
445
|
+
this.performSearch();
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
protected performSearch(): void {
|
|
451
|
+
const searchOptions: SearchInWorkspaceOptions = {
|
|
452
|
+
...this.searchInWorkspaceOptions,
|
|
453
|
+
followSymlinks: this.shouldFollowSymlinks(),
|
|
454
|
+
matchCase: this.shouldMatchCase(),
|
|
455
|
+
multiline: this.searchTerm.includes('\n')
|
|
456
|
+
};
|
|
457
|
+
this.resultTreeWidget.search(this.searchTerm, searchOptions);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
protected shouldFollowSymlinks(): boolean {
|
|
461
|
+
return this.searchInWorkspacePreferences['search.followSymlinks'];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Determine if search should be case sensitive.
|
|
466
|
+
*/
|
|
467
|
+
protected shouldMatchCase(): boolean {
|
|
468
|
+
if (this.matchCaseState.enabled) {
|
|
469
|
+
return this.matchCaseState.enabled;
|
|
470
|
+
}
|
|
471
|
+
// search.smartCase makes siw search case-sensitive if the search term contains uppercase letter(s).
|
|
472
|
+
return (
|
|
473
|
+
!!this.searchInWorkspacePreferences['search.smartCase']
|
|
474
|
+
&& this.searchTerm !== this.searchTerm.toLowerCase()
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
protected renderSearchField(): React.ReactNode {
|
|
479
|
+
const input = <SearchInWorkspaceTextArea
|
|
480
|
+
id='search-input-field'
|
|
481
|
+
className='theia-input'
|
|
482
|
+
title={SearchInWorkspaceWidget.LABEL}
|
|
483
|
+
placeholder={SearchInWorkspaceWidget.LABEL}
|
|
484
|
+
defaultValue={this.searchTerm}
|
|
485
|
+
autoComplete='off'
|
|
486
|
+
onKeyUp={this.search}
|
|
487
|
+
onKeyDown={this.onKeyDownSearch}
|
|
488
|
+
onFocus={this.handleFocusSearchInputBox}
|
|
489
|
+
onBlur={this.handleBlurSearchInputBox}
|
|
490
|
+
ref={this.searchRef}
|
|
491
|
+
/>;
|
|
492
|
+
const notification = this.renderNotification();
|
|
493
|
+
const optionContainer = this.renderOptionContainer();
|
|
494
|
+
const tooMany = this.searchInWorkspaceOptions.maxResults && this.resultNumber >= this.searchInWorkspaceOptions.maxResults ? 'tooManyResults' : '';
|
|
495
|
+
const className = `search-field-container ${tooMany} ${this.searchFieldContainerIsFocused ? 'focused' : ''}`;
|
|
496
|
+
return <div className={className}>
|
|
497
|
+
<div className='search-field' tabIndex={-1} onFocus={this.focusSearchFieldContainer} onBlur={this.blurSearchFieldContainer}>
|
|
498
|
+
{input}
|
|
499
|
+
{optionContainer}
|
|
500
|
+
</div>
|
|
501
|
+
{notification}
|
|
502
|
+
</div>;
|
|
503
|
+
}
|
|
504
|
+
protected handleFocusSearchInputBox = (event: React.FocusEvent<HTMLTextAreaElement>) => {
|
|
505
|
+
event.target.placeholder = `${SearchInWorkspaceWidget.LABEL} (⇅ ${nls.localizeByDefault('for history')})`;
|
|
506
|
+
this.contextKeyService.setSearchInputBoxFocus(true);
|
|
507
|
+
};
|
|
508
|
+
protected handleBlurSearchInputBox = (event: React.FocusEvent<HTMLTextAreaElement>) => {
|
|
509
|
+
event.target.placeholder = SearchInWorkspaceWidget.LABEL;
|
|
510
|
+
this.contextKeyService.setSearchInputBoxFocus(false);
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
protected readonly updateReplaceTerm = (e: React.KeyboardEvent) => this.doUpdateReplaceTerm(e);
|
|
514
|
+
protected doUpdateReplaceTerm(e: React.KeyboardEvent): void {
|
|
515
|
+
if (e.target) {
|
|
516
|
+
this.replaceTerm = (e.target as HTMLInputElement).value;
|
|
517
|
+
this.resultTreeWidget.replaceTerm = this.replaceTerm;
|
|
518
|
+
if (KeyCode.createKeyCode(e.nativeEvent).key?.keyCode === Key.ENTER.keyCode) { this.performSearch(); }
|
|
519
|
+
this.update();
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
protected renderReplaceField(): React.ReactNode {
|
|
524
|
+
const replaceAllButtonContainer = this.renderReplaceAllButtonContainer();
|
|
525
|
+
const replace = nls.localizeByDefault('Replace');
|
|
526
|
+
return <div className={`replace-field${this.showReplaceField ? '' : ' hidden'}`}>
|
|
527
|
+
<SearchInWorkspaceTextArea
|
|
528
|
+
id='replace-input-field'
|
|
529
|
+
className='theia-input'
|
|
530
|
+
title={replace}
|
|
531
|
+
placeholder={replace}
|
|
532
|
+
defaultValue={this.replaceTerm}
|
|
533
|
+
autoComplete='off'
|
|
534
|
+
onKeyUp={this.updateReplaceTerm}
|
|
535
|
+
onFocus={this.handleFocusReplaceInputBox}
|
|
536
|
+
onBlur={this.handleBlurReplaceInputBox}
|
|
537
|
+
ref={this.replaceRef}
|
|
538
|
+
/>
|
|
539
|
+
{replaceAllButtonContainer}
|
|
540
|
+
</div>;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
protected handleFocusReplaceInputBox = (event: React.FocusEvent<HTMLTextAreaElement>) => {
|
|
544
|
+
event.target.placeholder = `${nls.localizeByDefault('Replace')} (⇅ ${nls.localizeByDefault('for history')})`;
|
|
545
|
+
this.contextKeyService.setReplaceInputBoxFocus(true);
|
|
546
|
+
};
|
|
547
|
+
protected handleBlurReplaceInputBox = (event: React.FocusEvent<HTMLTextAreaElement>) => {
|
|
548
|
+
event.target.placeholder = nls.localizeByDefault('Replace');
|
|
549
|
+
this.contextKeyService.setReplaceInputBoxFocus(false);
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
protected renderReplaceAllButtonContainer(): React.ReactNode {
|
|
553
|
+
// The `Replace All` button is enabled if there is a search term present with results.
|
|
554
|
+
const enabled: boolean = this.searchTerm !== '' && this.resultNumber > 0;
|
|
555
|
+
return <div className='replace-all-button-container'>
|
|
556
|
+
<span
|
|
557
|
+
title={nls.localizeByDefault('Replace All')}
|
|
558
|
+
className={`${codicon('replace-all', true)} ${enabled ? ' ' : ' disabled'}`}
|
|
559
|
+
onClick={() => {
|
|
560
|
+
if (enabled) {
|
|
561
|
+
this.resultTreeWidget.replace(undefined);
|
|
562
|
+
}
|
|
563
|
+
}}>
|
|
564
|
+
</span>
|
|
565
|
+
</div>;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
protected renderOptionContainer(): React.ReactNode {
|
|
569
|
+
const matchCaseOption = this.renderOptionElement(this.matchCaseState);
|
|
570
|
+
const wholeWordOption = this.renderOptionElement(this.wholeWordState);
|
|
571
|
+
const regexOption = this.renderOptionElement(this.regExpState);
|
|
572
|
+
const includeIgnoredOption = this.renderOptionElement(this.includeIgnoredState);
|
|
573
|
+
return <div className='option-buttons'>{matchCaseOption}{wholeWordOption}{regexOption}{includeIgnoredOption}</div>;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
protected renderOptionElement(opt: SearchFieldState): React.ReactNode {
|
|
577
|
+
return <span
|
|
578
|
+
className={`${opt.className} option action-label ${opt.enabled ? 'enabled' : ''}`}
|
|
579
|
+
title={opt.title}
|
|
580
|
+
onClick={() => this.handleOptionClick(opt)}></span>;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
protected handleOptionClick(option: SearchFieldState): void {
|
|
584
|
+
option.enabled = !option.enabled;
|
|
585
|
+
this.updateSearchOptions();
|
|
586
|
+
this.searchFieldContainerIsFocused = true;
|
|
587
|
+
this.performSearch();
|
|
588
|
+
this.update();
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
protected updateSearchOptions(): void {
|
|
592
|
+
this.searchInWorkspaceOptions.matchCase = this.matchCaseState.enabled;
|
|
593
|
+
this.searchInWorkspaceOptions.matchWholeWord = this.wholeWordState.enabled;
|
|
594
|
+
this.searchInWorkspaceOptions.useRegExp = this.regExpState.enabled;
|
|
595
|
+
this.searchInWorkspaceOptions.includeIgnored = this.includeIgnoredState.enabled;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
protected renderSearchDetails(): React.ReactNode {
|
|
599
|
+
const expandButton = this.renderExpandGlobFieldsButton();
|
|
600
|
+
const globFieldContainer = this.renderGlobFieldContainer();
|
|
601
|
+
return <div className='search-details'>{expandButton}{globFieldContainer}</div>;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
protected renderGlobFieldContainer(): React.ReactNode {
|
|
605
|
+
const includeField = this.renderGlobField('include');
|
|
606
|
+
const excludeField = this.renderGlobField('exclude');
|
|
607
|
+
return <div className={`glob-field-container${!this.showSearchDetails ? ' hidden' : ''}`}>{includeField}{excludeField}</div>;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
protected renderExpandGlobFieldsButton(): React.ReactNode {
|
|
611
|
+
return <div className='button-container'>
|
|
612
|
+
<span
|
|
613
|
+
title={nls.localizeByDefault('Toggle Search Details')}
|
|
614
|
+
className={codicon('ellipsis')}
|
|
615
|
+
onClick={() => {
|
|
616
|
+
this.showSearchDetails = !this.showSearchDetails;
|
|
617
|
+
this.update();
|
|
618
|
+
}}></span>
|
|
619
|
+
</div>;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
protected renderGlobField(kind: 'include' | 'exclude'): React.ReactNode {
|
|
623
|
+
const currentValue = this.searchInWorkspaceOptions[kind];
|
|
624
|
+
const value = currentValue && currentValue.join(', ') || '';
|
|
625
|
+
return <div className='glob-field'>
|
|
626
|
+
<div className='label'>{nls.localizeByDefault('files to ' + kind)}</div>
|
|
627
|
+
<SearchInWorkspaceInput
|
|
628
|
+
className='theia-input'
|
|
629
|
+
type='text'
|
|
630
|
+
size={1}
|
|
631
|
+
defaultValue={value}
|
|
632
|
+
autoComplete='off'
|
|
633
|
+
id={kind + '-glob-field'}
|
|
634
|
+
placeholder={kind === 'include'
|
|
635
|
+
? nls.localizeByDefault('e.g. *.ts, src/**/include')
|
|
636
|
+
: nls.localizeByDefault('e.g. *.ts, src/**/exclude')
|
|
637
|
+
}
|
|
638
|
+
onKeyUp={e => {
|
|
639
|
+
if (e.target) {
|
|
640
|
+
const targetValue = (e.target as HTMLInputElement).value || '';
|
|
641
|
+
let shouldSearch = Key.ENTER.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode;
|
|
642
|
+
const currentOptions = (this.searchInWorkspaceOptions[kind] || []).slice().map(s => s.trim()).sort();
|
|
643
|
+
const candidateOptions = this.splitOnComma(targetValue).map(s => s.trim()).sort();
|
|
644
|
+
const sameAs = (left: string[], right: string[]) => {
|
|
645
|
+
if (left.length !== right.length) {
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
for (let i = 0; i < left.length; i++) {
|
|
649
|
+
if (left[i] !== right[i]) {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return true;
|
|
654
|
+
};
|
|
655
|
+
if (!sameAs(currentOptions, candidateOptions)) {
|
|
656
|
+
this.searchInWorkspaceOptions[kind] = this.splitOnComma(targetValue);
|
|
657
|
+
shouldSearch = true;
|
|
658
|
+
}
|
|
659
|
+
if (shouldSearch) {
|
|
660
|
+
this.performSearch();
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}}
|
|
664
|
+
onFocus={kind === 'include' ? this.handleFocusIncludesInputBox : this.handleFocusExcludesInputBox}
|
|
665
|
+
onBlur={kind === 'include' ? this.handleBlurIncludesInputBox : this.handleBlurExcludesInputBox}
|
|
666
|
+
ref={kind === 'include' ? this.includeRef : this.excludeRef}
|
|
667
|
+
/>
|
|
668
|
+
</div>;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
protected handleFocusIncludesInputBox = () => this.contextKeyService.setPatternIncludesInputBoxFocus(true);
|
|
672
|
+
protected handleBlurIncludesInputBox = () => this.contextKeyService.setPatternIncludesInputBoxFocus(false);
|
|
673
|
+
|
|
674
|
+
protected handleFocusExcludesInputBox = () => this.contextKeyService.setPatternExcludesInputBoxFocus(true);
|
|
675
|
+
protected handleBlurExcludesInputBox = () => this.contextKeyService.setPatternExcludesInputBoxFocus(false);
|
|
676
|
+
|
|
677
|
+
protected splitOnComma(patterns: string): string[] {
|
|
678
|
+
return patterns.length > 0 ? patterns.split(',').map(s => s.trim()) : [];
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
protected renderSearchInfo(): React.ReactNode {
|
|
682
|
+
const message = this.getSearchResultMessage() || '';
|
|
683
|
+
return <div className='search-info'>{message}</div>;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
protected getSearchResultMessage(): string | undefined {
|
|
687
|
+
|
|
688
|
+
if (!this.searchTerm) {
|
|
689
|
+
return undefined;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (this.resultNumber === 0) {
|
|
693
|
+
const isIncludesPresent = this.searchInWorkspaceOptions.include && this.searchInWorkspaceOptions.include.length > 0;
|
|
694
|
+
const isExcludesPresent = this.searchInWorkspaceOptions.exclude && this.searchInWorkspaceOptions.exclude.length > 0;
|
|
695
|
+
|
|
696
|
+
let message: string;
|
|
697
|
+
if (isIncludesPresent && isExcludesPresent) {
|
|
698
|
+
message = nls.localizeByDefault("No results found in '{0}' excluding '{1}' - ",
|
|
699
|
+
this.searchInWorkspaceOptions.include!.toString(), this.searchInWorkspaceOptions.exclude!.toString());
|
|
700
|
+
} else if (isIncludesPresent) {
|
|
701
|
+
message = nls.localizeByDefault("No results found in '{0}' - ",
|
|
702
|
+
this.searchInWorkspaceOptions.include!.toString());
|
|
703
|
+
} else if (isExcludesPresent) {
|
|
704
|
+
message = nls.localizeByDefault("No results found excluding '{0}' - ",
|
|
705
|
+
this.searchInWorkspaceOptions.exclude!.toString());
|
|
706
|
+
} else {
|
|
707
|
+
message = nls.localizeByDefault('No results found') + ' - ';
|
|
708
|
+
}
|
|
709
|
+
// We have to trim here as vscode will always add a trailing " - " string
|
|
710
|
+
return message.substring(0, message.length - 2).trim();
|
|
711
|
+
} else {
|
|
712
|
+
if (this.resultNumber === 1 && this.resultTreeWidget.fileNumber === 1) {
|
|
713
|
+
return nls.localizeByDefault('{0} result in {1} file',
|
|
714
|
+
this.resultNumber.toString(), this.resultTreeWidget.fileNumber.toString());
|
|
715
|
+
} else if (this.resultTreeWidget.fileNumber === 1) {
|
|
716
|
+
return nls.localizeByDefault('{0} results in {1} file',
|
|
717
|
+
this.resultNumber.toString(), this.resultTreeWidget.fileNumber.toString());
|
|
718
|
+
} else if (this.resultTreeWidget.fileNumber > 0) {
|
|
719
|
+
return nls.localizeByDefault('{0} results in {1} files',
|
|
720
|
+
this.resultNumber.toString(), this.resultTreeWidget.fileNumber.toString());
|
|
721
|
+
} else {
|
|
722
|
+
// if fileNumber === 0, return undefined so that `onUpdateRequest()` would not re-render component
|
|
723
|
+
return undefined;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|