@theia/search-in-workspace 1.40.1 → 1.42.0

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 (28) hide show
  1. package/lib/browser/components/search-in-workspace-textarea.d.ts +40 -0
  2. package/lib/browser/components/search-in-workspace-textarea.d.ts.map +1 -0
  3. package/lib/browser/components/search-in-workspace-textarea.js +131 -0
  4. package/lib/browser/components/search-in-workspace-textarea.js.map +1 -0
  5. package/lib/browser/search-in-workspace-result-tree-widget.d.ts +1 -0
  6. package/lib/browser/search-in-workspace-result-tree-widget.d.ts.map +1 -1
  7. package/lib/browser/search-in-workspace-result-tree-widget.js +43 -31
  8. package/lib/browser/search-in-workspace-result-tree-widget.js.map +1 -1
  9. package/lib/browser/search-in-workspace-widget.d.ts.map +1 -1
  10. package/lib/browser/search-in-workspace-widget.js +9 -3
  11. package/lib/browser/search-in-workspace-widget.js.map +1 -1
  12. package/lib/common/search-in-workspace-interface.d.ts +4 -0
  13. package/lib/common/search-in-workspace-interface.d.ts.map +1 -1
  14. package/lib/common/search-in-workspace-interface.js.map +1 -1
  15. package/lib/node/ripgrep-search-in-workspace-server.d.ts +2 -2
  16. package/lib/node/ripgrep-search-in-workspace-server.d.ts.map +1 -1
  17. package/lib/node/ripgrep-search-in-workspace-server.js +15 -8
  18. package/lib/node/ripgrep-search-in-workspace-server.js.map +1 -1
  19. package/lib/node/ripgrep-search-in-workspace-server.slow-spec.js +10 -10
  20. package/lib/node/ripgrep-search-in-workspace-server.slow-spec.js.map +1 -1
  21. package/package.json +11 -10
  22. package/src/browser/components/search-in-workspace-textarea.tsx +153 -0
  23. package/src/browser/search-in-workspace-result-tree-widget.tsx +53 -39
  24. package/src/browser/search-in-workspace-widget.tsx +10 -9
  25. package/src/browser/styles/index.css +30 -15
  26. package/src/common/search-in-workspace-interface.ts +4 -0
  27. package/src/node/ripgrep-search-in-workspace-server.slow-spec.ts +10 -10
  28. package/src/node/ripgrep-search-in-workspace-server.ts +16 -8
