Skip to content

Commit 876baea

Browse files
authored
vscode: add start & stop server commands (#576)
also update the status bar to show when the server is stopped
1 parent 1696779 commit 876baea

File tree

4 files changed

+206
-68
lines changed

4 files changed

+206
-68
lines changed

crates/squawk/src/main.rs

Lines changed: 50 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -216,62 +216,61 @@ Please open an issue at https://github.com/sbdchd/squawk/issues/new with the log
216216

217217
let mut clap_app = Opt::clap();
218218
let is_stdin = !atty::is(Stream::Stdin);
219-
220-
if let Some(subcommand) = opts.cmd {
221-
match subcommand {
222-
Command::Server => {
223-
squawk_server::run().context("language server failed")?;
224-
}
225-
Command::UploadToGithub(args) => {
226-
github::check_and_comment_on_pr(
227-
args,
228-
&conf,
229-
is_stdin,
230-
opts.stdin_filepath,
231-
&excluded_rules,
232-
&excluded_paths,
233-
pg_version,
234-
assume_in_transaction,
235-
)
236-
.context("Upload to GitHub failed")?;
237-
}
219+
match opts.cmd {
220+
Some(Command::Server) => {
221+
squawk_server::run().context("language server failed")?;
238222
}
239-
} else {
240-
let found_paths = find_paths(&opts.path_patterns, &excluded_paths).unwrap_or_else(|e| {
241-
eprintln!("Failed to find files: {e}");
242-
process::exit(1);
243-
});
244-
if found_paths.is_empty() && !opts.path_patterns.is_empty() {
245-
eprintln!(
246-
"Failed to find files for provided patterns: {:?}",
247-
opts.path_patterns
248-
);
249-
process::exit(1);
223+
Some(Command::UploadToGithub(args)) => {
224+
github::check_and_comment_on_pr(
225+
args,
226+
&conf,
227+
is_stdin,
228+
opts.stdin_filepath,
229+
&excluded_rules,
230+
&excluded_paths,
231+
pg_version,
232+
assume_in_transaction,
233+
)
234+
.context("Upload to GitHub failed")?;
250235
}
251-
if !found_paths.is_empty() || is_stdin {
252-
let stdout = io::stdout();
253-
let mut handle = stdout.lock();
236+
None => {
237+
let found_paths =
238+
find_paths(&opts.path_patterns, &excluded_paths).unwrap_or_else(|e| {
239+
eprintln!("Failed to find files: {e}");
240+
process::exit(1);
241+
});
242+
if found_paths.is_empty() && !opts.path_patterns.is_empty() {
243+
eprintln!(
244+
"Failed to find files for provided patterns: {:?}",
245+
opts.path_patterns
246+
);
247+
process::exit(1);
248+
}
249+
if !found_paths.is_empty() || is_stdin {
250+
let stdout = io::stdout();
251+
let mut handle = stdout.lock();
254252

255-
let read_stdin = found_paths.is_empty() && is_stdin;
256-
if let Some(kind) = opts.debug {
257-
debug(&mut handle, &found_paths, read_stdin, &kind, opts.verbose)?;
253+
let read_stdin = found_paths.is_empty() && is_stdin;
254+
if let Some(kind) = opts.debug {
255+
debug(&mut handle, &found_paths, read_stdin, &kind, opts.verbose)?;
256+
} else {
257+
let reporter = opts.reporter.unwrap_or(Reporter::Tty);
258+
let exit_code = check_and_dump_files(
259+
&mut handle,
260+
&found_paths,
261+
read_stdin,
262+
opts.stdin_filepath,
263+
&excluded_rules,
264+
pg_version,
265+
assume_in_transaction,
266+
&reporter,
267+
)?;
268+
return Ok(exit_code);
269+
}
258270
} else {
259-
let reporter = opts.reporter.unwrap_or(Reporter::Tty);
260-
let exit_code = check_and_dump_files(
261-
&mut handle,
262-
&found_paths,
263-
read_stdin,
264-
opts.stdin_filepath,
265-
&excluded_rules,
266-
pg_version,
267-
assume_in_transaction,
268-
&reporter,
269-
)?;
270-
return Ok(exit_code);
271+
clap_app.print_long_help()?;
272+
println!();
271273
}
272-
} else {
273-
clap_app.print_long_help()?;
274-
println!();
275274
}
276275
}
277276
Ok(ExitCode::SUCCESS)

squawk-vscode/eslint.config.mjs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,18 @@ import tseslint from "typescript-eslint"
55
export default tseslint.config(
66
{ ignores: ["out", "extension", "dist"] },
77
{
8-
extends: [js.configs.recommended, ...tseslint.configs.recommended],
8+
extends: [
9+
js.configs.recommended,
10+
...tseslint.configs.recommendedTypeChecked,
11+
],
912
files: ["**/*.{ts,tsx}"],
1013
languageOptions: {
1114
ecmaVersion: 2020,
1215
globals: globals.node,
16+
parserOptions: {
17+
project: "./tsconfig.json",
18+
tsconfigRootDir: import.meta.dirname,
19+
},
1320
},
1421
plugins: {},
1522
rules: {

squawk-vscode/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@
5151
"command": "squawk.showClientLogs",
5252
"title": "Show Client Logs",
5353
"category": "Squawk"
54+
},
55+
{
56+
"command": "squawk.startServer",
57+
"title": "Start Server",
58+
"category": "Squawk"
59+
},
60+
{
61+
"command": "squawk.stopServer",
62+
"title": "Stop Server",
63+
"category": "Squawk"
5464
}
5565
],
5666
"languages": [

squawk-vscode/src/extension.ts

Lines changed: 138 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import {
55
LanguageClientOptions,
66
Executable,
77
ServerOptions,
8+
State,
9+
StateChangeEvent,
810
} from "vscode-languageclient/node"
911

1012
let client: LanguageClient | undefined
1113
let log: Pick<
1214
vscode.LogOutputChannel,
1315
"trace" | "debug" | "info" | "warn" | "error" | "show"
1416
>
17+
const onClientStateChange = new vscode.EventEmitter<StateChangeEvent>()
1518

1619
export async function activate(context: vscode.ExtensionContext) {
1720
log = vscode.window.createOutputChannel("Squawk Client", {
@@ -41,11 +44,15 @@ export async function activate(context: vscode.ExtensionContext) {
4144
)
4245
return version
4346
} catch (error) {
44-
vscode.window.showErrorMessage(`Failed to get server version: ${error}`)
47+
vscode.window.showErrorMessage(
48+
`Failed to get server version: ${String(error)}`,
49+
)
4550
}
4651
}),
4752
)
4853

54+
setupStatusBarItem(context)
55+
4956
context.subscriptions.push(
5057
vscode.commands.registerCommand("squawk.showLogs", () => {
5158
client?.outputChannel?.show()
@@ -58,15 +65,19 @@ export async function activate(context: vscode.ExtensionContext) {
5865
}),
5966
)
6067

61-
const statusBarItem = vscode.window.createStatusBarItem(
62-
vscode.StatusBarAlignment.Right,
63-
100,
68+
context.subscriptions.push(
69+
vscode.commands.registerCommand("squawk.startServer", async () => {
70+
await startServer(context)
71+
}),
6472
)
65-
statusBarItem.text = "Squawk"
66-
statusBarItem.tooltip = "Click to show Squawk Language Server logs"
67-
statusBarItem.command = "squawk.showLogs"
68-
statusBarItem.show()
69-
context.subscriptions.push(statusBarItem)
73+
74+
context.subscriptions.push(
75+
vscode.commands.registerCommand("squawk.stopServer", async () => {
76+
await stopServer()
77+
}),
78+
)
79+
80+
context.subscriptions.push(onClientStateChange)
7081

7182
await startServer(context)
7283
}
@@ -83,12 +94,88 @@ function isSqlEditor(editor: vscode.TextEditor): boolean {
8394
return isSqlDocument(editor.document)
8495
}
8596

97+
function setupStatusBarItem(context: vscode.ExtensionContext) {
98+
const statusBarItem = vscode.window.createStatusBarItem(
99+
vscode.StatusBarAlignment.Left,
100+
)
101+
statusBarItem.text = "Squawk"
102+
statusBarItem.command = "squawk.showLogs"
103+
context.subscriptions.push(statusBarItem)
104+
105+
const onDidChangeActiveTextEditor = (
106+
editor: vscode.TextEditor | undefined,
107+
) => {
108+
if (editor && isSqlEditor(editor)) {
109+
updateStatusBarItem(statusBarItem)
110+
statusBarItem.show()
111+
} else {
112+
statusBarItem.hide()
113+
}
114+
}
115+
116+
onDidChangeActiveTextEditor(vscode.window.activeTextEditor)
117+
118+
context.subscriptions.push(
119+
vscode.window.onDidChangeActiveTextEditor((editor) => {
120+
onDidChangeActiveTextEditor(editor)
121+
}),
122+
)
123+
124+
context.subscriptions.push(
125+
onClientStateChange.event(() => {
126+
updateStatusBarItem(statusBarItem)
127+
}),
128+
)
129+
}
130+
131+
function updateStatusBarItem(statusBarItem: vscode.StatusBarItem) {
132+
if (!client) {
133+
return
134+
}
135+
let statusText: string
136+
let icon: string
137+
let backgroundColor: vscode.ThemeColor | undefined
138+
switch (client.state) {
139+
case State.Stopped:
140+
statusText = "Stopped"
141+
icon = "$(error) "
142+
backgroundColor = new vscode.ThemeColor("statusBarItem.warningBackground")
143+
break
144+
case State.Starting:
145+
statusText = "Starting..."
146+
icon = "$(loading~spin) "
147+
backgroundColor = undefined
148+
break
149+
case State.Running:
150+
statusText = "Running"
151+
icon = ""
152+
backgroundColor = undefined
153+
break
154+
default:
155+
assertNever(client.state)
156+
}
157+
158+
statusBarItem.text = `${icon}Squawk`
159+
statusBarItem.backgroundColor = backgroundColor
160+
statusBarItem.tooltip = `Status: ${statusText}\nClick to show server logs`
161+
}
162+
86163
function getSquawkPath(context: vscode.ExtensionContext): vscode.Uri {
87164
const ext = process.platform === "win32" ? ".exe" : ""
88165
return vscode.Uri.joinPath(context.extensionUri, "server", `squawk${ext}`)
89166
}
90167

91168
async function startServer(context: vscode.ExtensionContext) {
169+
if (client?.state === State.Running) {
170+
log.info("Server is already running")
171+
return
172+
}
173+
174+
if (client?.state === State.Starting) {
175+
log.info("Server is already starting")
176+
return
177+
}
178+
92179
log.info("Starting Squawk Language Server...")
93180

94181
const squawkPath = getSquawkPath(context)
@@ -120,9 +207,38 @@ async function startServer(context: vscode.ExtensionContext) {
120207
clientOptions,
121208
)
122209

123-
log.info("Language client created, starting...")
124-
client.start()
125-
log.info("Language client started")
210+
context.subscriptions.push(
211+
client.onDidChangeState((event) => {
212+
onClientStateChange.fire(event)
213+
}),
214+
)
215+
216+
log.info("server starting...")
217+
try {
218+
await client.start()
219+
log.info("server started successfully")
220+
} catch (error) {
221+
log.error(`Failed to start server:`, error)
222+
vscode.window.showErrorMessage(`Failed to start server: ${String(error)}`)
223+
}
224+
}
225+
226+
async function stopServer() {
227+
if (!client) {
228+
log.info("No client to stop server")
229+
return
230+
}
231+
232+
if (client.state === State.Stopped) {
233+
log.info("Server is already stopped")
234+
return
235+
}
236+
237+
log.info("Stopping server...")
238+
239+
await client.stop()
240+
241+
log.info("server stopped")
126242
}
127243

128244
// Based on rust-analyzer's SyntaxTree support:
@@ -181,18 +297,24 @@ class SyntaxTreeProvider implements vscode.TextDocumentContentProvider {
181297
if (!document) {
182298
return "Error: no active editor found"
183299
}
300+
if (!client) {
301+
return "Error: no client found"
302+
}
184303
const text = document.getText()
185304
const uri = document.uri.toString()
186305
log.info(`Requesting syntax tree for: ${uri}`)
187-
const response = await client?.sendRequest("squawk/syntaxTree", {
306+
const response = await client.sendRequest<string>("squawk/syntaxTree", {
188307
textDocument: { uri },
189308
text,
190309
})
191310
log.info("Syntax tree received")
192-
return response as string
311+
return response
193312
} catch (error) {
194-
log.error(`Failed to get syntax tree: ${error}`)
195-
return `Error: Failed to get syntax tree: ${error}`
313+
log.error(`Failed to get syntax tree:`, error)
314+
return `Error: Failed to get syntax tree: ${String(error)}`
196315
}
197316
}
198317
}
318+
function assertNever(param: never): never {
319+
throw new Error(`should never get here, but got ${String(param)}`)
320+
}

0 commit comments

Comments
 (0)