Skip to content

Commit e0c6f71

Browse files
authored
WorkspaceSymbolProvider improvements (#1366)
1 parent 4d0a962 commit e0c6f71

File tree

2 files changed

+103
-140
lines changed

2 files changed

+103
-140
lines changed

src/providers/FileSystemProvider/FileSearchProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export class FileSearchProvider implements vscode.FileSearchProvider {
4747
pattern = !csp ? query.pattern.replace(/\//g, ".") : query.pattern;
4848
if (pattern.includes("_") || pattern.includes("%")) {
4949
// Need to escape any % or _ characters
50-
filter = `Name LIKE '%${pattern.replace(/_/g, "$_").replace(/%/g, "$%")}%' ESCAPE '$'`;
50+
filter = `Name LIKE '%${pattern.replace(/(_|%|\\)/g, "\\$1")}%' ESCAPE '\\'`;
5151
} else {
5252
filter = `Name LIKE '%${pattern}%'`;
5353
}

src/providers/WorkspaceSymbolProvider.ts

Lines changed: 102 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,55 @@
11
import * as vscode from "vscode";
22
import { AtelierAPI } from "../api";
33
import { DocumentContentProvider } from "./DocumentContentProvider";
4+
import { filesystemSchemas } from "../extension";
5+
import { fileSpecFromURI } from "../utils/FileProviderUtil";
46

57
export class WorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider {
6-
private sql: string =
7-
"SELECT * FROM (" +
8-
"SELECT Name, Parent->ID AS Parent, 'method' AS Type FROM %Dictionary.MethodDefinition" +
9-
" UNION ALL %PARALLEL " +
10-
"SELECT Name, Parent->ID AS Parent, 'property' AS Type FROM %Dictionary.PropertyDefinition" +
11-
" UNION ALL %PARALLEL " +
12-
"SELECT Name, Parent->ID AS Parent, 'parameter' AS Type FROM %Dictionary.ParameterDefinition" +
13-
" UNION ALL %PARALLEL " +
14-
"SELECT Name, Parent->ID AS Parent, 'index' AS Type FROM %Dictionary.IndexDefinition" +
15-
" UNION ALL %PARALLEL " +
16-
"SELECT Name, Parent->ID AS Parent, 'foreignkey' AS Type FROM %Dictionary.ForeignKeyDefinition" +
17-
" UNION ALL %PARALLEL " +
18-
"SELECT Name, Parent->ID AS Parent, 'xdata' AS Type FROM %Dictionary.XDataDefinition" +
19-
" UNION ALL %PARALLEL " +
20-
"SELECT Name, Parent->ID AS Parent, 'query' AS Type FROM %Dictionary.QueryDefinition" +
21-
" UNION ALL %PARALLEL " +
22-
"SELECT Name, Parent->ID AS Parent, 'trigger' AS Type FROM %Dictionary.TriggerDefinition" +
23-
" UNION ALL %PARALLEL " +
24-
"SELECT Name, Parent->ID AS Parent, 'storage' AS Type FROM %Dictionary.StorageDefinition" +
25-
" UNION ALL %PARALLEL " +
26-
"SELECT Name, Parent->ID AS Parent, 'projection' AS Type FROM %Dictionary.ProjectionDefinition" +
27-
") WHERE %SQLUPPER Name %MATCHES ?";
8+
private readonly _sqlPrefix: string =
9+
"SELECT mem.Name, mem.Parent, mem.Type FROM (" +
10+
" SELECT Name, Name AS Parent, 'Class' AS Type FROM %Dictionary.ClassDefinition" +
11+
" UNION SELECT Name, Parent->ID AS Parent, 'Method' AS Type FROM %Dictionary.MethodDefinition" +
12+
" UNION SELECT Name, Parent->ID AS Parent, 'Property' AS Type FROM %Dictionary.PropertyDefinition" +
13+
" UNION SELECT Name, Parent->ID AS Parent, 'Parameter' AS Type FROM %Dictionary.ParameterDefinition" +
14+
" UNION SELECT Name, Parent->ID AS Parent, 'Index' AS Type FROM %Dictionary.IndexDefinition" +
15+
" UNION SELECT Name, Parent->ID AS Parent, 'ForeignKey' AS Type FROM %Dictionary.ForeignKeyDefinition" +
16+
" UNION SELECT Name, Parent->ID AS Parent, 'XData' AS Type FROM %Dictionary.XDataDefinition" +
17+
" UNION SELECT Name, Parent->ID AS Parent, 'Query' AS Type FROM %Dictionary.QueryDefinition" +
18+
" UNION SELECT Name, Parent->ID AS Parent, 'Trigger' AS Type FROM %Dictionary.TriggerDefinition" +
19+
" UNION SELECT Name, Parent->ID AS Parent, 'Storage' AS Type FROM %Dictionary.StorageDefinition" +
20+
" UNION SELECT Name, Parent->ID AS Parent, 'Projection' AS Type FROM %Dictionary.ProjectionDefinition" +
21+
") AS mem JOIN ";
2822

29-
private sqlNoSystem: string =
30-
"SELECT dict.Name, dict.Parent, dict.Type FROM (" +
31-
"SELECT Name, Parent->ID AS Parent, 'method' AS Type FROM %Dictionary.MethodDefinition" +
32-
" UNION ALL %PARALLEL " +
33-
"SELECT Name, Parent->ID AS Parent, 'property' AS Type FROM %Dictionary.PropertyDefinition" +
34-
" UNION ALL %PARALLEL " +
35-
"SELECT Name, Parent->ID AS Parent, 'parameter' AS Type FROM %Dictionary.ParameterDefinition" +
36-
" UNION ALL %PARALLEL " +
37-
"SELECT Name, Parent->ID AS Parent, 'index' AS Type FROM %Dictionary.IndexDefinition" +
38-
" UNION ALL %PARALLEL " +
39-
"SELECT Name, Parent->ID AS Parent, 'foreignkey' AS Type FROM %Dictionary.ForeignKeyDefinition" +
40-
" UNION ALL %PARALLEL " +
41-
"SELECT Name, Parent->ID AS Parent, 'xdata' AS Type FROM %Dictionary.XDataDefinition" +
42-
" UNION ALL %PARALLEL " +
43-
"SELECT Name, Parent->ID AS Parent, 'query' AS Type FROM %Dictionary.QueryDefinition" +
44-
" UNION ALL %PARALLEL " +
45-
"SELECT Name, Parent->ID AS Parent, 'trigger' AS Type FROM %Dictionary.TriggerDefinition" +
46-
" UNION ALL %PARALLEL " +
47-
"SELECT Name, Parent->ID AS Parent, 'storage' AS Type FROM %Dictionary.StorageDefinition" +
48-
" UNION ALL %PARALLEL " +
49-
"SELECT Name, Parent->ID AS Parent, 'projection' AS Type FROM %Dictionary.ProjectionDefinition" +
50-
") AS dict, (" +
51-
"SELECT Name FROM %Library.RoutineMgr_StudioOpenDialog(?,?,?,?,?,?,?)" +
52-
") AS sod WHERE %SQLUPPER dict.Name %MATCHES ? AND {fn CONCAT(dict.Parent,'.cls')} = sod.Name";
23+
private readonly _sqlPrj: string =
24+
"%Studio.Project_ProjectItemsList(?) AS pil ON mem.Parent = pil.Name AND pil.Type = 'CLS'";
5325

54-
private queryResultToSymbols(data: any, folderUri: vscode.Uri) {
26+
private readonly _sqlDocs: string =
27+
"%Library.RoutineMgr_StudioOpenDialog(?,1,1,?,1,0,?,'Type = 4',0,?) AS sod ON mem.Parent = $EXTRACT(sod.Name,1,$LENGTH(sod.Name)-4)";
28+
29+
private readonly _sqlSuffix: string = " WHERE mem.Name LIKE ? ESCAPE '\\'";
30+
31+
/**
32+
* Convert the query results to VS Code symbols. Needs to be typed as `any[]`
33+
* because we aren't including ranges. They will be resolved later.
34+
*/
35+
private _queryResultToSymbols(data: any, wsFolder: vscode.WorkspaceFolder): any[] {
5536
const result = [];
5637
const uris: Map<string, vscode.Uri> = new Map();
5738
for (const element of data.result.content) {
5839
const kind: vscode.SymbolKind = (() => {
5940
switch (element.Type) {
60-
case "query":
61-
case "method":
41+
case "Query":
42+
case "Method":
6243
return vscode.SymbolKind.Method;
63-
case "parameter":
44+
case "Parameter":
6445
return vscode.SymbolKind.Constant;
65-
case "index":
46+
case "Index":
6647
return vscode.SymbolKind.Key;
67-
case "xdata":
68-
case "storage":
48+
case "XData":
49+
case "Storage":
6950
return vscode.SymbolKind.Struct;
70-
case "property":
51+
case "Class":
52+
return vscode.SymbolKind.Class;
7153
default:
7254
return vscode.SymbolKind.Property;
7355
}
@@ -77,14 +59,21 @@ export class WorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider {
7759
if (uris.has(element.Parent)) {
7860
uri = uris.get(element.Parent);
7961
} else {
80-
uri = DocumentContentProvider.getUri(`${element.Parent}.cls`, undefined, undefined, undefined, folderUri);
62+
uri = DocumentContentProvider.getUri(
63+
`${element.Parent}.cls`,
64+
wsFolder.name,
65+
undefined,
66+
undefined,
67+
wsFolder.uri,
68+
// Only "file" scheme is fully supported for client-side editing
69+
wsFolder.uri.scheme != "file"
70+
);
8171
uris.set(element.Parent, uri);
8272
}
8373

8474
result.push({
8575
name: element.Name,
86-
containerName:
87-
element.Type === "foreignkey" ? "ForeignKey" : element.Type.charAt(0).toUpperCase() + element.Type.slice(1),
76+
containerName: element.Type,
8877
kind,
8978
location: {
9079
uri,
@@ -94,96 +83,70 @@ export class WorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider {
9483
return result;
9584
}
9685

97-
public async provideWorkspaceSymbols(query: string): Promise<vscode.SymbolInformation[]> {
98-
if (query.length === 0) {
99-
return null;
100-
}
101-
// Convert query to a %MATCHES compatible pattern
102-
let pattern = "";
103-
for (let i = 0; i < query.length; i++) {
104-
const char = query.charAt(i);
105-
pattern += char === "*" || char === "?" ? `*\\${char}` : `*${char}`;
106-
}
107-
pattern = pattern.toUpperCase() + "*";
108-
// Filter the folders to search so we don't query the same ns on the same server twice
109-
const serversToQuery: {
110-
api: AtelierAPI;
111-
uri: vscode.Uri;
112-
system: boolean;
113-
}[] = [];
114-
for (const folder of vscode.workspace.workspaceFolders) {
115-
const folderApi = new AtelierAPI(folder.uri);
116-
const found = serversToQuery.findIndex(
117-
(server) =>
118-
server.api.config.host.toLowerCase() === folderApi.config.host.toLowerCase() &&
119-
server.api.config.port === folderApi.config.port &&
120-
server.api.config.pathPrefix.toLowerCase() === folderApi.config.pathPrefix.toLowerCase() &&
121-
server.api.config.ns.toLowerCase() === folderApi.config.ns.toLowerCase()
122-
);
123-
if (found === -1) {
124-
serversToQuery.push({
125-
api: folderApi,
126-
uri: folder.uri,
127-
system: true,
128-
});
129-
} else if (serversToQuery[found].uri.scheme.startsWith("isfs") && !folder.uri.scheme.startsWith("isfs")) {
130-
// If we have multiple folders connected to the same server and ns
131-
// and one is not isfs, keep the non-isfs one
132-
serversToQuery[found].uri = folder.uri;
133-
}
134-
}
135-
serversToQuery.map((server) => {
136-
if (server.api.config.ns.toLowerCase() !== "%sys") {
137-
const found = serversToQuery.findIndex(
138-
(server2) =>
139-
server2.api.config.host.toLowerCase() === server.api.config.host.toLowerCase() &&
140-
server2.api.config.port === server.api.config.port &&
141-
server2.api.config.pathPrefix.toLowerCase() === server.api.config.pathPrefix.toLowerCase() &&
142-
server2.api.config.ns.toLowerCase() === "%sys"
143-
);
144-
if (found !== -1) {
145-
server.system = false;
146-
}
147-
}
148-
return server;
149-
});
86+
public async provideWorkspaceSymbols(
87+
query: string,
88+
token: vscode.CancellationToken
89+
): Promise<vscode.SymbolInformation[]> {
90+
if (!vscode.workspace.workspaceFolders?.length) return;
91+
// Convert query to a LIKE compatible pattern
92+
let pattern = "%";
93+
for (const c of query) pattern += `${["_", "%", "\\"].includes(c) ? "\\" : ""}${c}%`;
94+
if (token.isCancellationRequested) return;
95+
// Get results for all workspace folders
15096
return Promise.allSettled(
151-
serversToQuery
152-
.map((server) => {
153-
// Set the system property so we don't show system items multiple times if this
154-
// workspace is connected to both the %SYS and a non-%SYS namespace on the same server
155-
if (server.api.config.ns.toLowerCase() !== "%sys") {
156-
const found = serversToQuery.findIndex(
157-
(server2) =>
158-
server2.api.config.host.toLowerCase() === server.api.config.host.toLowerCase() &&
159-
server2.api.config.port === server.api.config.port &&
160-
server2.api.config.pathPrefix.toLowerCase() === server.api.config.pathPrefix.toLowerCase() &&
161-
server2.api.config.ns.toLowerCase() === "%sys"
162-
);
163-
if (found !== -1) {
164-
server.system = false;
165-
}
97+
vscode.workspace.workspaceFolders.map((wsFolder) => {
98+
if (filesystemSchemas.includes(wsFolder.uri.scheme)) {
99+
const params = new URLSearchParams(wsFolder.uri.query);
100+
if (params.has("csp") && ["", "1"].includes(params.get("csp"))) {
101+
// No classes or class members in web application folders
102+
return Promise.resolve([]);
103+
} else {
104+
const api = new AtelierAPI(wsFolder.uri);
105+
if (!api.active || token.isCancellationRequested) return Promise.resolve([]);
106+
const project = params.get("project") ?? "";
107+
return api
108+
.actionQuery(`${this._sqlPrefix}${project.length ? this._sqlPrj : this._sqlDocs}${this._sqlSuffix}`, [
109+
project.length ? project : fileSpecFromURI(wsFolder.uri),
110+
params.has("system") && params.get("system").length
111+
? params.get("system")
112+
: api.ns == "%SYS"
113+
? "1"
114+
: "0",
115+
params.has("generated") && params.get("generated").length ? params.get("generated") : "0",
116+
params.has("mapped") && params.get("mapped") == "0" ? "0" : "1",
117+
pattern,
118+
])
119+
.then((data) => (token.isCancellationRequested ? [] : this._queryResultToSymbols(data, wsFolder)));
166120
}
167-
return server;
168-
})
169-
.map((server) =>
170-
server.system
171-
? server.api.actionQuery(this.sql, [pattern]).then((data) => this.queryResultToSymbols(data, server.uri))
172-
: server.api
173-
.actionQuery(this.sqlNoSystem, ["*.cls", "1", "1", "0", "1", "0", "0", pattern])
174-
.then((data) => this.queryResultToSymbols(data, server.uri))
175-
)
176-
).then((results) => results.flatMap((result) => (result.status === "fulfilled" ? result.value : [])));
121+
} else {
122+
// Client-side folders should use the isfs default parameters
123+
const api = new AtelierAPI(wsFolder.uri);
124+
if (!api.active || token.isCancellationRequested) return Promise.resolve([]);
125+
return api
126+
.actionQuery(`${this._sqlPrefix}${this._sqlDocs}${this._sqlSuffix}`, ["*.cls", "0", "0", "1", pattern])
127+
.then((data) => (token.isCancellationRequested ? [] : this._queryResultToSymbols(data, wsFolder)));
128+
}
129+
})
130+
).then((results) => results.flatMap((result) => (result.status == "fulfilled" ? result.value : [])));
177131
}
178132

179133
resolveWorkspaceSymbol(symbol: vscode.SymbolInformation): vscode.ProviderResult<vscode.SymbolInformation> {
180134
return vscode.commands
181135
.executeCommand<vscode.DocumentSymbol[]>("vscode.executeDocumentSymbolProvider", symbol.location.uri)
182136
.then((docSymbols) => {
183-
for (const docSymbol of docSymbols[0].children) {
184-
if (docSymbol.name === symbol.name && docSymbol.kind === symbol.kind) {
185-
symbol.location.range = docSymbol.selectionRange;
186-
break;
137+
if (!Array.isArray(docSymbols) || !docSymbols.length) return;
138+
if (symbol.kind == vscode.SymbolKind.Class) {
139+
symbol.location.range = docSymbols[0].selectionRange;
140+
} else {
141+
const memberType = symbol.containerName.toUpperCase();
142+
const unquote = (n: string): string => {
143+
return n[0] == '"' ? n.slice(1, -1).replace(/""/g, '"') : n;
144+
};
145+
for (const docSymbol of docSymbols[0].children) {
146+
if (unquote(docSymbol.name) == symbol.name && docSymbol.detail.toUpperCase().includes(memberType)) {
147+
symbol.location.range = docSymbol.selectionRange;
148+
break;
149+
}
187150
}
188151
}
189152
return symbol;

0 commit comments

Comments
 (0)