@@ -0,0 +1,153 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2021 Ericsson 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 { Key, KeyCode } from '@theia/core/lib/browser';
18
+ import * as React from '@theia/core/shared/react';
19
+ import TextareaAutosize from 'react-autosize-textarea';
20
+ import debounce = require('@theia/core/shared/lodash.debounce');
21
+
22
+ interface HistoryState {
23
+ history: string[];
24
+ index: number;
25
+ };
26
+ type TextareaAttributes = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
27
+
28
+ export class SearchInWorkspaceTextArea extends React.Component<TextareaAttributes, HistoryState> {
29
+ static LIMIT = 100;
30
+
31
+ private textarea = React.createRef<HTMLTextAreaElement>();
32
+
33
+ constructor(props: TextareaAttributes) {
34
+ super(props);
35
+ this.state = {
36
+ history: [],
37
+ index: 0,
38
+ };
39
+ }
40
+
41
+ updateState(index: number, history?: string[]): void {
42
+ this.value = history ? history[index] : this.state.history[index];
43
+ this.setState(prevState => {
44
+ const newState = {
45
+ ...prevState,
46
+ index,
47
+ };
48
+ if (history) {
49
+ newState.history = history;
50
+ }
51
+ return newState;
52
+ });
53
+ }
54
+
55
+ get value(): string {
56
+ return this.textarea.current?.value ?? '';
57
+ }
58
+
59
+ set value(value: string) {
60
+ if (this.textarea.current) {
61
+ this.textarea.current.value = value;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Handle history navigation without overriding the parent's onKeyDown handler, if any.
67
+ */
68
+ protected readonly onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>): void => {
69
+ // Navigate history only when cursor is at first or last position of the textarea
70
+ if (Key.ARROW_UP.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode && e.currentTarget.selectionStart === 0) {
71
+ e.preventDefault();
72
+ this.previousValue();
73
+ } else if (Key.ARROW_DOWN.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode && e.currentTarget.selectionEnd === e.currentTarget.value.length) {
74
+ e.preventDefault();
75
+ this.nextValue();
76
+ }
77
+
78
+ // Prevent newline on enter
79
+ if (Key.ENTER.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode && !e.nativeEvent.shiftKey) {
80
+ e.preventDefault();
81
+ }
82
+
83
+ this.props.onKeyDown?.(e);
84
+ };
85
+
86
+ /**
87
+ * Switch the textarea's text to the previous value, if any.
88
+ */
89
+ previousValue(): void {
90
+ const { history, index } = this.state;
91
+ if (!this.value) {
92
+ this.value = history[index];
93
+ } else if (index > 0 && index < history.length) {
94
+ this.updateState(index - 1);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Switch the textarea's text to the next value, if any.
100
+ */
101
+ nextValue(): void {
102
+ const { history, index } = this.state;
103
+ if (index === history.length - 1) {
104
+ this.value = '';
105
+ } else if (!this.value) {
106
+ this.value = history[index];
107
+ } else if (index >= 0 && index < history.length - 1) {
108
+ this.updateState(index + 1);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Handle history collection and textarea resizing without overriding the parent's onChange handler, if any.
114
+ */
115
+ protected readonly onChange = (e: React.ChangeEvent<HTMLTextAreaElement>): void => {
116
+ this.addToHistory();
117
+ this.props.onChange?.(e);
118
+ };
119
+
120
+ /**
121
+ * Add a nonempty current value to the history, if not already present. (Debounced, 1 second delay.)
122
+ */
123
+ readonly addToHistory = debounce(this.doAddToHistory, 1000);
124
+
125
+ private doAddToHistory(): void {
126
+ if (!this.value) {
127
+ return;
128
+ }
129
+ const history = this.state.history
130
+ .filter(term => term !== this.value)
131
+ .concat(this.value)
132
+ .slice(-SearchInWorkspaceTextArea.LIMIT);
133
+ this.updateState(history.length - 1, history);
134
+ }
135
+
136
+ override render(): React.ReactNode {
137
+ const { onResize, ...filteredProps } = this.props;
138
+ return (
139
+ <TextareaAutosize
140
+ {...filteredProps}
141
+ autoCapitalize="off"
142
+ autoCorrect="off"
143
+ maxRows={7} /* from VS Code */
144
+ onChange={this.onChange}
145
+ onKeyDown={this.onKeyDown}
146
+ ref={this.textarea}
147
+ rows={1}
148
+ spellCheck={false}
149
+ >
150
+ </TextareaAutosize>
151
+ );
152
+ }
153
+ }
@@ -36,7 +36,7 @@ import {
36
36
  import { CancellationTokenSource, Emitter, EOL, Event, ProgressService } from '@theia/core';
37
37
  import {
38
38
  EditorManager, EditorDecoration, TrackedRangeStickiness, OverviewRulerLane,
39
- EditorWidget, EditorOpenerOptions, FindMatch
39
+ EditorWidget, EditorOpenerOptions, FindMatch, Position
40
40
  } from '@theia/editor/lib/browser';
41
41
  import { WorkspaceService } from '@theia/workspace/lib/browser';
42
42
  import { FileResourceResolver, FileSystemPreferences } from '@theia/filesystem/lib/browser';
@@ -303,12 +303,16 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
303
303
 
304
304
  const matches: SearchMatch[] = [];
305
305
  results.forEach(r => {
306
- const lineText: string = widget.editor.document.getLineContent(r.range.start.line);
306
+ const numberOfLines = searchTerm.split('\n').length;
307
+ const lineTexts = [];
308
+ for (let i = 0; i < numberOfLines; i++) {
309
+ lineTexts.push(widget.editor.document.getLineContent(r.range.start.line + i));
310
+ }
307
311
  matches.push({
308
312
  line: r.range.start.line,
309
313
  character: r.range.start.character,
310
- length: r.range.end.character - r.range.start.character,
311
- lineText
314
+ length: searchTerm.length,
315
+ lineText: lineTexts.join('\n')
312
316
  });
313
317
  });
314
318
 
@@ -873,6 +877,7 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
873
877
  // Open the file only if the function is called to replace all matches under a specific node.
874
878
  const widget: EditorWidget = replaceOne ? await this.doOpen(toReplace[0]) : await this.doGetWidget(toReplace[0]);
875
879
  const source: string = widget.editor.document.getText();
880
+
876
881
  const replaceOperations = toReplace.map(resultLineNode => ({
877
882
  text: replacementText,
878
883
  range: {
@@ -880,12 +885,10 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
880
885
  line: resultLineNode.line - 1,
881
886
  character: resultLineNode.character - 1
882
887
  },
883
- end: {
884
- line: resultLineNode.line - 1,
885
- character: resultLineNode.character - 1 + resultLineNode.length
886
- }
888
+ end: this.findEndCharacterPosition(resultLineNode),
887
889
  }
888
890
  }));
891
+
889
892
  // Replace the text.
890
893
  await widget.editor.replaceText({
891
894
  source,
@@ -955,6 +958,23 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
955
958
  }
956
959
  }
957
960
 
961
+ private findEndCharacterPosition(node: SearchInWorkspaceResultLineNode): Position {
962
+ const lineText = typeof node.lineText === 'string' ? node.lineText : node.lineText.text;
963
+ const lines = lineText.split('\n');
964
+ const line = node.line + lines.length - 2;
965
+ let character = node.character - 1 + node.length;
966
+ if (lines.length > 1) {
967
+ character = node.length - lines[0].length + node.character - lines.length;
968
+ if (lines.length > 2) {
969
+ for (const lineNum of Array(lines.length - 2).keys()) {
970
+ character -= lines[lineNum + 1].length;
971
+ }
972
+ }
973
+ }
974
+
975
+ return { line, character };
976
+ }
977
+
958
978
  protected renderRootFolderNode(node: SearchInWorkspaceRootFolderNode): React.ReactNode {
959
979
  return <div className='result'>
960
980
  <div className='result-head'>
@@ -1017,28 +1037,33 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
1017
1037
  wordBreak.lastIndex++;
1018
1038
  }
1019
1039
 
1020
- const before = lineText.slice(start, character - 1).trimLeft();
1021
-
1022
- return <div className={`resultLine noWrapInfo noselect ${node.selected ? 'selected' : ''}`} title={lineText.trim()}>
1023
- {this.searchInWorkspacePreferences['search.lineNumbers'] && <span className='theia-siw-lineNumber'>{node.line}</span>}
1024
- <span>
1025
- {before}
1026
- </span>
1027
- {this.renderMatchLinePart(node)}
1028
- <span>
1029
- {lineText.slice(node.character + node.length - 1, 250 - before.length + node.length)}
1030
- </span>
1031
- </div>;
1040
+ const before = lineText.slice(start, character - 1).trimStart();
1041
+ const lineCount = lineText.split('\n').length;
1042
+
1043
+ return <>
1044
+ <div className={`resultLine noWrapInfo noselect ${node.selected ? 'selected' : ''}`} title={lineText.trim()}>
1045
+ {this.searchInWorkspacePreferences['search.lineNumbers'] && <span className='theia-siw-lineNumber'>{node.line}</span>}
1046
+ <span>
1047
+ {before}
1048
+ </span>
1049
+ {this.renderMatchLinePart(node)}
1050
+ {lineCount > 1 || <span>
1051
+ {lineText.slice(node.character + node.length - 1, 250 - before.length + node.length)}
1052
+ </span>}
1053
+ </div>
1054
+ {lineCount > 1 && <div className='match-line-num'>+{lineCount - 1}</div>}
1055
+ </>;
1032
1056
  }
1033
1057
 
1034
1058
  protected renderMatchLinePart(node: SearchInWorkspaceResultLineNode): React.ReactNode {
1035
- const replaceTerm = this.isReplacing ? <span className='replace-term'>{this._replaceTerm}</span> : '';
1059
+ const replaceTermLines = this._replaceTerm.split('\n');
1060
+ const replaceTerm = this.isReplacing ? <span className='replace-term'>{replaceTermLines[0]}</span> : '';
1036
1061
  const className = `match${this.isReplacing ? ' strike-through' : ''}`;
1037
- const match = typeof node.lineText === 'string' ?
1038
- node.lineText.substring(node.character - 1, node.length + node.character - 1)
1039
- : node.lineText.text.substring(node.lineText.character - 1, node.length + node.lineText.character - 1);
1062
+ const text = typeof node.lineText === 'string' ? node.lineText : node.lineText.text;
1063
+ const match = text.substring(node.character - 1, node.character + node.length - 1);
1064
+ const matchLines = match.split('\n');
1040
1065
  return <React.Fragment>
1041
- <span className={className}>{match}</span>
1066
+ <span className={className}>{matchLines[0]}</span>
1042
1067
  {replaceTerm}
1043
1068
  </React.Fragment>;
1044
1069
  }
@@ -1071,10 +1096,7 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
1071
1096
  line: node.line - 1,
1072
1097
  character: node.character - 1
1073
1098
  },
1074
- end: {
1075
- line: node.line - 1,
1076
- character: node.character - 1 + node.length
1077
- }
1099
+ end: this.findEndCharacterPosition(node),
1078
1100
  },
1079
1101
  mode: preview ? 'reveal' : 'activate',
1080
1102
  preview,
@@ -1100,16 +1122,8 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
1100
1122
  content = await resource.readContents();
1101
1123
  }
1102
1124
 
1103
- const lines = content.split('\n');
1104
- node.children.forEach(l => {
1105
- const leftPositionedNodes = node.children.filter(rl => rl.line === l.line && rl.character < l.character);
1106
- const diff = (this._replaceTerm.length - this.searchTerm.length) * leftPositionedNodes.length;
1107
- const start = lines[l.line - 1].substring(0, l.character - 1 + diff);
1108
- const end = lines[l.line - 1].substring(l.character - 1 + diff + l.length);
1109
- lines[l.line - 1] = start + this._replaceTerm + end;
1110
- });
1111
-
1112
- return fileUri.withScheme(MEMORY_TEXT).withQuery(lines.join('\n'));
1125
+ const searchTermRegExp = new RegExp(this.searchTerm, 'g');
1126
+ return fileUri.withScheme(MEMORY_TEXT).withQuery(content.replace(searchTermRegExp, this._replaceTerm));
1113
1127
  }
