diff --git a/vscode/package.json b/vscode/package.json index 21a7dfbb6..c4803d22f 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -140,6 +140,11 @@ "title": "Ruby file operations", "category": "Ruby LSP", "icon": "$(ruby)" + }, + { + "command": "rubyLsp.collectRubyLspInfo", + "title": "Collect Ruby LSP Information for Issue Reporting", + "category": "Ruby LSP" } ], "configuration": { diff --git a/vscode/src/common.ts b/vscode/src/common.ts index 9a868a135..ab6434592 100644 --- a/vscode/src/common.ts +++ b/vscode/src/common.ts @@ -26,6 +26,7 @@ export enum Command { RailsGenerate = "rubyLsp.railsGenerate", RailsDestroy = "rubyLsp.railsDestroy", NewMinitestFile = "rubyLsp.newMinitestFile", + CollectRubyLspInfo = "rubyLsp.collectRubyLspInfo", } export interface RubyInterface { diff --git a/vscode/src/infoCollector.ts b/vscode/src/infoCollector.ts new file mode 100644 index 000000000..4198a79e8 --- /dev/null +++ b/vscode/src/infoCollector.ts @@ -0,0 +1,173 @@ +import * as vscode from "vscode"; + +import { Workspace } from "./workspace"; + +export async function collectRubyLspInfo(workspace: Workspace | undefined) { + if (!workspace) { + await vscode.window.showErrorMessage("No active Ruby workspace found."); + return; + } + + const lspInfo = await gatherLspInfo(workspace); + const panel = vscode.window.createWebviewPanel( + "rubyLspInfo", + "Ruby LSP Information", + vscode.ViewColumn.One, + { enableScripts: true }, + ); + + panel.webview.html = generateRubyLspInfoReport(lspInfo); +} + +async function gatherLspInfo( + workspace: Workspace, +): Promise>> { + const vscodeVersion = vscode.version; + const rubyLspExtension = vscode.extensions.getExtension("Shopify.ruby-lsp")!; + const rubyLspExtensionVersion = rubyLspExtension.packageJSON.version; + const rubyLspVersion = workspace.lspClient?.serverVersion ?? "Unknown"; + const rubyLspAddons = + workspace.lspClient?.addons?.map((addon) => addon.name) ?? []; + const extensions = await getPublicExtensions(); + + // Fetch rubyLsp settings + const workspaceSettings = vscode.workspace.getConfiguration( + "rubyLsp", + workspace.workspaceFolder, + ); + const userSettings = vscode.workspace.getConfiguration("rubyLsp"); + + // Get only the workspace-specific settings + const workspaceSpecificSettings: Record = {}; + for (const key of Object.keys(workspaceSettings)) { + if (workspaceSettings.inspect(key)?.workspaceValue !== undefined) { + workspaceSpecificSettings[key] = workspaceSettings.get(key); + } + } + + return { + /* eslint-disable @typescript-eslint/naming-convention */ + "VS Code Version": vscodeVersion, + "Ruby LSP Extension Version": rubyLspExtensionVersion, + "Ruby LSP Server Version": rubyLspVersion, + "Ruby LSP Addons": rubyLspAddons, + "Ruby Version": workspace.ruby.rubyVersion ?? "Unknown", + "Ruby Version Manager": workspace.ruby.versionManager.identifier, + "Installed Extensions": extensions, + "Ruby LSP Settings": { + Workspace: workspaceSpecificSettings, + User: userSettings, + }, + /* eslint-enable @typescript-eslint/naming-convention */ + }; +} + +async function getPublicExtensions(): Promise { + return vscode.extensions.all + .filter((ext) => { + // Filter out built-in extensions + if (ext.packageJSON.isBuiltin) { + return false; + } + + // Assume if an extension doesn't have a license, it's private and should not be listed + if ( + ext.packageJSON.license === "UNLICENSED" || + !ext.packageJSON.license + ) { + return false; + } + + return true; + }) + .map((ext) => `${ext.packageJSON.name} (${ext.packageJSON.version})`); +} + +function generateRubyLspInfoReport( + info: Record>, +): string { + let markdown = "\n### Ruby LSP Information\n\n"; + + for (const [key, value] of Object.entries(info)) { + markdown += `#### ${key}\n\n`; + if (Array.isArray(value)) { + if (key === "Installed Extensions") { + markdown += + "<details>\n<summary>Click to expand</summary>\n\n"; + markdown += `${value.map((val) => `- ${val}`).join("\n")}\n`; + markdown += "</details>\n"; + } else { + markdown += `${value.map((val) => `- ${val}`).join("\n")}\n`; + } + } else if (typeof value === "object" && value !== null) { + markdown += + "<details>\n<summary>Click to expand</summary>\n\n"; + for (const [subKey, subValue] of Object.entries(value)) { + markdown += `##### ${subKey}\n\n`; + markdown += `\`\`\`json\n${JSON.stringify(subValue, null, 2)}\n\`\`\`\n\n`; + } + markdown += "</details>\n"; + } else { + markdown += `${value}\n`; + } + markdown += "\n"; + } + + const html = ` + + + + + + Ruby LSP Information + + + +

Ruby LSP Information

+

Please copy the content below and paste it into the issue you're opening:

+
${markdown}
+ + + + `; + + return html; +} diff --git a/vscode/src/rubyLsp.ts b/vscode/src/rubyLsp.ts index 148fa14a2..f83e6583f 100644 --- a/vscode/src/rubyLsp.ts +++ b/vscode/src/rubyLsp.ts @@ -17,6 +17,7 @@ import { Debugger } from "./debugger"; import { DependenciesTree } from "./dependenciesTree"; import { Rails } from "./rails"; import { ChatAgent } from "./chatAgent"; +import { collectRubyLspInfo } from "./infoCollector"; // The RubyLsp class represents an instance of the entire extension. This should only be instantiated once at the // activation event. One instance of this class controls all of the existing workspaces, telemetry and handles all @@ -570,6 +571,10 @@ export class RubyLsp { await vscode.commands.executeCommand(pick.command, ...pick.args); }), vscode.commands.registerCommand(Command.NewMinitestFile, newMinitestFile), + vscode.commands.registerCommand(Command.CollectRubyLspInfo, async () => { + const workspace = await this.showWorkspacePick(); + await collectRubyLspInfo(workspace); + }), ); }