Skip to content

Commit

Permalink
WorkspaceSymbolProvider improvements (#1366)
Browse files Browse the repository at this point in the history
  • Loading branch information
isc-bsaviano authored May 21, 2024
1 parent 4d0a962 commit e0c6f71
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 140 deletions.
2 changes: 1 addition & 1 deletion src/providers/FileSystemProvider/FileSearchProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class FileSearchProvider implements vscode.FileSearchProvider {
pattern = !csp ? query.pattern.replace(/\//g, ".") : query.pattern;
if (pattern.includes("_") || pattern.includes("%")) {
// Need to escape any % or _ characters
filter = `Name LIKE '%${pattern.replace(/_/g, "$_").replace(/%/g, "$%")}%' ESCAPE '$'`;
filter = `Name LIKE '%${pattern.replace(/(_|%|\\)/g, "\\$1")}%' ESCAPE '\\'`;
} else {
filter = `Name LIKE '%${pattern}%'`;
}
Expand Down
241 changes: 102 additions & 139 deletions src/providers/WorkspaceSymbolProvider.ts
Original file line number Diff line number Diff line change
@@ -1,73 +1,55 @@
import * as vscode from "vscode";
import { AtelierAPI } from "../api";
import { DocumentContentProvider } from "./DocumentContentProvider";
import { filesystemSchemas } from "../extension";
import { fileSpecFromURI } from "../utils/FileProviderUtil";

export class WorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider {
private sql: string =
"SELECT * FROM (" +
"SELECT Name, Parent->ID AS Parent, 'method' AS Type FROM %Dictionary.MethodDefinition" +
" UNION ALL %PARALLEL " +
"SELECT Name, Parent->ID AS Parent, 'property' AS Type FROM %Dictionary.PropertyDefinition" +
" UNION ALL %PARALLEL " +
"SELECT Name, Parent->ID AS Parent, 'parameter' AS Type FROM %Dictionary.ParameterDefinition" +
" UNION ALL %PARALLEL " +
"SELECT Name, Parent->ID AS Parent, 'index' AS Type FROM %Dictionary.IndexDefinition" +
" UNION ALL %PARALLEL " +
"SELECT Name, Parent->ID AS Parent, 'foreignkey' AS Type FROM %Dictionary.ForeignKeyDefinition" +
" UNION ALL %PARALLEL " +
"SELECT Name, Parent->ID AS Parent, 'xdata' AS Type FROM %Dictionary.XDataDefinition" +
" UNION ALL %PARALLEL " +
"SELECT Name, Parent->ID AS Parent, 'query' AS Type FROM %Dictionary.QueryDefinition" +
" UNION ALL %PARALLEL " +
"SELECT Name, Parent->ID AS Parent, 'trigger' AS Type FROM %Dictionary.TriggerDefinition" +
" UNION ALL %PARALLEL " +
"SELECT Name, Parent->ID AS Parent, 'storage' AS Type FROM %Dictionary.StorageDefinition" +
" UNION ALL %PARALLEL " +
"SELECT Name, Parent->ID AS Parent, 'projection' AS Type FROM %Dictionary.ProjectionDefinition" +
") WHERE %SQLUPPER Name %MATCHES ?";
private readonly _sqlPrefix: string =
"SELECT mem.Name, mem.Parent, mem.Type FROM (" +
" SELECT Name, Name AS Parent, 'Class' AS Type FROM %Dictionary.ClassDefinition" +
" UNION SELECT Name, Parent->ID AS Parent, 'Method' AS Type FROM %Dictionary.MethodDefinition" +
" UNION SELECT Name, Parent->ID AS Parent, 'Property' AS Type FROM %Dictionary.PropertyDefinition" +
" UNION SELECT Name, Parent->ID AS Parent, 'Parameter' AS Type FROM %Dictionary.ParameterDefinition" +
" UNION SELECT Name, Parent->ID AS Parent, 'Index' AS Type FROM %Dictionary.IndexDefinition" +
" UNION SELECT Name, Parent->ID AS Parent, 'ForeignKey' AS Type FROM %Dictionary.ForeignKeyDefinition" +
" UNION SELECT Name, Parent->ID AS Parent, 'XData' AS Type FROM %Dictionary.XDataDefinition" +
" UNION SELECT Name, Parent->ID AS Parent, 'Query' AS Type FROM %Dictionary.QueryDefinition" +
" UNION SELECT Name, Parent->ID AS Parent, 'Trigger' AS Type FROM %Dictionary.TriggerDefinition" +
" UNION SELECT Name, Parent->ID AS Parent, 'Storage' AS Type FROM %Dictionary.StorageDefinition" +
" UNION SELECT Name, Parent->ID AS Parent, 'Projection' AS Type FROM %Dictionary.ProjectionDefinition" +
") AS mem JOIN ";

private sqlNoSystem: string =
"SELECT dict.Name, dict.Parent, dict.Type FROM (" +
"SELECT Name, Parent->ID AS Parent, 'method' AS Type FROM %Dictionary.MethodDefinition" +
" UNION ALL %PARALLEL " +
"SELECT Name, Parent->ID AS Parent, 'property' AS Type FROM %Dictionary.PropertyDefinition" +
" UNION ALL %PARALLEL " +
"SELECT Name, Parent->ID AS Parent, 'parameter' AS Type FROM %Dictionary.ParameterDefinition" +
" UNION ALL %PARALLEL " +
"SELECT Name, Parent->ID AS Parent, 'index' AS Type FROM %Dictionary.IndexDefinition" +
" UNION ALL %PARALLEL " +
"SELECT Name, Parent->ID AS Parent, 'foreignkey' AS Type FROM %Dictionary.ForeignKeyDefinition" +
" UNION ALL %PARALLEL " +
"SELECT Name, Parent->ID AS Parent, 'xdata' AS Type FROM %Dictionary.XDataDefinition" +
" UNION ALL %PARALLEL " +
"SELECT Name, Parent->ID AS Parent, 'query' AS Type FROM %Dictionary.QueryDefinition" +
" UNION ALL %PARALLEL " +
"SELECT Name, Parent->ID AS Parent, 'trigger' AS Type FROM %Dictionary.TriggerDefinition" +
" UNION ALL %PARALLEL " +
"SELECT Name, Parent->ID AS Parent, 'storage' AS Type FROM %Dictionary.StorageDefinition" +
" UNION ALL %PARALLEL " +
"SELECT Name, Parent->ID AS Parent, 'projection' AS Type FROM %Dictionary.ProjectionDefinition" +
") AS dict, (" +
"SELECT Name FROM %Library.RoutineMgr_StudioOpenDialog(?,?,?,?,?,?,?)" +
") AS sod WHERE %SQLUPPER dict.Name %MATCHES ? AND {fn CONCAT(dict.Parent,'.cls')} = sod.Name";
private readonly _sqlPrj: string =
"%Studio.Project_ProjectItemsList(?) AS pil ON mem.Parent = pil.Name AND pil.Type = 'CLS'";

private queryResultToSymbols(data: any, folderUri: vscode.Uri) {
private readonly _sqlDocs: string =
"%Library.RoutineMgr_StudioOpenDialog(?,1,1,?,1,0,?,'Type = 4',0,?) AS sod ON mem.Parent = $EXTRACT(sod.Name,1,$LENGTH(sod.Name)-4)";

private readonly _sqlSuffix: string = " WHERE mem.Name LIKE ? ESCAPE '\\'";

/**
* Convert the query results to VS Code symbols. Needs to be typed as `any[]`
* because we aren't including ranges. They will be resolved later.
*/
private _queryResultToSymbols(data: any, wsFolder: vscode.WorkspaceFolder): any[] {
const result = [];
const uris: Map<string, vscode.Uri> = new Map();
for (const element of data.result.content) {
const kind: vscode.SymbolKind = (() => {
switch (element.Type) {
case "query":
case "method":
case "Query":
case "Method":
return vscode.SymbolKind.Method;
case "parameter":
case "Parameter":
return vscode.SymbolKind.Constant;
case "index":
case "Index":
return vscode.SymbolKind.Key;
case "xdata":
case "storage":
case "XData":
case "Storage":
return vscode.SymbolKind.Struct;
case "property":
case "Class":
return vscode.SymbolKind.Class;
default:
return vscode.SymbolKind.Property;
}
Expand All @@ -77,14 +59,21 @@ export class WorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider {
if (uris.has(element.Parent)) {
uri = uris.get(element.Parent);
} else {
uri = DocumentContentProvider.getUri(`${element.Parent}.cls`, undefined, undefined, undefined, folderUri);
uri = DocumentContentProvider.getUri(
`${element.Parent}.cls`,
wsFolder.name,
undefined,
undefined,
wsFolder.uri,
// Only "file" scheme is fully supported for client-side editing
wsFolder.uri.scheme != "file"
);
uris.set(element.Parent, uri);
}

result.push({
name: element.Name,
containerName:
element.Type === "foreignkey" ? "ForeignKey" : element.Type.charAt(0).toUpperCase() + element.Type.slice(1),
containerName: element.Type,
kind,
location: {
uri,
Expand All @@ -94,96 +83,70 @@ export class WorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider {
return result;
}

public async provideWorkspaceSymbols(query: string): Promise<vscode.SymbolInformation[]> {
if (query.length === 0) {
return null;
}
// Convert query to a %MATCHES compatible pattern
let pattern = "";
for (let i = 0; i < query.length; i++) {
const char = query.charAt(i);
pattern += char === "*" || char === "?" ? `*\\${char}` : `*${char}`;
}
pattern = pattern.toUpperCase() + "*";
// Filter the folders to search so we don't query the same ns on the same server twice
const serversToQuery: {
api: AtelierAPI;
uri: vscode.Uri;
system: boolean;
}[] = [];
for (const folder of vscode.workspace.workspaceFolders) {
const folderApi = new AtelierAPI(folder.uri);
const found = serversToQuery.findIndex(
(server) =>
server.api.config.host.toLowerCase() === folderApi.config.host.toLowerCase() &&
server.api.config.port === folderApi.config.port &&
server.api.config.pathPrefix.toLowerCase() === folderApi.config.pathPrefix.toLowerCase() &&
server.api.config.ns.toLowerCase() === folderApi.config.ns.toLowerCase()
);
if (found === -1) {
serversToQuery.push({
api: folderApi,
uri: folder.uri,
system: true,
});
} else if (serversToQuery[found].uri.scheme.startsWith("isfs") && !folder.uri.scheme.startsWith("isfs")) {
// If we have multiple folders connected to the same server and ns
// and one is not isfs, keep the non-isfs one
serversToQuery[found].uri = folder.uri;
}
}
serversToQuery.map((server) => {
if (server.api.config.ns.toLowerCase() !== "%sys") {
const found = serversToQuery.findIndex(
(server2) =>
server2.api.config.host.toLowerCase() === server.api.config.host.toLowerCase() &&
server2.api.config.port === server.api.config.port &&
server2.api.config.pathPrefix.toLowerCase() === server.api.config.pathPrefix.toLowerCase() &&
server2.api.config.ns.toLowerCase() === "%sys"
);
if (found !== -1) {
server.system = false;
}
}
return server;
});
public async provideWorkspaceSymbols(
query: string,
token: vscode.CancellationToken
): Promise<vscode.SymbolInformation[]> {
if (!vscode.workspace.workspaceFolders?.length) return;
// Convert query to a LIKE compatible pattern
let pattern = "%";
for (const c of query) pattern += `${["_", "%", "\\"].includes(c) ? "\\" : ""}${c}%`;
if (token.isCancellationRequested) return;
// Get results for all workspace folders
return Promise.allSettled(
serversToQuery
.map((server) => {
// Set the system property so we don't show system items multiple times if this
// workspace is connected to both the %SYS and a non-%SYS namespace on the same server
if (server.api.config.ns.toLowerCase() !== "%sys") {
const found = serversToQuery.findIndex(
(server2) =>
server2.api.config.host.toLowerCase() === server.api.config.host.toLowerCase() &&
server2.api.config.port === server.api.config.port &&
server2.api.config.pathPrefix.toLowerCase() === server.api.config.pathPrefix.toLowerCase() &&
server2.api.config.ns.toLowerCase() === "%sys"
);
if (found !== -1) {
server.system = false;
}
vscode.workspace.workspaceFolders.map((wsFolder) => {
if (filesystemSchemas.includes(wsFolder.uri.scheme)) {
const params = new URLSearchParams(wsFolder.uri.query);
if (params.has("csp") && ["", "1"].includes(params.get("csp"))) {
// No classes or class members in web application folders
return Promise.resolve([]);
} else {
const api = new AtelierAPI(wsFolder.uri);
if (!api.active || token.isCancellationRequested) return Promise.resolve([]);
const project = params.get("project") ?? "";
return api
.actionQuery(`${this._sqlPrefix}${project.length ? this._sqlPrj : this._sqlDocs}${this._sqlSuffix}`, [
project.length ? project : fileSpecFromURI(wsFolder.uri),
params.has("system") && params.get("system").length
? params.get("system")
: api.ns == "%SYS"
? "1"
: "0",
params.has("generated") && params.get("generated").length ? params.get("generated") : "0",
params.has("mapped") && params.get("mapped") == "0" ? "0" : "1",
pattern,
])
.then((data) => (token.isCancellationRequested ? [] : this._queryResultToSymbols(data, wsFolder)));
}
return server;
})
.map((server) =>
server.system
? server.api.actionQuery(this.sql, [pattern]).then((data) => this.queryResultToSymbols(data, server.uri))
: server.api
.actionQuery(this.sqlNoSystem, ["*.cls", "1", "1", "0", "1", "0", "0", pattern])
.then((data) => this.queryResultToSymbols(data, server.uri))
)
).then((results) => results.flatMap((result) => (result.status === "fulfilled" ? result.value : [])));
} else {
// Client-side folders should use the isfs default parameters
const api = new AtelierAPI(wsFolder.uri);
if (!api.active || token.isCancellationRequested) return Promise.resolve([]);
return api
.actionQuery(`${this._sqlPrefix}${this._sqlDocs}${this._sqlSuffix}`, ["*.cls", "0", "0", "1", pattern])
.then((data) => (token.isCancellationRequested ? [] : this._queryResultToSymbols(data, wsFolder)));
}
})
).then((results) => results.flatMap((result) => (result.status == "fulfilled" ? result.value : [])));
}

resolveWorkspaceSymbol(symbol: vscode.SymbolInformation): vscode.ProviderResult<vscode.SymbolInformation> {
return vscode.commands
.executeCommand<vscode.DocumentSymbol[]>("vscode.executeDocumentSymbolProvider", symbol.location.uri)
.then((docSymbols) => {
for (const docSymbol of docSymbols[0].children) {
if (docSymbol.name === symbol.name && docSymbol.kind === symbol.kind) {
symbol.location.range = docSymbol.selectionRange;
break;
if (!Array.isArray(docSymbols) || !docSymbols.length) return;
if (symbol.kind == vscode.SymbolKind.Class) {
symbol.location.range = docSymbols[0].selectionRange;
} else {
const memberType = symbol.containerName.toUpperCase();
const unquote = (n: string): string => {
return n[0] == '"' ? n.slice(1, -1).replace(/""/g, '"') : n;
};
for (const docSymbol of docSymbols[0].children) {
if (unquote(docSymbol.name) == symbol.name && docSymbol.detail.toUpperCase().includes(memberType)) {
symbol.location.range = docSymbol.selectionRange;
break;
}
}
}
return symbol;
Expand Down

0 comments on commit e0c6f71

Please sign in to comment.