1114
1128
 
1115
1129
  protected decorateEditor(node: SearchInWorkspaceFileNode | undefined, editorWidget: EditorWidget): void {
@@ -28,6 +28,7 @@ import { ProgressBarFactory } from '@theia/core/lib/browser/progress-bar-factory
28
28
  import { EditorManager } from '@theia/editor/lib/browser';
29
29
  import { SearchInWorkspacePreferences } from './search-in-workspace-preferences';
30
30
  import { SearchInWorkspaceInput } from './components/search-in-workspace-input';
31
+ import { SearchInWorkspaceTextArea } from './components/search-in-workspace-textarea';
31
32
  import { nls } from '@theia/core/lib/common/nls';
32
33
 
33
34
  export interface SearchFieldState {
@@ -65,8 +66,8 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge
65
66
  protected searchTerm = '';
66
67
  protected replaceTerm = '';
67
68
 
68
- private searchRef = React.createRef<SearchInWorkspaceInput>();
69
- private replaceRef = React.createRef<SearchInWorkspaceInput>();
69
+ private searchRef = React.createRef<SearchInWorkspaceTextArea>();
70
+ private replaceRef = React.createRef<SearchInWorkspaceTextArea>();
70
71
  private includeRef = React.createRef<SearchInWorkspaceInput>();
71
72
  private excludeRef = React.createRef<SearchInWorkspaceInput>();
72
73
 
@@ -142,6 +143,7 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge
142
143
  matchCase: false,
143
144
  matchWholeWord: false,
144
145
  useRegExp: false,
146
+ multiline: false,
145
147
  includeIgnored: false,
146
148
  include: [],
147
149
  exclude: [],
@@ -323,6 +325,8 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge
323
325
 
324
326
  protected override onResize(msg: Widget.ResizeMessage): void {
325
327
  super.onResize(msg);
328
+ this.searchRef.current?.forceUpdate();
329
+ this.replaceRef.current?.forceUpdate();
326
330
  MessageLoop.sendMessage(this.resultTreeWidget, Widget.ResizeMessage.UnknownSize);
327
331
  }
328
332
 
@@ -447,7 +451,8 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge
447
451
  const searchOptions: SearchInWorkspaceOptions = {
448
452
  ...this.searchInWorkspaceOptions,
449
453
  followSymlinks: this.shouldFollowSymlinks(),
450
- matchCase: this.shouldMatchCase()
454
+ matchCase: this.shouldMatchCase(),
455
+ multiline: this.searchTerm.includes('\n')
451
456
  };
452
457
  this.resultTreeWidget.search(this.searchTerm, searchOptions);
453
458
  }
@@ -471,12 +476,10 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge
471
476
  }
