Skip to content

Commit 8346209

Browse files
authored
Add command for showing all class members, including inherited (#1656)
1 parent ffc47fe commit 8346209

File tree

5 files changed

+234
-42
lines changed

5 files changed

+234
-42
lines changed

package.json

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,11 @@
225225
},
226226
{
227227
"command": "vscode-objectscript.showClassDocumentationPreview",
228-
"when": "editorLangId == objectscript-class && activeCustomEditorId == ''"
228+
"when": "false"
229+
},
230+
{
231+
"command": "vscode-objectscript.showAllClassMembers",
232+
"when": "false"
229233
},
230234
{
231235
"command": "vscode-objectscript.exportCurrentFile",
@@ -535,14 +539,14 @@
535539
"command": "vscode-objectscript.showClassDocumentationPreview",
536540
"group": "navigation@3",
537541
"when": "editorLangId == objectscript-class && activeCustomEditorId == ''"
542+
},
543+
{
544+
"command": "vscode-objectscript.showAllClassMembers",
545+
"group": "[email protected]",
546+
"when": "vscode-objectscript.connectActive && editorLangId == objectscript-class && activeCustomEditorId == ''"
538547
}
539548
],
540549
"editor/title/context": [
541-
{
542-
"command": "vscode-objectscript.showClassDocumentationPreview",
543-
"group": "1_open",
544-
"when": "resourceLangId == objectscript-class && activeCustomEditorId == ''"
545-
},
546550
{
547551
"command": "vscode-objectscript.showRESTDebugWebview",
548552
"group": "1_open",
@@ -1194,6 +1198,12 @@
11941198
"command": "vscode-objectscript.openISCDocument",
11951199
"title": "Open InterSystems Document...",
11961200
"icon": "$(go-to-file)"
1201+
},
1202+
{
1203+
"category": "ObjectScript",
1204+
"command": "vscode-objectscript.showAllClassMembers",
1205+
"title": "Show All Class Members",
1206+
"icon": "$(symbol-class)"
11971207
}
11981208
],
11991209
"keybindings": [

src/commands/documaticPreviewPanel.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,10 @@ export class DocumaticPreviewPanel {
3838
*/
3939
public static currentPanel: DocumaticPreviewPanel | undefined;
4040

41-
public static create(): void {
41+
public static create(uri: vscode.Uri): void {
4242
// Get the open document and check that it's an ObjectScript class
43-
const openEditor = vscode.window.activeTextEditor;
43+
const uriString = uri.toString();
44+
const openEditor = vscode.window.visibleTextEditors.find((e) => e.document.uri.toString() == uriString);
4445
if (openEditor === undefined) {
4546
// Need an open document to preview
4647
return;
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import * as vscode from "vscode";
2+
import { AtelierAPI } from "../api";
3+
import { clsLangId, lsExtensionId } from "../extension";
4+
import { currentFile, handleError, stripClassMemberNameQuotes } from "../utils";
5+
import { DocumentContentProvider } from "../providers/DocumentContentProvider";
6+
7+
export async function showAllClassMembers(uri: vscode.Uri): Promise<void> {
8+
try {
9+
// Determine the name of the class
10+
const uriString = uri.toString();
11+
const textDocument = vscode.workspace.textDocuments.find((td) => td.uri.toString() == uriString);
12+
if (textDocument?.languageId != clsLangId) {
13+
vscode.window.showErrorMessage("The document in the active text editor is not a class definition.", "Dismiss");
14+
return;
15+
}
16+
const file = currentFile(textDocument);
17+
if (!file) {
18+
vscode.window.showErrorMessage("The class definition in the active text editor is malformed.", "Dismiss");
19+
return;
20+
}
21+
const cls = file.name.slice(0, -4);
22+
const api = new AtelierAPI(file.uri);
23+
if (!api.active) {
24+
vscode.window.showErrorMessage("Showing all members of a class requires an active server connection.", "Dismiss");
25+
return;
26+
}
27+
// Get an array of all members
28+
const members: {
29+
Name: string;
30+
Origin: string;
31+
MemberType: "f" | "i" | "m" | "p" | "j" | "a" | "q" | "s" | "t" | "x";
32+
Info: string;
33+
}[] = await api
34+
.actionQuery(
35+
`SELECT Name, Origin, MemberType, Info FROM (
36+
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
37+
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
38+
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
39+
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
40+
SELECT Name, Origin, 'j' AS MemberType, Parent, Internal, NotInheritable, Type AS Info FROM %Dictionary.CompiledProjection UNION
41+
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
42+
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
43+
SELECT Name, Origin, 's' AS MemberType, Parent, Internal, NotInheritable, Type AS Info FROM %Dictionary.CompiledStorage UNION
44+
SELECT Name, Origin, 't' AS MemberType, Parent, Internal, NotInheritable, Event||' '||_Time||' '||Foreach AS Info FROM %Dictionary.CompiledTrigger UNION
45+
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
46+
) WHERE Parent = ? AND ((NotInheritable = 0 AND Internal = 0) OR (Origin = Parent)) ORDER BY Name`.replaceAll(
47+
"\n",
48+
" "
49+
),
50+
[cls]
51+
)
52+
.then((data) => data?.result?.content ?? []);
53+
if (!members.length) {
54+
vscode.window.showWarningMessage(
55+
"The server returned no members for this class. If members are expected, re-compile the class then try again.",
56+
"Dismiss"
57+
);
58+
return;
59+
}
60+
// Prompt the user to pick one
61+
const member = await vscode.window.showQuickPick(
62+
// Convert the query rows into QuickPickItems
63+
members.map((m) => {
64+
const [iconId, memberType] = (() => {
65+
switch (m.MemberType) {
66+
case "m":
67+
return ["method", "Method"];
68+
case "q":
69+
return ["function", "Query"];
70+
case "t":
71+
return ["event", "Trigger"];
72+
case "p":
73+
return ["constant", "Parameter"];
74+
case "i":
75+
return ["array", "Index"];
76+
case "f":
77+
return ["key", "ForeignKey"];
78+
case "x":
79+
return ["struct", "XData"];
80+
case "s":
81+
return ["object", "Storage"];
82+
case "j":
83+
return ["interface", "Projection"];
84+
default:
85+
return ["property", "Property"];
86+
}
87+
})();
88+
let detail = m.Info;
89+
if ("mq".includes(m.MemberType)) {
90+
// Need to beautify the argument list
91+
detail = "";
92+
let inQuotes = false;
93+
let braceDepth = 0;
94+
for (const c of m.Info) {
95+
if (c == '"') {
96+
inQuotes = !inQuotes;
97+
detail += c;
98+
continue;
99+
}
100+
if (!inQuotes) {
101+
if (c == "{") {
102+
braceDepth++;
103+
detail += c;
104+
continue;
105+
} else if (c == "}") {
106+
braceDepth = Math.max(0, braceDepth - 1);
107+
detail += c;
108+
continue;
109+
}
110+
}
111+
if (!inQuotes && braceDepth == 0 && ":&*=".includes(c)) {
112+
detail += c == ":" ? " As " : c == "&" ? "ByRef " : c == "*" ? "Output " : " = ";
113+
} else {
114+
detail += c;
115+
}
116+
}
117+
}
118+
return {
119+
label: m.Name,
120+
description: m.Origin,
121+
detail,
122+
iconPath: new vscode.ThemeIcon(`symbol-${iconId}`),
123+
memberType,
124+
};
125+
}),
126+
{
127+
title: `All members of ${cls}`,
128+
placeHolder: "Pick a member to show it in the editor",
129+
}
130+
);
131+
if (!member) return;
132+
// Show the picked member
133+
const targetUri =
134+
member.description == cls
135+
? uri
136+
: DocumentContentProvider.getUri(
137+
`${member.description}.cls`,
138+
undefined,
139+
undefined,
140+
undefined,
141+
vscode.workspace.getWorkspaceFolder(uri)?.uri
142+
);
143+
const symbols = (
144+
await vscode.commands.executeCommand<vscode.DocumentSymbol[]>("vscode.executeDocumentSymbolProvider", targetUri)
145+
)[0]?.children;
146+
// Find the symbol for this member
147+
const memberType = member.memberType.toLowerCase();
148+
const symbol = symbols?.find(
149+
(s) =>
150+
stripClassMemberNameQuotes(s.name) == member.label &&
151+
(memberType == "method"
152+
? s.detail.toLowerCase().includes(memberType)
153+
: memberType == "property"
154+
? ["property", "relationship"].includes(s.detail.toLowerCase())
155+
: s.detail.toLowerCase() == memberType)
156+
);
157+
if (!symbol) {
158+
vscode.window.showErrorMessage(
159+
`Did not find ${member.memberType} '${member.label}' in class '${member.description}'.`,
160+
"Dismiss"
161+
);
162+
return;
163+
}
164+
// If Language Server is active, selectionRange is the member name.
165+
// Else, range is the first line of the member definition excluding description.
166+
const position = vscode.extensions.getExtension(lsExtensionId)?.isActive
167+
? symbol.selectionRange.start
168+
: symbol.range.start;
169+
await vscode.window.showTextDocument(targetUri, {
170+
selection: new vscode.Range(position, position),
171+
preview: false,
172+
});
173+
} catch (error) {
174+
handleError(error, "Failed to show all class members.");
175+
}
176+
}

src/extension.ts

Lines changed: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ import {
158158
import { WorkspaceNode, NodeBase } from "./explorer/nodes";
159159
import { showPlanWebview } from "./commands/showPlanPanel";
160160
import { isfsConfig } from "./utils/FileProviderUtil";
161+
import { showAllClassMembers } from "./commands/showAllClassMembers";
161162

162163
const packageJson = vscode.extensions.getExtension(extensionId).packageJSON;
163164
const extensionVersion = packageJson.version;
@@ -886,21 +887,8 @@ export async function activate(context: vscode.ExtensionContext): Promise<any> {
886887

887888
documentContentProvider = new DocumentContentProvider();
888889
fileSystemProvider = new FileSystemProvider();
889-
890890
explorerProvider = new ObjectScriptExplorerProvider();
891-
vscode.window.createTreeView("ObjectScriptExplorer", {
892-
treeDataProvider: explorerProvider,
893-
showCollapseAll: true,
894-
canSelectMany: true,
895-
});
896-
897891
projectsExplorerProvider = new ProjectsExplorerProvider();
898-
vscode.window.createTreeView("ObjectScriptProjectsExplorer", {
899-
treeDataProvider: projectsExplorerProvider,
900-
showCollapseAll: true,
901-
canSelectMany: false,
902-
});
903-
904892
posPanel = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 0);
905893
posPanel.command = "vscode-objectscript.jumpToTagAndOffset";
906894
posPanel.show();
@@ -1125,11 +1113,28 @@ export async function activate(context: vscode.ExtensionContext): Promise<any> {
11251113
}
11261114
}),
11271115
vscode.window.onDidChangeActiveTextEditor(async (editor) => {
1128-
if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 1) {
1116+
if (vscode.workspace.workspaceFolders?.length > 1) {
11291117
const workspaceFolder = currentWorkspaceFolder();
1130-
if (workspaceFolder && workspaceFolder !== workspaceState.get<string>("workspaceFolder")) {
1118+
if (workspaceFolder && workspaceFolder != workspaceState.get<string>("workspaceFolder")) {
11311119
await workspaceState.update("workspaceFolder", workspaceFolder);
1132-
await checkConnection(false, editor?.document.uri);
1120+
// Only need to check when editor is undefined because
1121+
// we will always check when editor is defined below
1122+
if (!editor) await checkConnection(false);
1123+
}
1124+
}
1125+
if (editor) {
1126+
const conf = vscode.workspace.getConfiguration("objectscript");
1127+
const uriString = editor.document.uri.toString();
1128+
await checkConnection(false, editor.document.uri);
1129+
if (conf.get("autoPreviewXML") && editor.document.uri.path.toLowerCase().endsWith("xml")) {
1130+
previewXMLAsUDL(editor, true);
1131+
} else if (
1132+
conf.get("openClassContracted") &&
1133+
editor.document.languageId == clsLangId &&
1134+
!openedClasses.includes(uriString)
1135+
) {
1136+
vscode.commands.executeCommand("editor.foldLevel1");
1137+
openedClasses.push(uriString);
11331138
}
11341139
}
11351140
}),
@@ -1422,9 +1427,9 @@ export async function activate(context: vscode.ExtensionContext): Promise<any> {
14221427
sendCommandTelemetryEvent("editOthers");
14231428
viewOthers(true);
14241429
}),
1425-
vscode.commands.registerCommand("vscode-objectscript.showClassDocumentationPreview", () => {
1430+
vscode.commands.registerCommand("vscode-objectscript.showClassDocumentationPreview", (uri: vscode.Uri) => {
14261431
sendCommandTelemetryEvent("showClassDocumentationPreview");
1427-
DocumaticPreviewPanel.create();
1432+
if (uri instanceof vscode.Uri) DocumaticPreviewPanel.create(uri);
14281433
}),
14291434
vscode.commands.registerCommand("vscode-objectscript.showRESTDebugWebview", () => {
14301435
sendCommandTelemetryEvent("showRESTDebugWebview");
@@ -1492,15 +1497,6 @@ export async function activate(context: vscode.ExtensionContext): Promise<any> {
14921497
})
14931498
);
14941499
}),
1495-
vscode.window.onDidChangeActiveTextEditor((editor: vscode.TextEditor) => {
1496-
if (config("openClassContracted") && editor && editor.document.languageId === clsLangId) {
1497-
const uri: string = editor.document.uri.toString();
1498-
if (!openedClasses.includes(uri)) {
1499-
vscode.commands.executeCommand("editor.foldLevel1");
1500-
openedClasses.push(uri);
1501-
}
1502-
}
1503-
}),
15041500
vscode.workspace.onDidCloseTextDocument((doc: vscode.TextDocument) => {
15051501
const uri: string = doc.uri.toString();
15061502
const idx: number = openedClasses.indexOf(uri);
@@ -1618,13 +1614,6 @@ export async function activate(context: vscode.ExtensionContext): Promise<any> {
16181614
terminals.splice(terminalIndex, 1);
16191615
}
16201616
}),
1621-
vscode.window.onDidChangeActiveTextEditor(async (textEditor: vscode.TextEditor) => {
1622-
if (!textEditor) return;
1623-
await checkConnection(false, textEditor.document.uri);
1624-
if (textEditor.document.uri.path.toLowerCase().endsWith(".xml") && config("autoPreviewXML")) {
1625-
return previewXMLAsUDL(textEditor, true);
1626-
}
1627-
}),
16281617
vscode.window.onDidChangeTextEditorSelection(async (event: vscode.TextEditorSelectionChangeEvent) => {
16291618
const document = event.textEditor.document;
16301619
// 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<any> {
18941883
if (typeof args != "object") return;
18951884
showPlanWebview(args);
18961885
}),
1886+
vscode.window.createTreeView("ObjectScriptExplorer", {
1887+
treeDataProvider: explorerProvider,
1888+
showCollapseAll: true,
1889+
canSelectMany: true,
1890+
}),
1891+
vscode.window.createTreeView("ObjectScriptProjectsExplorer", {
1892+
treeDataProvider: projectsExplorerProvider,
1893+
showCollapseAll: true,
1894+
canSelectMany: false,
1895+
}),
1896+
vscode.commands.registerCommand("vscode-objectscript.showAllClassMembers", (uri: vscode.Uri) => {
1897+
sendCommandTelemetryEvent("showAllClassMembers");
1898+
if (uri instanceof vscode.Uri) showAllClassMembers(uri);
1899+
}),
18971900
// These three listeners are needed to keep track of which file events were caused by VS Code
18981901
// to support the "vscodeOnly" option for the objectscript.syncLocalChanges setting.
18991902
// They store the URIs of files that are about to be changed by VS Code.

src/utils/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,9 @@ export function outputConsole(data: string[]): void {
110110
}
111111

112112
export interface CurrentFile {
113+
/** The name of the document, like `User.Test.cls` */
113114
name: string;
115+
/** `uri.fsPath` */
114116
fileName: string;
115117
uri: vscode.Uri;
116118
unredirectedUri?: vscode.Uri;

0 commit comments

Comments
 (0)