Skip to content

WorkspaceSymbolProvider improvements #1366

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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