472
477
 
473
478
  protected renderSearchField(): React.ReactNode {
474
- const input = <SearchInWorkspaceInput
479
+ const input = <SearchInWorkspaceTextArea
475
480
  id='search-input-field'
476
481
  className='theia-input'
477
482
  title={SearchInWorkspaceWidget.LABEL}
478
- type='text'
479
- size={1}
480
483
  placeholder={SearchInWorkspaceWidget.LABEL}
481
484
  defaultValue={this.searchTerm}
482
485
  autoComplete='off'
@@ -516,12 +519,10 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge
516
519
  const replaceAllButtonContainer = this.renderReplaceAllButtonContainer();
517
520
  const replace = nls.localizeByDefault('Replace');
518
521
  return <div className={`replace-field${this.showReplaceField ? '' : ' hidden'}`}>
519
- <SearchInWorkspaceInput
522
+ <SearchInWorkspaceTextArea
520
523
  id='replace-input-field'
521
524
  className='theia-input'
522
525
  title={replace}
523
- type='text'
524
- size={1}
525
526
  placeholder={replace}
526
527
  defaultValue={this.replaceTerm}
527
528
  autoComplete='off'
@@ -36,9 +36,14 @@
36
36
  }
37
37
 
38
38
  .t-siw-search-container .theia-input {
39
+ box-sizing: border-box;
39
40
  flex: 1;
40
41
  line-height: var(--theia-content-line-height);
42
+ max-height: calc(2 * 3px + 7 * var(--theia-content-line-height));
43
+ min-width: 16px;
41
44
  padding: 3px 0 3px 4px;
45
+ resize: none;
46
+ width: 100%;
42
47
  }
