diff --git a/package.json b/package.json index 92f69b74..dccc12ed 100644 --- a/package.json +++ b/package.json @@ -225,7 +225,11 @@ }, { "command": "vscode-objectscript.showClassDocumentationPreview", - "when": "editorLangId == objectscript-class && activeCustomEditorId == ''" + "when": "false" + }, + { + "command": "vscode-objectscript.showAllClassMembers", + "when": "false" }, { "command": "vscode-objectscript.exportCurrentFile", @@ -535,14 +539,14 @@ "command": "vscode-objectscript.showClassDocumentationPreview", "group": "navigation@3", "when": "editorLangId == objectscript-class && activeCustomEditorId == ''" + }, + { + "command": "vscode-objectscript.showAllClassMembers", + "group": "navigation@0.9", + "when": "vscode-objectscript.connectActive && editorLangId == objectscript-class && activeCustomEditorId == ''" } ], "editor/title/context": [ - { - "command": "vscode-objectscript.showClassDocumentationPreview", - "group": "1_open", - "when": "resourceLangId == objectscript-class && activeCustomEditorId == ''" - }, { "command": "vscode-objectscript.showRESTDebugWebview", "group": "1_open", @@ -1194,6 +1198,12 @@ "command": "vscode-objectscript.openISCDocument", "title": "Open InterSystems Document...", "icon": "$(go-to-file)" + }, + { + "category": "ObjectScript", + "command": "vscode-objectscript.showAllClassMembers", + "title": "Show All Class Members", + "icon": "$(symbol-class)" } ], "keybindings": [ diff --git a/src/commands/documaticPreviewPanel.ts b/src/commands/documaticPreviewPanel.ts index 3bee6640..58598ae3 100644 --- a/src/commands/documaticPreviewPanel.ts +++ b/src/commands/documaticPreviewPanel.ts @@ -38,9 +38,10 @@ export class DocumaticPreviewPanel { */ public static currentPanel: DocumaticPreviewPanel | undefined; - public static create(): void { + public static create(uri: vscode.Uri): void { // Get the open document and check that it's an ObjectScript class - const openEditor = vscode.window.activeTextEditor; + const uriString = uri.toString(); + const openEditor = vscode.window.visibleTextEditors.find((e) => e.document.uri.toString() == uriString); if (openEditor === undefined) { // Need an open document to preview return; diff --git a/src/commands/showAllClassMembers.ts b/src/commands/showAllClassMembers.ts new file mode 100644 index 00000000..ebab8dce --- /dev/null +++ b/src/commands/showAllClassMembers.ts @@ -0,0 +1,176 @@ +import * as vscode from "vscode"; +import { AtelierAPI } from "../api"; +import { clsLangId, lsExtensionId } from "../extension"; +import { currentFile, handleError, stripClassMemberNameQuotes } from "../utils"; +import { DocumentContentProvider } from "../providers/DocumentContentProvider"; + +export async function showAllClassMembers(uri: vscode.Uri): Promise { + try { + // Determine the name of the class + const uriString = uri.toString(); + const textDocument = vscode.workspace.textDocuments.find((td) => td.uri.toString() == uriString); + if (textDocument?.languageId != clsLangId) { + vscode.window.showErrorMessage("The document in the active text editor is not a class definition.", "Dismiss"); + return; + } + const file = currentFile(textDocument); + if (!file) { + vscode.window.showErrorMessage("The class definition in the active text editor is malformed.", "Dismiss"); + return; + } + const cls = file.name.slice(0, -4); + const api = new AtelierAPI(file.uri); + if (!api.active) { + vscode.window.showErrorMessage("Showing all members of a class requires an active server connection.", "Dismiss"); + return; + } + // Get an array of all members + const members: { + Name: string; + Origin: string; + MemberType: "f" | "i" | "m" | "p" | "j" | "a" | "q" | "s" | "t" | "x"; + Info: string; + }[] = await api + .actionQuery( + `SELECT Name, Origin, MemberType, Info FROM ( +SELECT Name, Origin, 'f' AS MemberType, Parent, Internal, NotInheritable, '('||REPLACE(Properties,',',', ')||') References '||ReferencedClass||(CASE WHEN ReferencedKey IS NOT NULL THEN '('||ReferencedKey||')' ELSE '' END) AS Info FROM %Dictionary.CompiledForeignKey UNION +SELECT Name, Origin, 'i' AS MemberType, Parent, Internal, NotInheritable, (CASE WHEN Properties LIKE '%,%' THEN 'On ('||REPLACE(Properties,',',', ')||') ' WHEN Properties IS NOT NULL THEN 'On '||Properties||' ' ELSE '' END)||(CASE WHEN Type IS NOT NULL THEN '[ Type = '||Type||' ]' ELSE '' END) AS Info FROM %Dictionary.CompiledIndex WHERE NOT (Name %STARTSWITH '$') UNION +SELECT Name, Origin, 'm' AS MemberType, Parent, Internal, NotInheritable, '('||(CASE WHEN FormalSpec IS NULL THEN '' ELSE REPLACE(REPLACE(FormalSpec,',',', '),'=',' = ') END)||')'||(CASE WHEN ReturnType IS NOT NULL THEN ' As '||ReturnType||(CASE WHEN ReturnTypeParams IS NOT NULL THEN '('||REPLACE(ReturnTypeParams,'=',' = ')||')' ELSE '' END) ELSE '' END) AS Info FROM %Dictionary.CompiledMethod WHERE Stub IS NULL UNION +SELECT Name, Origin, 'p' AS MemberType, Parent, Internal, NotInheritable, CASE WHEN Expression IS NOT NULL THEN Expression WHEN _Default IS NOT NULL THEN _Default ELSE Type END AS Info FROM %Dictionary.CompiledParameter UNION +SELECT Name, Origin, 'j' AS MemberType, Parent, Internal, NotInheritable, Type AS Info FROM %Dictionary.CompiledProjection UNION +SELECT Name, Origin, 'a' AS MemberType, Parent, Internal, NotInheritable, CASE WHEN Collection IS NOT NULL THEN Collection||' Of '||Type ELSE Type END AS Info FROM %Dictionary.CompiledProperty UNION +SELECT Name, Origin, 'q' AS MemberType, Parent, Internal, NotInheritable, '('||(CASE WHEN FormalSpec IS NULL THEN '' ELSE REPLACE(REPLACE(FormalSpec,',',', '),'=',' = ') END)||') As '||Type AS Info FROM %Dictionary.CompiledQuery UNION +SELECT Name, Origin, 's' AS MemberType, Parent, Internal, NotInheritable, Type AS Info FROM %Dictionary.CompiledStorage UNION +SELECT Name, Origin, 't' AS MemberType, Parent, Internal, NotInheritable, Event||' '||_Time||' '||Foreach AS Info FROM %Dictionary.CompiledTrigger UNION +SELECT Name, Origin, 'x' AS MemberType, Parent, Internal, 0 AS NotInheritable, MimeType||(CASE WHEN SUBSTR(MimeType,-4) = '/xml' AND XMLNamespace IS NOT NULL THEN ' ('||XMLNamespace||')' ELSE '' END) AS Info FROM %Dictionary.CompiledXData +) WHERE Parent = ? AND ((NotInheritable = 0 AND Internal = 0) OR (Origin = Parent)) ORDER BY Name`.replaceAll( + "\n", + " " + ), + [cls] + ) + .then((data) => data?.result?.content ?? []); + if (!members.length) { + vscode.window.showWarningMessage( + "The server returned no members for this class. If members are expected, re-compile the class then try again.", + "Dismiss" + ); + return; + } + // Prompt the user to pick one + const member = await vscode.window.showQuickPick( + // Convert the query rows into QuickPickItems + members.map((m) => { + const [iconId, memberType] = (() => { + switch (m.MemberType) { + case "m": + return ["method", "Method"]; + case "q": + return ["function", "Query"]; + case "t": + return ["event", "Trigger"]; + case "p": + return ["constant", "Parameter"]; + case "i": + return ["array", "Index"]; + case "f": + return ["key", "ForeignKey"]; + case "x": + return ["struct", "XData"]; + case "s": + return ["object", "Storage"]; + case "j": + return ["interface", "Projection"]; + default: + return ["property", "Property"]; + } + })(); + let detail = m.Info; + if ("mq".includes(m.MemberType)) { + // Need to beautify the argument list + detail = ""; + let inQuotes = false; + let braceDepth = 0; + for (const c of m.Info) { + if (c == '"') { + inQuotes = !inQuotes; + detail += c; + continue; + } + if (!inQuotes) { + if (c == "{") { + braceDepth++; + detail += c; + continue; + } else if (c == "}") { + braceDepth = Math.max(0, braceDepth - 1); + detail += c; + continue; + } + } + if (!inQuotes && braceDepth == 0 && ":&*=".includes(c)) { + detail += c == ":" ? " As " : c == "&" ? "ByRef " : c == "*" ? "Output " : " = "; + } else { + detail += c; + } + } + } + return { + label: m.Name, + description: m.Origin, + detail, + iconPath: new vscode.ThemeIcon(`symbol-${iconId}`), + memberType, + }; + }), + { + title: `All members of ${cls}`, + placeHolder: "Pick a member to show it in the editor", + } + ); + if (!member) return; + // Show the picked member + const targetUri = + member.description == cls + ? uri + : DocumentContentProvider.getUri( + `${member.description}.cls`, + undefined, + undefined, + undefined, + vscode.workspace.getWorkspaceFolder(uri)?.uri + ); + const symbols = ( + await vscode.commands.executeCommand("vscode.executeDocumentSymbolProvider", targetUri) + )[0]?.children; + // Find the symbol for this member + const memberType = member.memberType.toLowerCase(); + const symbol = symbols?.find( + (s) => + stripClassMemberNameQuotes(s.name) == member.label && + (memberType == "method" + ? s.detail.toLowerCase().includes(memberType) + : memberType == "property" + ? ["property", "relationship"].includes(s.detail.toLowerCase()) + : s.detail.toLowerCase() == memberType) + ); + if (!symbol) { + vscode.window.showErrorMessage( + `Did not find ${member.memberType} '${member.label}' in class '${member.description}'.`, + "Dismiss" + ); + return; + } + // If Language Server is active, selectionRange is the member name. + // Else, range is the first line of the member definition excluding description. + const position = vscode.extensions.getExtension(lsExtensionId)?.isActive + ? symbol.selectionRange.start + : symbol.range.start; + await vscode.window.showTextDocument(targetUri, { + selection: new vscode.Range(position, position), + preview: false, + }); + } catch (error) { + handleError(error, "Failed to show all class members."); + } +} diff --git a/src/extension.ts b/src/extension.ts index 5ed42f0f..45c1a4e0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -158,6 +158,7 @@ import { import { WorkspaceNode, NodeBase } from "./explorer/nodes"; import { showPlanWebview } from "./commands/showPlanPanel"; import { isfsConfig } from "./utils/FileProviderUtil"; +import { showAllClassMembers } from "./commands/showAllClassMembers"; const packageJson = vscode.extensions.getExtension(extensionId).packageJSON; const extensionVersion = packageJson.version; @@ -886,21 +887,8 @@ export async function activate(context: vscode.ExtensionContext): Promise { documentContentProvider = new DocumentContentProvider(); fileSystemProvider = new FileSystemProvider(); - explorerProvider = new ObjectScriptExplorerProvider(); - vscode.window.createTreeView("ObjectScriptExplorer", { - treeDataProvider: explorerProvider, - showCollapseAll: true, - canSelectMany: true, - }); - projectsExplorerProvider = new ProjectsExplorerProvider(); - vscode.window.createTreeView("ObjectScriptProjectsExplorer", { - treeDataProvider: projectsExplorerProvider, - showCollapseAll: true, - canSelectMany: false, - }); - posPanel = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 0); posPanel.command = "vscode-objectscript.jumpToTagAndOffset"; posPanel.show(); @@ -1125,11 +1113,28 @@ export async function activate(context: vscode.ExtensionContext): Promise { } }), vscode.window.onDidChangeActiveTextEditor(async (editor) => { - if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 1) { + if (vscode.workspace.workspaceFolders?.length > 1) { const workspaceFolder = currentWorkspaceFolder(); - if (workspaceFolder && workspaceFolder !== workspaceState.get("workspaceFolder")) { + if (workspaceFolder && workspaceFolder != workspaceState.get("workspaceFolder")) { await workspaceState.update("workspaceFolder", workspaceFolder); - await checkConnection(false, editor?.document.uri); + // Only need to check when editor is undefined because + // we will always check when editor is defined below + if (!editor) await checkConnection(false); + } + } + if (editor) { + const conf = vscode.workspace.getConfiguration("objectscript"); + const uriString = editor.document.uri.toString(); + await checkConnection(false, editor.document.uri); + if (conf.get("autoPreviewXML") && editor.document.uri.path.toLowerCase().endsWith("xml")) { + previewXMLAsUDL(editor, true); + } else if ( + conf.get("openClassContracted") && + editor.document.languageId == clsLangId && + !openedClasses.includes(uriString) + ) { + vscode.commands.executeCommand("editor.foldLevel1"); + openedClasses.push(uriString); } } }), @@ -1422,9 +1427,9 @@ export async function activate(context: vscode.ExtensionContext): Promise { sendCommandTelemetryEvent("editOthers"); viewOthers(true); }), - vscode.commands.registerCommand("vscode-objectscript.showClassDocumentationPreview", () => { + vscode.commands.registerCommand("vscode-objectscript.showClassDocumentationPreview", (uri: vscode.Uri) => { sendCommandTelemetryEvent("showClassDocumentationPreview"); - DocumaticPreviewPanel.create(); + if (uri instanceof vscode.Uri) DocumaticPreviewPanel.create(uri); }), vscode.commands.registerCommand("vscode-objectscript.showRESTDebugWebview", () => { sendCommandTelemetryEvent("showRESTDebugWebview"); @@ -1492,15 +1497,6 @@ export async function activate(context: vscode.ExtensionContext): Promise { }) ); }), - vscode.window.onDidChangeActiveTextEditor((editor: vscode.TextEditor) => { - if (config("openClassContracted") && editor && editor.document.languageId === clsLangId) { - const uri: string = editor.document.uri.toString(); - if (!openedClasses.includes(uri)) { - vscode.commands.executeCommand("editor.foldLevel1"); - openedClasses.push(uri); - } - } - }), vscode.workspace.onDidCloseTextDocument((doc: vscode.TextDocument) => { const uri: string = doc.uri.toString(); const idx: number = openedClasses.indexOf(uri); @@ -1618,13 +1614,6 @@ export async function activate(context: vscode.ExtensionContext): Promise { terminals.splice(terminalIndex, 1); } }), - vscode.window.onDidChangeActiveTextEditor(async (textEditor: vscode.TextEditor) => { - if (!textEditor) return; - await checkConnection(false, textEditor.document.uri); - if (textEditor.document.uri.path.toLowerCase().endsWith(".xml") && config("autoPreviewXML")) { - return previewXMLAsUDL(textEditor, true); - } - }), vscode.window.onDidChangeTextEditorSelection(async (event: vscode.TextEditorSelectionChangeEvent) => { const document = event.textEditor.document; // Avoid losing position indicator if event came from output channel or a non-active editor @@ -1894,6 +1883,20 @@ export async function activate(context: vscode.ExtensionContext): Promise { if (typeof args != "object") return; showPlanWebview(args); }), + vscode.window.createTreeView("ObjectScriptExplorer", { + treeDataProvider: explorerProvider, + showCollapseAll: true, + canSelectMany: true, + }), + vscode.window.createTreeView("ObjectScriptProjectsExplorer", { + treeDataProvider: projectsExplorerProvider, + showCollapseAll: true, + canSelectMany: false, + }), + vscode.commands.registerCommand("vscode-objectscript.showAllClassMembers", (uri: vscode.Uri) => { + sendCommandTelemetryEvent("showAllClassMembers"); + if (uri instanceof vscode.Uri) showAllClassMembers(uri); + }), // These three listeners are needed to keep track of which file events were caused by VS Code // to support the "vscodeOnly" option for the objectscript.syncLocalChanges setting. // They store the URIs of files that are about to be changed by VS Code. diff --git a/src/utils/index.ts b/src/utils/index.ts index 7a4cb880..210ab744 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -110,7 +110,9 @@ export function outputConsole(data: string[]): void { } export interface CurrentFile { + /** The name of the document, like `User.Test.cls` */ name: string; + /** `uri.fsPath` */ fileName: string; uri: vscode.Uri; unredirectedUri?: vscode.Uri;