Skip to content

Commit 24de768

Browse files
authored
Improve TextSearchProvider (#852)
1 parent c75b393 commit 24de768

File tree

4 files changed

+260
-60
lines changed

4 files changed

+260
-60
lines changed

src/api/atelier.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export interface ServerInfo {
4545

4646
export interface SearchMatch {
4747
text: string;
48-
line?: number;
48+
line?: string | number;
4949
member?: string;
5050
attr?: string;
5151
attrline?: number;

src/providers/FileSystemProvider/TextSearchProvider.ts

Lines changed: 192 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,23 @@ import * as vscode from "vscode";
22
import { SearchResult, SearchMatch } from "../../api/atelier";
33
import { AtelierAPI } from "../../api";
44
import { DocumentContentProvider } from "../DocumentContentProvider";
5-
import { outputChannel } from "../../utils";
5+
import { notNull, outputChannel, throttleRequests } from "../../utils";
6+
import { config } from "../../extension";
7+
import { fileSpecFromURI } from "../../utils/FileProviderUtil";
8+
9+
/**
10+
* Convert an `attrline` in a description to a line number in `document`.
11+
*/
12+
function descLineToDocLine(content: string[], attrline: number, line: number): number {
13+
let result = 0;
14+
for (let i = line - 1; i >= 0; i--) {
15+
if (!content[i].startsWith("///")) {
16+
result = i;
17+
break;
18+
}
19+
}
20+
return result + attrline;
21+
}
622

723
export class TextSearchProvider implements vscode.TextSearchProvider {
824
/**
@@ -21,67 +37,196 @@ export class TextSearchProvider implements vscode.TextSearchProvider {
2137
const api = new AtelierAPI(options.folder);
2238
let counter = 0;
2339
if (!api.enabled) {
24-
return null;
40+
return {
41+
message: {
42+
text: "An active server connection is required for searching `isfs` folders.",
43+
type: vscode.TextSearchCompleteMessageType.Warning,
44+
},
45+
};
46+
}
47+
if (token.isCancellationRequested) {
48+
return;
2549
}
2650
return api
2751
.actionSearch({
2852
query: query.pattern,
2953
regex: query.isRegExp,
3054
word: query.isWordMatch,
3155
case: query.isCaseSensitive,
56+
files: fileSpecFromURI(options.folder),
3257
// If options.maxResults is null the search is supposed to return an unlimited number of results
33-
// Since there's no way for us to pass "unlimited" to the server, I choose a very large number
58+
// Since there's no way for us to pass "unlimited" to the server, I chose a very large number
3459
max: options.maxResults ?? 100000,
3560
})
3661
.then((data) => data.result)
37-
.then((files: SearchResult[]) =>
38-
files.map(async (file) => {
39-
const fileName = file.doc;
40-
const uri = DocumentContentProvider.getUri(fileName, "", "", true, options.folder);
41-
try {
42-
const document = await vscode.workspace.openTextDocument(uri);
43-
return {
44-
...file,
45-
uri,
46-
document,
47-
};
48-
} catch (_ex) {
49-
return null;
50-
}
51-
})
52-
)
53-
.then((files) => Promise.all(files))
54-
.then((files) => {
55-
files.forEach((file) => {
56-
const { uri, document, matches } = file;
57-
matches.forEach((match: SearchMatch) => {
58-
const { text, member } = match;
59-
let { line } = match;
60-
if (member) {
61-
const memberMatchPattern = new RegExp(`((?:Class)?Method|Property|XData|Query|Trigger) ${member}`, "i");
62-
for (let i = 0; i < document.lineCount; i++) {
63-
const text = document.lineAt(i).text;
64-
if (text.match(memberMatchPattern)) {
65-
line = line ? line + i + 1 : i;
66-
}
62+
.then(async (files: SearchResult[]) => {
63+
if (token.isCancellationRequested) {
64+
return;
65+
}
66+
const result = await Promise.allSettled(
67+
files.map(
68+
throttleRequests(async (file: SearchResult) => {
69+
if (token.isCancellationRequested) {
70+
throw new vscode.CancellationError();
6771
}
68-
}
69-
progress.report({
70-
uri,
71-
lineNumber: line || 1,
72-
text,
72+
const uri = DocumentContentProvider.getUri(file.doc, "", "", true, options.folder);
73+
const content = await api.getDoc(file.doc).then((data) => <string[]>data.result.content);
74+
// Find all lines that we have matches on
75+
const lines = file.matches
76+
.map((match: SearchMatch) => {
77+
let line = Number(match.line);
78+
if (match.member !== undefined) {
79+
// This is an attribute of a class member
80+
const memberMatchPattern = new RegExp(
81+
`^((?:Class|Client)?Method|Property|XData|Query|Trigger|Parameter|Relationship|Index|ForeignKey|Storage|Projection) ${match.member}`
82+
);
83+
for (let i = 0; i < content.length; i++) {
84+
if (content[i].match(memberMatchPattern)) {
85+
let memend = i + 1;
86+
if (
87+
config("multilineMethodArgs", api.configName) &&
88+
content[i].match(/^(?:Class|Client)?Method|Query /)
89+
) {
90+
// The class member definition is on multiple lines so update the end
91+
for (let j = i + 1; j < content.length; j++) {
92+
if (content[j].trim() === "{") {
93+
memend = j;
94+
break;
95+
}
96+
}
97+
}
98+
if (match.attr === undefined) {
99+
if (match.line === undefined) {
100+
// This is in the class member definition
101+
line = i;
102+
} else {
103+
// This is in the implementation
104+
line = memend + Number(match.line);
105+
}
106+
} else {
107+
if (match.attrline === undefined) {
108+
// This is in the class member definition
109+
line = 1;
110+
} else {
111+
if (match.attr === "Description") {
112+
// This is in the description
113+
line = descLineToDocLine(content, match.attrline, i);
114+
} else {
115+
// This is in the implementation
116+
line = memend + match.attrline;
117+
}
118+
}
119+
}
120+
break;
121+
}
122+
}
123+
} else if (match.attr !== undefined) {
124+
if (match.attr === "IncludeCode") {
125+
// This is in the Include line
126+
for (let i = 0; i < content.length; i++) {
127+
if (content[i].match(/^Include /)) {
128+
line = i;
129+
break;
130+
}
131+
}
132+
} else if (match.attr === "IncludeGenerator") {
133+
// This is in the IncludeGenerator line
134+
for (let i = 0; i < content.length; i++) {
135+
if (content[i].match(/^IncludeGenerator/)) {
136+
line = i;
137+
break;
138+
}
139+
}
140+
} else if (match.attr === "Import") {
141+
// This is in the Import line
142+
for (let i = 0; i < content.length; i++) {
143+
if (content[i].match(/^Import/)) {
144+
line = i;
145+
break;
146+
}
147+
}
148+
} else {
149+
// This is in the class definition
150+
const classMatchPattern = new RegExp(`^Class ${file.doc.slice(0, file.doc.lastIndexOf("."))}`);
151+
for (let i = 0; i < content.length; i++) {
152+
if (content[i].match(classMatchPattern)) {
153+
if (match.attrline) {
154+
// This is in the class description
155+
line = descLineToDocLine(content, match.attrline, i);
156+
} else {
157+
line = i;
158+
}
159+
break;
160+
}
161+
}
162+
}
163+
}
164+
return typeof line === "number" ? line : null;
165+
})
166+
.filter(notNull);
167+
// Filter out duplicates and compute all matches for each one
168+
[...new Set(lines)].forEach((line) => {
169+
const text = content[line];
170+
const regex = new RegExp(
171+
query.isRegExp ? query.pattern : query.pattern.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"),
172+
query.isCaseSensitive ? "g" : "gi"
173+
);
174+
let regexMatch: RegExpExecArray;
175+
const matchRanges: vscode.Range[] = [];
176+
const previewRanges: vscode.Range[] = [];
177+
while ((regexMatch = regex.exec(text)) !== null && counter < options.maxResults) {
178+
const start = regexMatch.index;
179+
const end = start + regexMatch[0].length;
180+
matchRanges.push(new vscode.Range(line, start, line, end));
181+
previewRanges.push(new vscode.Range(0, start, 0, end));
182+
counter++;
183+
}
184+
if (matchRanges.length && previewRanges.length) {
185+
progress.report({
186+
uri,
187+
ranges: matchRanges,
188+
preview: {
189+
text,
190+
matches: previewRanges,
191+
},
192+
});
193+
}
194+
});
195+
})
196+
)
197+
);
198+
if (token.isCancellationRequested) {
199+
return;
200+
}
201+
let message: vscode.TextSearchCompleteMessage;
202+
const rejected = result.filter((r) => r.status == "rejected").length;
203+
if (rejected > 0) {
204+
outputChannel.appendLine("Search errors:");
205+
result
206+
.filter((r) => r.status == "rejected")
207+
.forEach((r: PromiseRejectedResult) => {
208+
outputChannel.appendLine(typeof r.reason == "object" ? r.reason.toString() : String(r.reason));
73209
});
74-
counter++;
75-
if (counter >= options.maxResults) {
76-
return;
77-
}
78-
});
79-
});
80-
return { limitHit: counter >= options.maxResults };
210+
message = {
211+
text: `Failed to display results from ${rejected} file${
212+
rejected > 1 ? "s" : ""
213+
}. Check \`ObjectScript\` Output channel for details.`,
214+
type: vscode.TextSearchCompleteMessageType.Warning,
215+
};
216+
}
217+
return {
218+
limitHit: counter >= options.maxResults,
219+
message,
220+
};
81221
})
82222
.catch((error) => {
83-
outputChannel.appendLine(error);
84-
return null;
223+
outputChannel.appendLine(typeof error == "object" ? error.toString() : String(error));
224+
return {
225+
message: {
226+
text: "An error occurred during the search. Check `ObjectScript` Output channel for details.",
227+
type: vscode.TextSearchCompleteMessageType.Warning,
228+
},
229+
};
85230
});
86231
}
87232
}

src/utils/FileProviderUtil.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,12 @@ import * as vscode from "vscode";
22
import * as url from "url";
33
import { AtelierAPI } from "../api";
44

5-
export function studioOpenDialogFromURI(
6-
uri: vscode.Uri,
7-
overrides: { flat?: boolean; filter?: string; type?: string } = { flat: false, filter: "", type: "" }
8-
): Promise<any> {
9-
const api = new AtelierAPI(uri);
10-
if (!api.active) {
11-
return;
12-
}
13-
const sql = `CALL %Library.RoutineMgr_StudioOpenDialog(?,?,?,?,?,?,?,?)`;
5+
export function fileSpecFromURI(uri: vscode.Uri, overrideType?: string): string {
146
const { query } = url.parse(uri.toString(true), true);
157
const csp = query.csp === "" || query.csp === "1";
168
const type =
17-
overrides.type && overrides.type != ""
18-
? overrides.type
9+
overrideType && overrideType != ""
10+
? overrideType
1911
: query.type && query.type != ""
2012
? query.type.toString()
2113
: csp
@@ -50,7 +42,20 @@ export function studioOpenDialogFromURI(
5042
} else {
5143
specOpts = "*.cls,*.inc,*.mac,*.int";
5244
}
53-
const spec = csp ? folder + specOpts : folder.length > 1 ? folder.slice(1) + "/" + specOpts : specOpts;
45+
return csp ? folder + specOpts : folder.length > 1 ? folder.slice(1) + "/" + specOpts : specOpts;
46+
}
47+
48+
export function studioOpenDialogFromURI(
49+
uri: vscode.Uri,
50+
overrides: { flat?: boolean; filter?: string; type?: string } = { flat: false, filter: "", type: "" }
51+
): Promise<any> {
52+
const api = new AtelierAPI(uri);
53+
if (!api.active) {
54+
return;
55+
}
56+
const sql = `CALL %Library.RoutineMgr_StudioOpenDialog(?,?,?,?,?,?,?,?)`;
57+
const { query } = url.parse(uri.toString(true), true);
58+
const spec = fileSpecFromURI(uri, overrides.type);
5459
const notStudio = "0";
5560
const dir = "1";
5661
const orderBy = "1";

src/utils/index.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,3 +489,53 @@ export function redirectDotvscodeRoot(uri: vscode.Uri): vscode.Uri {
489489
return uri;
490490
}
491491
}
492+
493+
// ---------------------------------------------------------------------
494+
// Source: https://github.com/amsterdamharu/lib/blob/master/src/index.js
495+
496+
const promiseLike = (x) => x !== undefined && typeof x.then === "function";
497+
const ifPromise = (fn) => (x) => promiseLike(x) ? x.then(fn) : fn(x);
498+
499+
/*
500+
causes a promise returning function not to be called
501+
until less than max are active
502+
usage example:
503+
max2 = throttle(2);
504+
urls = [url1,url2,url3...url100]
505+
Promise.all(//even though a 100 promises are created, only 2 are active
506+
urls.map(max2(fetch))
507+
)
508+
*/
509+
const throttle = (max: number): ((fn: any) => (arg: any) => Promise<any>) => {
510+
let que = [];
511+
let queIndex = -1;
512+
let running = 0;
513+
const wait = (resolve, fn, arg) => () => resolve(ifPromise(fn)(arg)) || true; //should always return true
514+
const nextInQue = () => {
515+
++queIndex;
516+
if (typeof que[queIndex] === "function") {
517+
return que[queIndex]();
518+
} else {
519+
que = [];
520+
queIndex = -1;
521+
running = 0;
522+
return "Does not matter, not used";
523+
}
524+
};
525+
const queItem = (fn, arg) => new Promise((resolve, reject) => que.push(wait(resolve, fn, arg)));
526+
return (fn) => (arg) => {
527+
const p = queItem(fn, arg).then((x) => nextInQue() && x);
528+
running++;
529+
if (running <= max) {
530+
nextInQue();
531+
}
532+
return p;
533+
};
534+
};
535+
536+
// ---------------------------------------------------------------------
537+
538+
/**
539+
* Wrap around each promise in array to avoid overloading the server.
540+
*/
541+
export const throttleRequests = throttle(50);

0 commit comments

Comments
 (0)