diff --git a/crates/squawk/src/main.rs b/crates/squawk/src/main.rs index bb332dc4..37b27e61 100644 --- a/crates/squawk/src/main.rs +++ b/crates/squawk/src/main.rs @@ -216,62 +216,61 @@ Please open an issue at https://github.com/sbdchd/squawk/issues/new with the log let mut clap_app = Opt::clap(); let is_stdin = !atty::is(Stream::Stdin); - - if let Some(subcommand) = opts.cmd { - match subcommand { - Command::Server => { - squawk_server::run().context("language server failed")?; - } - Command::UploadToGithub(args) => { - github::check_and_comment_on_pr( - args, - &conf, - is_stdin, - opts.stdin_filepath, - &excluded_rules, - &excluded_paths, - pg_version, - assume_in_transaction, - ) - .context("Upload to GitHub failed")?; - } + match opts.cmd { + Some(Command::Server) => { + squawk_server::run().context("language server failed")?; } - } else { - let found_paths = find_paths(&opts.path_patterns, &excluded_paths).unwrap_or_else(|e| { - eprintln!("Failed to find files: {e}"); - process::exit(1); - }); - if found_paths.is_empty() && !opts.path_patterns.is_empty() { - eprintln!( - "Failed to find files for provided patterns: {:?}", - opts.path_patterns - ); - process::exit(1); + Some(Command::UploadToGithub(args)) => { + github::check_and_comment_on_pr( + args, + &conf, + is_stdin, + opts.stdin_filepath, + &excluded_rules, + &excluded_paths, + pg_version, + assume_in_transaction, + ) + .context("Upload to GitHub failed")?; } - if !found_paths.is_empty() || is_stdin { - let stdout = io::stdout(); - let mut handle = stdout.lock(); + None => { + let found_paths = + find_paths(&opts.path_patterns, &excluded_paths).unwrap_or_else(|e| { + eprintln!("Failed to find files: {e}"); + process::exit(1); + }); + if found_paths.is_empty() && !opts.path_patterns.is_empty() { + eprintln!( + "Failed to find files for provided patterns: {:?}", + opts.path_patterns + ); + process::exit(1); + } + if !found_paths.is_empty() || is_stdin { + let stdout = io::stdout(); + let mut handle = stdout.lock(); - let read_stdin = found_paths.is_empty() && is_stdin; - if let Some(kind) = opts.debug { - debug(&mut handle, &found_paths, read_stdin, &kind, opts.verbose)?; + let read_stdin = found_paths.is_empty() && is_stdin; + if let Some(kind) = opts.debug { + debug(&mut handle, &found_paths, read_stdin, &kind, opts.verbose)?; + } else { + let reporter = opts.reporter.unwrap_or(Reporter::Tty); + let exit_code = check_and_dump_files( + &mut handle, + &found_paths, + read_stdin, + opts.stdin_filepath, + &excluded_rules, + pg_version, + assume_in_transaction, + &reporter, + )?; + return Ok(exit_code); + } } else { - let reporter = opts.reporter.unwrap_or(Reporter::Tty); - let exit_code = check_and_dump_files( - &mut handle, - &found_paths, - read_stdin, - opts.stdin_filepath, - &excluded_rules, - pg_version, - assume_in_transaction, - &reporter, - )?; - return Ok(exit_code); + clap_app.print_long_help()?; + println!(); } - } else { - clap_app.print_long_help()?; - println!(); } } Ok(ExitCode::SUCCESS) diff --git a/squawk-vscode/eslint.config.mjs b/squawk-vscode/eslint.config.mjs index 019bee84..6cac8cfe 100644 --- a/squawk-vscode/eslint.config.mjs +++ b/squawk-vscode/eslint.config.mjs @@ -5,11 +5,18 @@ import tseslint from "typescript-eslint" export default tseslint.config( { ignores: ["out", "extension", "dist"] }, { - extends: [js.configs.recommended, ...tseslint.configs.recommended], + extends: [ + js.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + ], files: ["**/*.{ts,tsx}"], languageOptions: { ecmaVersion: 2020, globals: globals.node, + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: import.meta.dirname, + }, }, plugins: {}, rules: { diff --git a/squawk-vscode/package.json b/squawk-vscode/package.json index 6009703e..858cac86 100644 --- a/squawk-vscode/package.json +++ b/squawk-vscode/package.json @@ -51,6 +51,16 @@ "command": "squawk.showClientLogs", "title": "Show Client Logs", "category": "Squawk" + }, + { + "command": "squawk.startServer", + "title": "Start Server", + "category": "Squawk" + }, + { + "command": "squawk.stopServer", + "title": "Stop Server", + "category": "Squawk" } ], "languages": [ diff --git a/squawk-vscode/src/extension.ts b/squawk-vscode/src/extension.ts index 2f06a441..592414b0 100644 --- a/squawk-vscode/src/extension.ts +++ b/squawk-vscode/src/extension.ts @@ -5,6 +5,8 @@ import { LanguageClientOptions, Executable, ServerOptions, + State, + StateChangeEvent, } from "vscode-languageclient/node" let client: LanguageClient | undefined @@ -12,6 +14,7 @@ let log: Pick< vscode.LogOutputChannel, "trace" | "debug" | "info" | "warn" | "error" | "show" > +const onClientStateChange = new vscode.EventEmitter() export async function activate(context: vscode.ExtensionContext) { log = vscode.window.createOutputChannel("Squawk Client", { @@ -41,11 +44,15 @@ export async function activate(context: vscode.ExtensionContext) { ) return version } catch (error) { - vscode.window.showErrorMessage(`Failed to get server version: ${error}`) + vscode.window.showErrorMessage( + `Failed to get server version: ${String(error)}`, + ) } }), ) + setupStatusBarItem(context) + context.subscriptions.push( vscode.commands.registerCommand("squawk.showLogs", () => { client?.outputChannel?.show() @@ -58,15 +65,19 @@ export async function activate(context: vscode.ExtensionContext) { }), ) - const statusBarItem = vscode.window.createStatusBarItem( - vscode.StatusBarAlignment.Right, - 100, + context.subscriptions.push( + vscode.commands.registerCommand("squawk.startServer", async () => { + await startServer(context) + }), ) - statusBarItem.text = "Squawk" - statusBarItem.tooltip = "Click to show Squawk Language Server logs" - statusBarItem.command = "squawk.showLogs" - statusBarItem.show() - context.subscriptions.push(statusBarItem) + + context.subscriptions.push( + vscode.commands.registerCommand("squawk.stopServer", async () => { + await stopServer() + }), + ) + + context.subscriptions.push(onClientStateChange) await startServer(context) } @@ -83,12 +94,88 @@ function isSqlEditor(editor: vscode.TextEditor): boolean { return isSqlDocument(editor.document) } +function setupStatusBarItem(context: vscode.ExtensionContext) { + const statusBarItem = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Left, + ) + statusBarItem.text = "Squawk" + statusBarItem.command = "squawk.showLogs" + context.subscriptions.push(statusBarItem) + + const onDidChangeActiveTextEditor = ( + editor: vscode.TextEditor | undefined, + ) => { + if (editor && isSqlEditor(editor)) { + updateStatusBarItem(statusBarItem) + statusBarItem.show() + } else { + statusBarItem.hide() + } + } + + onDidChangeActiveTextEditor(vscode.window.activeTextEditor) + + context.subscriptions.push( + vscode.window.onDidChangeActiveTextEditor((editor) => { + onDidChangeActiveTextEditor(editor) + }), + ) + + context.subscriptions.push( + onClientStateChange.event(() => { + updateStatusBarItem(statusBarItem) + }), + ) +} + +function updateStatusBarItem(statusBarItem: vscode.StatusBarItem) { + if (!client) { + return + } + let statusText: string + let icon: string + let backgroundColor: vscode.ThemeColor | undefined + switch (client.state) { + case State.Stopped: + statusText = "Stopped" + icon = "$(error) " + backgroundColor = new vscode.ThemeColor("statusBarItem.warningBackground") + break + case State.Starting: + statusText = "Starting..." + icon = "$(loading~spin) " + backgroundColor = undefined + break + case State.Running: + statusText = "Running" + icon = "" + backgroundColor = undefined + break + default: + assertNever(client.state) + } + + statusBarItem.text = `${icon}Squawk` + statusBarItem.backgroundColor = backgroundColor + statusBarItem.tooltip = `Status: ${statusText}\nClick to show server logs` +} + 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) { + if (client?.state === State.Running) { + log.info("Server is already running") + return + } + + if (client?.state === State.Starting) { + log.info("Server is already starting") + return + } + log.info("Starting Squawk Language Server...") const squawkPath = getSquawkPath(context) @@ -120,9 +207,38 @@ async function startServer(context: vscode.ExtensionContext) { clientOptions, ) - log.info("Language client created, starting...") - client.start() - log.info("Language client started") + context.subscriptions.push( + client.onDidChangeState((event) => { + onClientStateChange.fire(event) + }), + ) + + log.info("server starting...") + try { + await client.start() + log.info("server started successfully") + } catch (error) { + log.error(`Failed to start server:`, error) + vscode.window.showErrorMessage(`Failed to start server: ${String(error)}`) + } +} + +async function stopServer() { + if (!client) { + log.info("No client to stop server") + return + } + + if (client.state === State.Stopped) { + log.info("Server is already stopped") + return + } + + log.info("Stopping server...") + + await client.stop() + + log.info("server stopped") } // Based on rust-analyzer's SyntaxTree support: @@ -181,18 +297,24 @@ class SyntaxTreeProvider implements vscode.TextDocumentContentProvider { if (!document) { return "Error: no active editor found" } + if (!client) { + return "Error: no client found" + } const text = document.getText() const uri = document.uri.toString() log.info(`Requesting syntax tree for: ${uri}`) - const response = await client?.sendRequest("squawk/syntaxTree", { + const response = await client.sendRequest("squawk/syntaxTree", { textDocument: { uri }, text, }) log.info("Syntax tree received") - return response as string + return response } catch (error) { - log.error(`Failed to get syntax tree: ${error}`) - return `Error: Failed to get syntax tree: ${error}` + log.error(`Failed to get syntax tree:`, error) + return `Error: Failed to get syntax tree: ${String(error)}` } } } +function assertNever(param: never): never { + throw new Error(`should never get here, but got ${String(param)}`) +}