From b037205d9aa86ce012f44fc9275350cfebd7b7fc Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Tue, 1 Jul 2025 22:00:09 -0400 Subject: [PATCH] vscode: add show client logs & show syntax tree cmds --- Cargo.lock | 1 + crates/squawk_server/Cargo.toml | 1 + crates/squawk_server/src/lib.rs | 34 +++++++++ squawk-vscode/eslint.config.mjs | 16 +++- squawk-vscode/package.json | 10 +++ squawk-vscode/src/extension.ts | 128 ++++++++++++++++++++++++++++---- 6 files changed, 175 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1135e083..3a19f78e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2677,6 +2677,7 @@ dependencies = [ "log", "lsp-server", "lsp-types", + "serde", "serde_json", "simplelog", "squawk_linter", diff --git a/crates/squawk_server/Cargo.toml b/crates/squawk_server/Cargo.toml index 7d97b805..2702d3f1 100644 --- a/crates/squawk_server/Cargo.toml +++ b/crates/squawk_server/Cargo.toml @@ -13,6 +13,7 @@ log.workspace = true simplelog.workspace = true lsp-server.workspace = true lsp-types.workspace = true +serde.workspace = true serde_json.workspace = true squawk_linter.workspace = true squawk_syntax.workspace = true diff --git a/crates/squawk_server/src/lib.rs b/crates/squawk_server/src/lib.rs index bdb60af8..6db160d6 100644 --- a/crates/squawk_server/src/lib.rs +++ b/crates/squawk_server/src/lib.rs @@ -63,6 +63,11 @@ fn main_loop(connection: Connection, params: serde_json::Value) -> Result<()> { continue; } + if req.method == "squawk/syntaxTree" { + handle_syntax_tree(&connection, req)?; + continue; + } + info!("Ignoring unhandled request: {}", req.method); } Message::Response(resp) => { @@ -235,3 +240,32 @@ fn lint(connection: &Connection, uri: lsp_types::Url, content: &str, version: i3 Ok(()) } + +#[derive(serde::Deserialize)] +struct SyntaxTreeParams { + #[serde(rename = "textDocument")] + text_document: lsp_types::TextDocumentIdentifier, + // TODO: once we start storing the text doc on the server we won't need to + // send the content across the wire + text: String, +} + +fn handle_syntax_tree(connection: &Connection, req: lsp_server::Request) -> Result<()> { + let params: SyntaxTreeParams = serde_json::from_value(req.params)?; + let uri = params.text_document.uri; + let content = params.text; + + info!("Generating syntax tree for: {}", uri); + + let parse: Parse = SourceFile::parse(&content); + let syntax_tree = format!("{:#?}", parse.syntax_node()); + + let resp = Response { + id: req.id, + result: Some(serde_json::to_value(&syntax_tree).unwrap()), + error: None, + }; + + connection.sender.send(Message::Response(resp))?; + Ok(()) +} diff --git a/squawk-vscode/eslint.config.mjs b/squawk-vscode/eslint.config.mjs index 82987e09..019bee84 100644 --- a/squawk-vscode/eslint.config.mjs +++ b/squawk-vscode/eslint.config.mjs @@ -12,6 +12,20 @@ export default tseslint.config( globals: globals.node, }, plugins: {}, - rules: {}, + rules: { + "no-console": "error", + "@typescript-eslint/no-unused-vars": [ + "error", + { + args: "all", + argsIgnorePattern: "^_", + caughtErrors: "all", + caughtErrorsIgnorePattern: "^_", + destructuredArrayIgnorePattern: "^_", + varsIgnorePattern: "^_", + ignoreRestSiblings: true, + }, + ], + }, }, ) diff --git a/squawk-vscode/package.json b/squawk-vscode/package.json index fd3d1961..6009703e 100644 --- a/squawk-vscode/package.json +++ b/squawk-vscode/package.json @@ -41,6 +41,16 @@ "command": "squawk.showLogs", "title": "Show Server Logs", "category": "Squawk" + }, + { + "command": "squawk.showSyntaxTree", + "title": "Show Syntax Tree", + "category": "Squawk" + }, + { + "command": "squawk.showClientLogs", + "title": "Show Client Logs", + "category": "Squawk" } ], "languages": [ diff --git a/squawk-vscode/src/extension.ts b/squawk-vscode/src/extension.ts index 46d2fce8..2f06a441 100644 --- a/squawk-vscode/src/extension.ts +++ b/squawk-vscode/src/extension.ts @@ -8,13 +8,28 @@ import { } from "vscode-languageclient/node" let client: LanguageClient | undefined +let log: Pick< + vscode.LogOutputChannel, + "trace" | "debug" | "info" | "warn" | "error" | "show" +> export async function activate(context: vscode.ExtensionContext) { - console.log("Squawk activate") + log = vscode.window.createOutputChannel("Squawk Client", { + log: true, + }) - const serverVersionCommand = vscode.commands.registerCommand( - "squawk.serverVersion", - () => { + log.info("Squawk activate") + + const syntaxTreeProvider = new SyntaxTreeProvider(context) + context.subscriptions.push( + vscode.workspace.registerTextDocumentContentProvider( + "squawk-syntax-tree", + syntaxTreeProvider, + ), + ) + + context.subscriptions.push( + vscode.commands.registerCommand("squawk.serverVersion", () => { try { const serverPath = getSquawkPath(context) const stdout = execFileSync(serverPath.path, ["--version"], { @@ -28,17 +43,20 @@ export async function activate(context: vscode.ExtensionContext) { } catch (error) { vscode.window.showErrorMessage(`Failed to get server version: ${error}`) } - }, + }), ) - context.subscriptions.push(serverVersionCommand) - const showLogsCommand = vscode.commands.registerCommand( - "squawk.showLogs", - () => { + context.subscriptions.push( + vscode.commands.registerCommand("squawk.showLogs", () => { client?.outputChannel?.show() - }, + }), + ) + + context.subscriptions.push( + vscode.commands.registerCommand("squawk.showClientLogs", () => { + log.show() + }), ) - context.subscriptions.push(showLogsCommand) const statusBarItem = vscode.window.createStatusBarItem( vscode.StatusBarAlignment.Right, @@ -57,13 +75,21 @@ export async function deactivate() { await client?.stop() } +function isSqlDocument(document: vscode.TextDocument): boolean { + return document.languageId === "sql" || document.languageId === "postgres" +} + +function isSqlEditor(editor: vscode.TextEditor): boolean { + return isSqlDocument(editor.document) +} + function getSquawkPath(context: vscode.ExtensionContext): vscode.Uri { const ext = process.platform === "win32" ? ".exe" : "" return vscode.Uri.joinPath(context.extensionUri, "server", `squawk${ext}`) } async function startServer(context: vscode.ExtensionContext) { - console.log("starting squawk server") + log.info("Starting Squawk Language Server...") const squawkPath = getSquawkPath(context) const hasBinary = await vscode.workspace.fs.stat(squawkPath).then( @@ -72,11 +98,11 @@ async function startServer(context: vscode.ExtensionContext) { ) if (!hasBinary) { const errorMsg = `Squawk binary not found at: ${squawkPath.path}` - console.error(errorMsg) + log.error(`ERROR: ${errorMsg}`) vscode.window.showErrorMessage(errorMsg) return } - console.log(`Found Squawk binary at: ${squawkPath}`) + log.info(`Found Squawk binary at: ${squawkPath.path}`) const serverExecutable: Executable = { command: squawkPath.path, @@ -94,5 +120,79 @@ async function startServer(context: vscode.ExtensionContext) { clientOptions, ) + log.info("Language client created, starting...") client.start() + log.info("Language client started") +} + +// Based on rust-analyzer's SyntaxTree support: +// https://github.com/rust-lang/rust-analyzer/blob/c0eaff7dd1fdfffed4e5706780e79967760d1d9b/editors/code/src/commands.ts#L432-L510 +class SyntaxTreeProvider implements vscode.TextDocumentContentProvider { + _eventEmitter = new vscode.EventEmitter() + _activeEditor: vscode.TextEditor | undefined + // TODO: for now we only show syntax highlighting when someone has + // rust-analyzer installed which ships with the rast grammar + _uri = vscode.Uri.parse("squawk-syntax-tree://syntaxtree/tree.rast") + + constructor(context: vscode.ExtensionContext) { + context.subscriptions.push( + vscode.window.onDidChangeActiveTextEditor((editor) => { + this._onDidChangeActiveTextEditor(editor) + }), + ) + context.subscriptions.push( + vscode.workspace.onDidChangeTextDocument((event) => { + this._onDidChangeTextDocument(event.document) + }), + ) + context.subscriptions.push( + vscode.commands.registerCommand("squawk.showSyntaxTree", async () => { + const doc = await vscode.workspace.openTextDocument(this._uri) + await vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside) + }), + ) + + // initial kick off to make sure we have the editor set + this._onDidChangeActiveTextEditor(vscode.window.activeTextEditor) + } + + onDidChange = this._eventEmitter.event + + _onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) { + if (editor && isSqlEditor(editor)) { + this._activeEditor = editor + this._eventEmitter.fire(this._uri) + } + } + + _onDidChangeTextDocument(document: vscode.TextDocument) { + if ( + isSqlDocument(document) && + this._activeEditor && + document === this._activeEditor.document + ) { + this._eventEmitter.fire(this._uri) + } + } + + async provideTextDocumentContent(_uri: vscode.Uri): Promise { + try { + const document = this._activeEditor?.document + if (!document) { + return "Error: no active editor found" + } + const text = document.getText() + const uri = document.uri.toString() + log.info(`Requesting syntax tree for: ${uri}`) + const response = await client?.sendRequest("squawk/syntaxTree", { + textDocument: { uri }, + text, + }) + log.info("Syntax tree received") + return response as string + } catch (error) { + log.error(`Failed to get syntax tree: ${error}`) + return `Error: Failed to get syntax tree: ${error}` + } + } }