Skip to content

Overhaul WorkspaceSymbolProvider #772

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
1 change: 0 additions & 1 deletion docs/SettingsReference.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ The extensions in the InterSystems ObjectScript Extension Pack provide many sett
| `"objectscript.ignoreInstallServerManager"` | Do not offer to install the [intersystems-community.servermanager](https://marketplace.visualstudio.com/items?itemName=intersystems-community.servermanager) extension. | `boolean` | `false` | |
| `"objectscript.multilineMethodArgs"` | List method arguments on multiple lines, if the server supports it. | `boolean` | `false` | Only supported on IRIS 2019.1.2, 2020.1.1+, 2021.1.0+ and subsequent versions! On all other versions, this setting will have no effect. |
| `"objectscript.overwriteServerChanges"` | Overwrite a changed server version without confirmation when importing the local file. | `boolean` | `false` | |
| `"objectscript.searchAllDocTypes"` | Whether [Quick Open](https://code.visualstudio.com/docs/getstarted/tips-and-tricks#_quick-open) should search across all Studio Document types. Default is to only search classes, routines and include files. | `boolean` | `false` | |
| `"objectscript.serverSideEditing"` | Allow editing code directly on the server after opening it from ObjectScript Explorer. | `boolean` | `false` | |
| `"objectscript.serverSourceControl.disableOtherActionTriggers"` | Prevent server-side source control 'other action' triggers from firing. | `boolean` | `false` | |
| `"objectscript.showExplorer"` | Show the ObjectScript Explorer view. | `boolean` | `true` | |
Expand Down
5 changes: 0 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -913,11 +913,6 @@
"default": "cuk",
"markdownDescription": "Compilation flags. Common compilation flags are ***b*** (compile dependent classes), ***k*** (keep generated source code) and ***u*** (skip related up-to-date documents). For descriptions of all available flags and qualifiers, click [here](https://docs.intersystems.com/irislatest/csp/docbook/Doc.View.cls?KEY=RCOS_vsystem#RCOS_vsystem_flags_qualifiers)."
},
"objectscript.searchAllDocTypes": {
"type": "boolean",
"default": false,
"markdownDescription": "Whether [Quick Open](https://code.visualstudio.com/docs/getstarted/tips-and-tricks#_quick-open) should search across all Studio Document types. Default is to only search classes, routines and include files."
},
"objectscript.overwriteServerChanges": {
"type": "boolean",
"default": false,
Expand Down
157 changes: 85 additions & 72 deletions src/providers/WorkspaceSymbolProvider.ts
Original file line number Diff line number Diff line change
@@ -1,85 +1,98 @@
import * as vscode from "vscode";
import { AtelierAPI } from "../api";
import { ClassDefinition } from "../utils/classDefinition";
import { currentWorkspaceFolder } from "../utils";
import { DocumentContentProvider } from "./DocumentContentProvider";
import { config } from "../extension";

export class WorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider {
public provideWorkspaceSymbols(
query: string,
token: vscode.CancellationToken
): vscode.ProviderResult<vscode.SymbolInformation[]> {
if (query.length < 3) {
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 ?";

public provideWorkspaceSymbols(query: string): vscode.ProviderResult<vscode.SymbolInformation[]> {
if (query.length === 0) {
return null;
}
return Promise.all([this.byStudioDocuments(query), this.byMethods(query)]).then(([documents, methods]) => [
...documents,
...methods,
]);
}

private getApi(): AtelierAPI {
const currentFileUri = vscode.window.activeTextEditor?.document.uri;
const firstFolder = vscode.workspace.workspaceFolders?.length ? vscode.workspace.workspaceFolders[0] : undefined;
return new AtelierAPI(currentFileUri || firstFolder?.uri || "");
}

private async byStudioDocuments(query: string): Promise<vscode.SymbolInformation[]> {
const searchAllDocTypes = config("searchAllDocTypes");
if (searchAllDocTypes) {
// Note: This query could be expensive if there are too many files available across the namespaces
// configured in the current vs code workspace. However, delimiting by specific file types
// means custom Studio documents cannot be found. So this is a trade off
query = `*${query}*`;
} else {
// Default is to only search classes, routines and include files
query = `*${query}*.cls,*${query}*.mac,*${query}*.int,*${query}*.inc`;
let pattern = "";
for (let i = 0; i < query.length; i++) {
const char = query.charAt(i);
pattern += char === "*" || char === "?" ? `*\\${char}` : `*${char}`;
}
const sql = `SELECT TOP 10 Name FROM %Library.RoutineMgr_StudioOpenDialog(?,?,?,?,?,?,?)`;
const api = this.getApi();
const direction = "1";
const orderBy = "1";
const systemFiles = "1";
const flat = "1";
const notStudio = "0";
const generated = "0";
const workspace = currentWorkspaceFolder();
const api = new AtelierAPI(workspace);
return api.actionQuery(this.sql, [pattern.toUpperCase() + "*"]).then((data) => {
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":
return vscode.SymbolKind.Method;
case "parameter":
return vscode.SymbolKind.Constant;
case "index":
return vscode.SymbolKind.Key;
case "xdata":
case "storage":
return vscode.SymbolKind.Struct;
case "property":
default:
return vscode.SymbolKind.Property;
}
})();

let uri: vscode.Uri;
if (uris.has(element.Parent)) {
uri = uris.get(element.Parent);
} else {
uri = DocumentContentProvider.getUri(`${element.Parent}.cls`, workspace);
uris.set(element.Parent, uri);
}

const kindFromName = (name: string) => {
const nameLowerCase = name.toLowerCase();
return nameLowerCase.endsWith("cls")
? vscode.SymbolKind.Class
: nameLowerCase.endsWith("zpm")
? vscode.SymbolKind.Module
: vscode.SymbolKind.File;
};
const data = await api.actionQuery(sql, [query, direction, orderBy, systemFiles, flat, notStudio, generated]);
return data.result.content.map(({ Name }) => ({
kind: kindFromName(Name),
location: {
uri: DocumentContentProvider.getUri(Name, undefined, api.ns),
},
name: Name,
}));
result.push({
name: element.Name,
containerName:
element.Type === "foreignkey" ? "ForeignKey" : element.Type.charAt(0).toUpperCase() + element.Type.slice(1),
kind,
location: {
uri,
},
});
}
return result;
});
}

private async byMethods(query: string): Promise<vscode.SymbolInformation[]> {
const api = this.getApi();
query = query.toUpperCase();
query = `*${query}*`;
const getLocation = async (className, name) => {
const classDef = new ClassDefinition(className, undefined, api.ns);
return classDef.getMemberLocation(name);
};
const sql = `
SELECT TOP 10 Parent ClassName, Name FROM %Dictionary.MethodDefinition WHERE %SQLUPPER Name %MATCHES ?`;
return api
.actionQuery(sql, [query])
.then((data): Promise<vscode.SymbolInformation>[] =>
data.result.content.map(
async ({ ClassName, Name }): Promise<vscode.SymbolInformation> =>
new vscode.SymbolInformation(Name, vscode.SymbolKind.Method, ClassName, await getLocation(ClassName, Name))
)
)
.then((data) => Promise.all(data));
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;
}
}
return symbol;
});
}
}