43
48
 
44
49
  .t-siw-search-container #search-input-field:focus {
@@ -99,16 +104,17 @@
99
104
  }
100
105
 
101
106
  .t-siw-search-container .searchHeader .search-field .option-buttons {
102
- height: 23px;
103
- display: flex;
104
- align-items: center;
105
- background-color: none;
107
+ height: 23px;
108
+ display: flex;
109
+ align-items: center;
110
+ align-self: flex-start;
111
+ background-color: none;
112
+ margin: 2px;
106
113
  }
107
114
 
108
115
  .t-siw-search-container .searchHeader .search-field-container.tooManyResults {
109
116
  border-style: solid;
110
117
  border-width: var(--theia-border-width);
111
- margin: -1px;
112
118
  border-color: var(--theia-inputValidation-warningBorder);
113
119
  }
114
120
 
@@ -234,7 +240,6 @@
234
240
  }
235
241
 
236
242
  .t-siw-search-container .resultLine .match {
237
- line-height: normal;
238
243
  white-space: pre;
239
244
  background: var(--theia-editor-findMatchHighlightBackground);
240
245
  border: 1px solid var(--theia-editor-findMatchHighlightBorder);
@@ -257,13 +262,17 @@
257
262
  border-style: dashed;
258
263
  }
259
264
 
