@theia/search-in-workspace 1.45.0 → 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.
Files changed (58) hide show
  1. package/README.md +40 -40
  2. package/lib/browser/components/search-in-workspace-input.d.ts +39 -39
  3. package/lib/browser/components/search-in-workspace-input.js +123 -123
  4. package/lib/browser/components/search-in-workspace-textarea.d.ts +39 -39
  5. package/lib/browser/components/search-in-workspace-textarea.js +130 -130
  6. package/lib/browser/search-in-workspace-context-key-service.d.ts +23 -23
  7. package/lib/browser/search-in-workspace-context-key-service.js +90 -90
  8. package/lib/browser/search-in-workspace-factory.d.ts +10 -10
  9. package/lib/browser/search-in-workspace-factory.js +68 -68
  10. package/lib/browser/search-in-workspace-frontend-contribution.d.ts +57 -55
  11. package/lib/browser/search-in-workspace-frontend-contribution.d.ts.map +1 -1
  12. package/lib/browser/search-in-workspace-frontend-contribution.js +516 -482
  13. package/lib/browser/search-in-workspace-frontend-contribution.js.map +1 -1
  14. package/lib/browser/search-in-workspace-frontend-module.d.ts +6 -6
  15. package/lib/browser/search-in-workspace-frontend-module.js +71 -71
  16. package/lib/browser/search-in-workspace-label-provider.d.ts +9 -9
  17. package/lib/browser/search-in-workspace-label-provider.js +57 -57
  18. package/lib/browser/search-in-workspace-preferences.d.ts +17 -17
  19. package/lib/browser/search-in-workspace-preferences.js +87 -87
  20. package/lib/browser/search-in-workspace-result-tree-widget.d.ts +259 -255
  21. package/lib/browser/search-in-workspace-result-tree-widget.d.ts.map +1 -1
  22. package/lib/browser/search-in-workspace-result-tree-widget.js +1172 -1099
  23. package/lib/browser/search-in-workspace-result-tree-widget.js.map +1 -1
  24. package/lib/browser/search-in-workspace-service.d.ts +35 -35
  25. package/lib/browser/search-in-workspace-service.js +158 -158
  26. package/lib/browser/search-in-workspace-widget.d.ts +121 -121
  27. package/lib/browser/search-in-workspace-widget.js +629 -629
  28. package/lib/browser/search-layout-migrations.d.ts +5 -5
  29. package/lib/browser/search-layout-migrations.js +64 -64
  30. package/lib/common/search-in-workspace-interface.d.ts +116 -116
  31. package/lib/common/search-in-workspace-interface.js +35 -35
  32. package/lib/node/ripgrep-search-in-workspace-server.d.ts +94 -94
  33. package/lib/node/ripgrep-search-in-workspace-server.js +430 -430
  34. package/lib/node/ripgrep-search-in-workspace-server.js.map +1 -1
  35. package/lib/node/ripgrep-search-in-workspace-server.slow-spec.d.ts +1 -1
  36. package/lib/node/ripgrep-search-in-workspace-server.slow-spec.js +899 -899
  37. package/lib/node/ripgrep-search-in-workspace-server.slow-spec.js.map +1 -1
  38. package/lib/node/search-in-workspace-backend-module.d.ts +3 -3
  39. package/lib/node/search-in-workspace-backend-module.js +32 -32
  40. package/package.json +9 -9
  41. package/src/browser/components/search-in-workspace-input.tsx +139 -139
  42. package/src/browser/components/search-in-workspace-textarea.tsx +153 -153
  43. package/src/browser/search-in-workspace-context-key-service.ts +93 -93
  44. package/src/browser/search-in-workspace-factory.ts +59 -59
  45. package/src/browser/search-in-workspace-frontend-contribution.ts +510 -474
  46. package/src/browser/search-in-workspace-frontend-module.ts +83 -83
  47. package/src/browser/search-in-workspace-label-provider.ts +48 -48
  48. package/src/browser/search-in-workspace-preferences.ts +96 -96
  49. package/src/browser/search-in-workspace-result-tree-widget.tsx +1318 -1245
  50. package/src/browser/search-in-workspace-service.ts +152 -152
  51. package/src/browser/search-in-workspace-widget.tsx +727 -727
  52. package/src/browser/search-layout-migrations.ts +53 -53
  53. package/src/browser/styles/index.css +400 -400
  54. package/src/browser/styles/search.svg +6 -6
  55. package/src/common/search-in-workspace-interface.ts +153 -153
  56. package/src/node/ripgrep-search-in-workspace-server.slow-spec.ts +1073 -1073
  57. package/src/node/ripgrep-search-in-workspace-server.ts +490 -490
  58. 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
+ }