|
| 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 | +} |
0 commit comments