260
- .t-siw-search-container .noWrapInfo {
261
- width: 100%;
265
+ .t-siw-search-container .match-line-num {
266
+ font-size: .9em;
267
+ margin-left: 7px;
268
+ margin-right: 4px;
269
+ opacity: .7;
262
270
  }
263
271
 
264
272
  .t-siw-search-container .result-head-info {
265
- display: inline-flex;
266
273
  align-items: center;
274
+ display: inline-flex;
275
+ flex-grow: 1;
267
276
  }
268
277
 
269
278
  .search-in-workspace-editor-match {
@@ -294,13 +303,18 @@
294
303
  }
295
304
 
296
305
  .result-node-buttons > span {
297
- width: 15px;
298
- height: 15px;
306
+ width: 16px;
307
+ height: 16px;
299
308
  margin-left: 2.5px;
300
309
  margin-right: 0.5px;
301
310
  background-repeat: no-repeat;
302
311
  background-position: center;
303
312
  background-size: contain;
313
+ border-radius: 5px;
314
+ }
315
+
316
+ .result-node-buttons > span:hover {
317
+ background-color: var(--theia-toolbar-hoverBackground);
304
318
  }
305
319
 
306
320
  .search-and-replace-container {
@@ -346,10 +360,11 @@
346
360
  }
347
361
 
348
362
  .replace-all-button-container {
349
- width: 25px;
350
- display: flex;
351
- align-items: center;
352
- justify-content: center;
363
+ width: 25px;
364
+ display: flex;
365
+ align-items: start;
366
+ justify-content: center;
367
+ padding-top: 3px;
353
368
  }
354
369
 
355
370
  .result-node-buttons .replace-result {
@@ -41,6 +41,10 @@ export interface SearchInWorkspaceOptions {
41
41
  * Use regular expressions for search if true.
42
42
  */
43
43
  useRegExp?: boolean;
44
+ /**
45
+ * Use multiline search if true.
46
+ */
47
+ multiline?: boolean;
44
48
  /**
45
49
  * Include all .gitignored and hidden files.
46
50
  */
@@ -957,10 +957,10 @@ describe('ripgrep-search-in-workspace-server', function (): void {
957
957
 
958
958
  describe('#extractSearchPathsFromIncludes', function (): void {
959
959
  this.timeout(10000);
960
- it('should not resolve paths from a not absolute / relative pattern', function (): void {
960
+ it('should not resolve paths from a not absolute / relative pattern', async () => {
961
961
  const pattern = 'carrots';
962
962
  const options = { include: [pattern] };
963
- const searchPaths = ripgrepServer['extractSearchPathsFromIncludes']([rootDirA], options);
963
+ const searchPaths = await ripgrepServer['extractSearchPathsFromIncludes']([rootDirA], options);
964
964
  // Same root directory
965
965
  expect(searchPaths.length).equal(1);
966
966
  expect(searchPaths[0]).equal(rootDirA);
@@ -970,21 +970,21 @@ describe('#extractSearchPathsFromIncludes', function (): void {
970
970
  expect(options.include[0]).equals(pattern);
971
971
  });
972
972
 
973
- it('should resolve pattern to path for relative filename', function (): void {
973
+ it('should resolve pattern to path for relative filename', async () => {
974
974
  const filename = 'carrots';
975
975
  const pattern = `./${filename}`;
976
- checkResolvedPathForPattern(pattern, path.join(rootDirA, filename));
976
+ await checkResolvedPathForPattern(pattern, path.join(rootDirA, filename));
977
977
  });
978
978
 
979
- it('should resolve relative pattern with sub-folders glob', function (): void {
979
+ it('should resolve relative pattern with sub-folders glob', async () => {
980
980
  const filename = 'carrots';
981
981
  const pattern = `./${filename}/**`;
982
- checkResolvedPathForPattern(pattern, path.join(rootDirA, filename));
982
+ await checkResolvedPathForPattern(pattern, path.join(rootDirA, filename));
983
983
  });
984
984
 
985
- it('should resolve absolute path pattern', function (): void {
985
+ it('should resolve absolute path pattern', async () => {
986
986
  const pattern = `${rootDirA}/carrots`;
987
- checkResolvedPathForPattern(pattern, pattern);
987
+ await checkResolvedPathForPattern(pattern, pattern);
988
988
  });
989
989
  });
990
990
 
@@ -1064,9 +1064,9 @@ describe('#addGlobArgs', function (): void {
1064
1064
  });
1065
1065
  });
1066
1066
 
1067
- function checkResolvedPathForPattern(pattern: string, expectedPath: string): void {
1067
+ async function checkResolvedPathForPattern(pattern: string, expectedPath: string): Promise<void> {
1068
1068
  const options = { include: [pattern] };
1069
- const searchPaths = ripgrepServer['extractSearchPathsFromIncludes']([rootDirA], options);
1069
+ const searchPaths = await ripgrepServer['extractSearchPathsFromIncludes']([rootDirA], options);
1070
1070
  expect(searchPaths.length).equal(1, 'searchPath result should contain exactly one element');
1071
1071
  expect(options.include.length).equals(0, 'options.include should be empty');
1072
1072
  expect(searchPaths[0]).equal(path.normalize(expectedPath));
@@ -101,6 +101,10 @@ export class RipgrepSearchInWorkspaceServer implements SearchInWorkspaceServer {
101
101
  args.add('--hidden');
102
102
  args.add('--json');
103
103
 
104
+ if (options?.multiline) {
105
+ args.add('--multiline');
106
+ }
107
+
104
108
  if (options?.matchCase) {
105
109
  args.add('--case-sensitive');
106
110
  } else {
@@ -216,7 +220,7 @@ export class RipgrepSearchInWorkspaceServer implements SearchInWorkspaceServer {
216
220
  const rootPaths = rootUris.map(root => FileUri.fsPath(root));
217
221
  // If there are absolute paths in `include` we will remove them and use
218
222
  // those as paths to search from.
219
- const searchPaths = this.extractSearchPathsFromIncludes(rootPaths, options);
223
+ const searchPaths = await this.extractSearchPathsFromIncludes(rootPaths, options);
220
224
  options.include = this.replaceRelativeToAbsolute(searchPaths, options.include);
221
225
  options.exclude = this.replaceRelativeToAbsolute(searchPaths, options.exclude);
222
226
  const rgArgs = this.getArgs(options);
@@ -388,23 +392,27 @@ export class RipgrepSearchInWorkspaceServer implements SearchInWorkspaceServer {
388
392
  * Any pattern that resulted in a valid search path will be removed from the 'include' list as it is
389
393
  * provided as an equivalent search path instead.
390
394
  */
391
- protected extractSearchPathsFromIncludes(rootPaths: string[], options: SearchInWorkspaceOptions): string[] {
395
+ protected async extractSearchPathsFromIncludes(rootPaths: string[], options: SearchInWorkspaceOptions): Promise<string[]> {
392
396
  if (!options.include) {
393
397
  return rootPaths;
394
398
  }
395
399
  const resolvedPaths = new Set<string>();
396
- options.include = options.include.filter(pattern => {
400
+ const include: string[] = [];
401
+ for (const pattern of options.include) {
397
402
  let keep = true;
398
403
  for (const root of rootPaths) {
399
- const absolutePath = this.getAbsolutePathFromPattern(root, pattern);
404
+ const absolutePath = await this.getAbsolutePathFromPattern(root, pattern);
400
405
  // undefined means the pattern cannot be converted into an absolute path
401
406
  if (absolutePath) {
402
407
  resolvedPaths.add(absolutePath);
403
408
  keep = false;
404
409
  }
405
410
  }
406
- return keep;
407
- });
411
+ if (keep) {
412
+ include.push(pattern);
413
+ }
414
+ }
415
+ options.include = include;
408
416
  return resolvedPaths.size > 0
409
417
  ? Array.from(resolvedPaths)
410
418
  : rootPaths;
@@ -417,7 +425,7 @@ export class RipgrepSearchInWorkspaceServer implements SearchInWorkspaceServer {
417
425
  *
418
426
  * @returns undefined if the pattern cannot be converted into an absolute path.
419
427
  */
420
- protected getAbsolutePathFromPattern(root: string, pattern: string): string | undefined {
428
+ protected async getAbsolutePathFromPattern(root: string, pattern: string): Promise<string | undefined> {
421
429
  pattern = pattern.replace(/\\/g, '/');
422
430
  // The pattern is not referring to a single file or folder, i.e. not to be converted
423
431
  if (!path.isAbsolute(pattern) && !pattern.startsWith('./')) {
@@ -429,7 +437,7 @@ export class RipgrepSearchInWorkspaceServer implements SearchInWorkspaceServer {
429
437
  }
430
438
  // if `pattern` is absolute then `root` will be ignored by `path.resolve()`
431
439
  const targetPath = path.resolve(root, pattern);
432
- if (fs.existsSync(targetPath)) {
440
+ if (await fs.pathExists(targetPath)) {
433
441
  return targetPath;
434
442
  }
435
443
  return undefined;