@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.
- package/lib/browser/components/search-in-workspace-textarea.d.ts +40 -0
- package/lib/browser/components/search-in-workspace-textarea.d.ts.map +1 -0
- package/lib/browser/components/search-in-workspace-textarea.js +131 -0
- package/lib/browser/components/search-in-workspace-textarea.js.map +1 -0
- package/lib/browser/search-in-workspace-result-tree-widget.d.ts +1 -0
- package/lib/browser/search-in-workspace-result-tree-widget.d.ts.map +1 -1
- package/lib/browser/search-in-workspace-result-tree-widget.js +43 -31
- package/lib/browser/search-in-workspace-result-tree-widget.js.map +1 -1
- package/lib/browser/search-in-workspace-widget.d.ts.map +1 -1
- package/lib/browser/search-in-workspace-widget.js +9 -3
- package/lib/browser/search-in-workspace-widget.js.map +1 -1
- package/lib/common/search-in-workspace-interface.d.ts +4 -0
- package/lib/common/search-in-workspace-interface.d.ts.map +1 -1
- package/lib/common/search-in-workspace-interface.js.map +1 -1
- package/lib/node/ripgrep-search-in-workspace-server.d.ts +2 -2
- package/lib/node/ripgrep-search-in-workspace-server.d.ts.map +1 -1
- package/lib/node/ripgrep-search-in-workspace-server.js +15 -8
- package/lib/node/ripgrep-search-in-workspace-server.js.map +1 -1
- package/lib/node/ripgrep-search-in-workspace-server.slow-spec.js +10 -10
- package/lib/node/ripgrep-search-in-workspace-server.slow-spec.js.map +1 -1
- package/package.json +11 -10
- package/src/browser/components/search-in-workspace-textarea.tsx +153 -0
- package/src/browser/search-in-workspace-result-tree-widget.tsx +53 -39
- package/src/browser/search-in-workspace-widget.tsx +10 -9
- package/src/browser/styles/index.css +30 -15
- package/src/common/search-in-workspace-interface.ts +4 -0
- package/src/node/ripgrep-search-in-workspace-server.slow-spec.ts +10 -10
- 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
|
|
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:
|
|
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).
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
<
|
|
1025
|
-
{
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
{
|
|
1030
|
-
|
|
1031
|
-
|
|
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
|
|
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
|
|
1038
|
-
|
|
1039
|
-
|
|
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}>{
|
|
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
|
|
1104
|
-
|
|
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<
|
|
69
|
-
private replaceRef = React.createRef<
|
|
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 = <
|
|
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
|
-
<
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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 .
|
|
261
|
-
|
|
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:
|
|
298
|
-
height:
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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 {
|
|
@@ -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',
|
|
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',
|
|
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',
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
440
|
+
if (await fs.pathExists(targetPath)) {
|
|
433
441
|
return targetPath;
|
|
434
442
|
}
|
|
435
443
|
return undefined;
|