diff --git a/src/providers/FileSystemProvider/FileSearchProvider.ts b/src/providers/FileSystemProvider/FileSearchProvider.ts index 616b74cc..db9c0f52 100644 --- a/src/providers/FileSystemProvider/FileSearchProvider.ts +++ b/src/providers/FileSystemProvider/FileSearchProvider.ts @@ -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}%'`; } diff --git a/src/providers/WorkspaceSymbolProvider.ts b/src/providers/WorkspaceSymbolProvider.ts index 9ea5ca9c..a7af302e 100644 --- a/src/providers/WorkspaceSymbolProvider.ts +++ b/src/providers/WorkspaceSymbolProvider.ts @@ -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 = 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; } @@ -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, @@ -94,96 +83,70 @@ export class WorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider { return result; } - public async provideWorkspaceSymbols(query: string): Promise { - 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 { + 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 { return vscode.commands .executeCommand("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;