1use anyhow::{Context, Result};
2use log::info;
3use lsp_server::{Connection, Message, Notification, Response};
4use lsp_types::{
5 CodeAction, CodeActionKind, CodeActionOptions, CodeActionOrCommand, CodeActionParams,
6 CodeActionProviderCapability, CodeActionResponse, Command, Diagnostic,
7 DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
8 GotoDefinitionParams, GotoDefinitionResponse, InitializeParams, Location, Position,
9 PublishDiagnosticsParams, Range, ServerCapabilities, TextDocumentSyncCapability,
10 TextDocumentSyncKind, Url, WorkDoneProgressOptions, WorkspaceEdit,
11 notification::{
12 DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, Notification as _,
13 PublishDiagnostics,
14 },
15 request::{CodeActionRequest, GotoDefinition, Request},
16};
17use squawk_syntax::{Parse, SourceFile};
18use std::collections::HashMap;
19
20use diagnostic::DIAGNOSTIC_NAME;
21
22use crate::diagnostic::AssociatedDiagnosticData;
23mod diagnostic;
24mod ignore;
25mod lint;
26mod lsp_utils;
27
28struct DocumentState {
29 content: String,
30 version: i32,
31}
32
33pub fn run() -> Result<()> {
34 info!("Starting Squawk LSP server");
35
36 let (connection, io_threads) = Connection::stdio();
37
38 let server_capabilities = serde_json::to_value(&ServerCapabilities {
39 text_document_sync: Some(TextDocumentSyncCapability::Kind(
40 TextDocumentSyncKind::INCREMENTAL,
41 )),
42 code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions {
43 code_action_kinds: Some(vec![CodeActionKind::QUICKFIX]),
44 work_done_progress_options: WorkDoneProgressOptions {
45 work_done_progress: None,
46 },
47 resolve_provider: None,
48 })),
49 ..Default::default()
51 })
52 .unwrap();
53
54 info!("LSP server initializing connection...");
55 let initialization_params = connection.initialize(server_capabilities)?;
56 info!("LSP server initialized, entering main loop");
57
58 main_loop(connection, initialization_params)?;
59
60 info!("LSP server shutting down");
61
62 io_threads.join()?;
63 Ok(())
64}
65
66fn main_loop(connection: Connection, params: serde_json::Value) -> Result<()> {
67 info!("Server main loop");
68
69 let init_params: InitializeParams = serde_json::from_value(params).unwrap_or_default();
70 info!("Client process ID: {:?}", init_params.process_id);
71 let client_name = init_params.client_info.map(|x| x.name);
72 info!("Client name: {client_name:?}");
73
74 let mut documents: HashMap<Url, DocumentState> = HashMap::new();
75
76 for msg in &connection.receiver {
77 match msg {
78 Message::Request(req) => {
79 info!("Received request: method={}, id={:?}", req.method, req.id);
80
81 if connection.handle_shutdown(&req)? {
82 info!("Received shutdown request, exiting");
83 return Ok(());
84 }
85
86 match req.method.as_ref() {
87 GotoDefinition::METHOD => {
88 handle_goto_definition(&connection, req)?;
89 }
90 CodeActionRequest::METHOD => {
91 handle_code_action(&connection, req, &documents)?;
92 }
93 "squawk/syntaxTree" => {
94 handle_syntax_tree(&connection, req, &documents)?;
95 }
96 "squawk/tokens" => {
97 handle_tokens(&connection, req, &documents)?;
98 }
99 _ => {
100 info!("Ignoring unhandled request: {}", req.method);
101 }
102 }
103 }
104 Message::Response(resp) => {
105 info!("Received response: id={:?}", resp.id);
106 }
107 Message::Notification(notif) => {
108 info!("Received notification: method={}", notif.method);
109 match notif.method.as_ref() {
110 DidOpenTextDocument::METHOD => {
111 handle_did_open(&connection, notif, &mut documents)?;
112 }
113 DidChangeTextDocument::METHOD => {
114 handle_did_change(&connection, notif, &mut documents)?;
115 }
116 DidCloseTextDocument::METHOD => {
117 handle_did_close(&connection, notif, &mut documents)?;
118 }
119 _ => {
120 info!("Ignoring unhandled notification: {}", notif.method);
121 }
122 }
123 }
124 }
125 }
126 Ok(())
127}
128
129fn handle_goto_definition(connection: &Connection, req: lsp_server::Request) -> Result<()> {
130 let params: GotoDefinitionParams = serde_json::from_value(req.params)?;
131
132 let location = Location {
133 uri: params.text_document_position_params.text_document.uri,
134 range: Range::new(Position::new(1, 2), Position::new(1, 3)),
135 };
136
137 let result = GotoDefinitionResponse::Scalar(location);
138 let resp = Response {
139 id: req.id,
140 result: Some(serde_json::to_value(&result).unwrap()),
141 error: None,
142 };
143
144 connection.sender.send(Message::Response(resp))?;
145 Ok(())
146}
147
148fn handle_code_action(
149 connection: &Connection,
150 req: lsp_server::Request,
151 _documents: &HashMap<Url, DocumentState>,
152) -> Result<()> {
153 let params: CodeActionParams = serde_json::from_value(req.params)?;
154 let uri = params.text_document.uri;
155
156 let mut actions = Vec::new();
157
158 for mut diagnostic in params
159 .context
160 .diagnostics
161 .into_iter()
162 .filter(|diagnostic| diagnostic.source.as_deref() == Some(DIAGNOSTIC_NAME))
163 {
164 let Some(rule_name) = diagnostic.code.as_ref().map(|x| match x {
165 lsp_types::NumberOrString::String(s) => s.clone(),
166 lsp_types::NumberOrString::Number(n) => n.to_string(),
167 }) else {
168 continue;
169 };
170 let Some(data) = diagnostic.data.take() else {
171 continue;
172 };
173
174 let associated_data: AssociatedDiagnosticData =
175 serde_json::from_value(data).context("deserializing diagnostic data")?;
176
177 if let Some(ignore_line_edit) = associated_data.ignore_line_edit {
178 let disable_line_action = CodeAction {
179 title: format!("Disable {rule_name} for this line"),
180 kind: Some(CodeActionKind::QUICKFIX),
181 diagnostics: Some(vec![diagnostic.clone()]),
182 edit: Some(WorkspaceEdit {
183 changes: Some({
184 let mut changes = HashMap::new();
185 changes.insert(uri.clone(), vec![ignore_line_edit]);
186 changes
187 }),
188 ..Default::default()
189 }),
190 command: None,
191 is_preferred: Some(false),
192 disabled: None,
193 data: None,
194 };
195 actions.push(CodeActionOrCommand::CodeAction(disable_line_action));
196 }
197 if let Some(ignore_file_edit) = associated_data.ignore_file_edit {
198 let disable_file_action = CodeAction {
199 title: format!("Disable {rule_name} for the entire file"),
200 kind: Some(CodeActionKind::QUICKFIX),
201 diagnostics: Some(vec![diagnostic.clone()]),
202 edit: Some(WorkspaceEdit {
203 changes: Some({
204 let mut changes = HashMap::new();
205 changes.insert(uri.clone(), vec![ignore_file_edit]);
206 changes
207 }),
208 ..Default::default()
209 }),
210 command: None,
211 is_preferred: Some(false),
212 disabled: None,
213 data: None,
214 };
215 actions.push(CodeActionOrCommand::CodeAction(disable_file_action));
216 }
217
218 let title = format!("Show documentation for {rule_name}");
219 let documentation_action = CodeAction {
220 title: title.clone(),
221 kind: Some(CodeActionKind::QUICKFIX),
222 diagnostics: Some(vec![diagnostic.clone()]),
223 edit: None,
224 command: Some(Command {
225 title,
226 command: "vscode.open".to_string(),
227 arguments: Some(vec![serde_json::to_value(format!(
228 "https://squawkhq.com/docs/{rule_name}"
229 ))?]),
230 }),
231 is_preferred: Some(false),
232 disabled: None,
233 data: None,
234 };
235 actions.push(CodeActionOrCommand::CodeAction(documentation_action));
236
237 if !associated_data.title.is_empty() && !associated_data.edits.is_empty() {
238 let fix_action = CodeAction {
239 title: associated_data.title,
240 kind: Some(CodeActionKind::QUICKFIX),
241 diagnostics: Some(vec![diagnostic.clone()]),
242 edit: Some(WorkspaceEdit {
243 changes: Some({
244 let mut changes = HashMap::new();
245 changes.insert(uri.clone(), associated_data.edits);
246 changes
247 }),
248 ..Default::default()
249 }),
250 command: None,
251 is_preferred: Some(true),
252 disabled: None,
253 data: None,
254 };
255 actions.push(CodeActionOrCommand::CodeAction(fix_action));
256 }
257 }
258
259 let result: CodeActionResponse = actions;
260 let resp = Response {
261 id: req.id,
262 result: Some(serde_json::to_value(&result).unwrap()),
263 error: None,
264 };
265
266 connection.sender.send(Message::Response(resp))?;
267 Ok(())
268}
269
270fn publish_diagnostics(
271 connection: &Connection,
272 uri: Url,
273 version: i32,
274 diagnostics: Vec<Diagnostic>,
275) -> Result<()> {
276 let publish_params = PublishDiagnosticsParams {
277 uri,
278 diagnostics,
279 version: Some(version),
280 };
281
282 let notification = Notification {
283 method: PublishDiagnostics::METHOD.to_owned(),
284 params: serde_json::to_value(publish_params)?,
285 };
286
287 connection
288 .sender
289 .send(Message::Notification(notification))?;
290 Ok(())
291}
292
293fn handle_did_open(
294 connection: &Connection,
295 notif: lsp_server::Notification,
296 documents: &mut HashMap<Url, DocumentState>,
297) -> Result<()> {
298 let params: DidOpenTextDocumentParams = serde_json::from_value(notif.params)?;
299 let uri = params.text_document.uri;
300 let content = params.text_document.text;
301 let version = params.text_document.version;
302
303 documents.insert(uri.clone(), DocumentState { content, version });
304
305 let content = documents.get(&uri).map_or("", |doc| &doc.content);
306
307 let diagnostics = lint::lint(content);
309 publish_diagnostics(connection, uri, version, diagnostics)?;
310
311 Ok(())
312}
313
314fn handle_did_change(
315 connection: &Connection,
316 notif: lsp_server::Notification,
317 documents: &mut HashMap<Url, DocumentState>,
318) -> Result<()> {
319 let params: DidChangeTextDocumentParams = serde_json::from_value(notif.params)?;
320 let uri = params.text_document.uri;
321 let version = params.text_document.version;
322
323 let Some(doc_state) = documents.get_mut(&uri) else {
324 return Ok(());
325 };
326
327 doc_state.content =
328 lsp_utils::apply_incremental_changes(&doc_state.content, params.content_changes);
329 doc_state.version = version;
330
331 let diagnostics = lint::lint(&doc_state.content);
332 publish_diagnostics(connection, uri, version, diagnostics)?;
333
334 Ok(())
335}
336
337fn handle_did_close(
338 connection: &Connection,
339 notif: lsp_server::Notification,
340 documents: &mut HashMap<Url, DocumentState>,
341) -> Result<()> {
342 let params: DidCloseTextDocumentParams = serde_json::from_value(notif.params)?;
343 let uri = params.text_document.uri;
344
345 documents.remove(&uri);
346
347 let publish_params = PublishDiagnosticsParams {
348 uri,
349 diagnostics: vec![],
350 version: None,
351 };
352
353 let notification = Notification {
354 method: PublishDiagnostics::METHOD.to_owned(),
355 params: serde_json::to_value(publish_params)?,
356 };
357
358 connection
359 .sender
360 .send(Message::Notification(notification))?;
361
362 Ok(())
363}
364
365#[derive(serde::Deserialize)]
366struct SyntaxTreeParams {
367 #[serde(rename = "textDocument")]
368 text_document: lsp_types::TextDocumentIdentifier,
369}
370
371fn handle_syntax_tree(
372 connection: &Connection,
373 req: lsp_server::Request,
374 documents: &HashMap<Url, DocumentState>,
375) -> Result<()> {
376 let params: SyntaxTreeParams = serde_json::from_value(req.params)?;
377 let uri = params.text_document.uri;
378
379 info!("Generating syntax tree for: {uri}");
380
381 let content = documents.get(&uri).map_or("", |doc| &doc.content);
382
383 let parse: Parse<SourceFile> = SourceFile::parse(content);
384 let syntax_tree = format!("{:#?}", parse.syntax_node());
385
386 let resp = Response {
387 id: req.id,
388 result: Some(serde_json::to_value(&syntax_tree).unwrap()),
389 error: None,
390 };
391
392 connection.sender.send(Message::Response(resp))?;
393 Ok(())
394}
395
396#[derive(serde::Deserialize)]
397struct TokensParams {
398 #[serde(rename = "textDocument")]
399 text_document: lsp_types::TextDocumentIdentifier,
400}
401
402fn handle_tokens(
403 connection: &Connection,
404 req: lsp_server::Request,
405 documents: &HashMap<Url, DocumentState>,
406) -> Result<()> {
407 let params: TokensParams = serde_json::from_value(req.params)?;
408 let uri = params.text_document.uri;
409
410 info!("Generating tokens for: {uri}");
411
412 let content = documents.get(&uri).map_or("", |doc| &doc.content);
413
414 let tokens = squawk_lexer::tokenize(content);
415
416 let mut output = Vec::new();
417 let mut char_pos = 0;
418 for token in tokens {
419 let token_start = char_pos;
420 let token_end = token_start + token.len as usize;
421 let token_text = &content[token_start..token_end];
422 output.push(format!(
423 "{:?}@{}..{} {:?}",
424 token.kind, token_start, token_end, token_text
425 ));
426 char_pos = token_end;
427 }
428
429 let tokens_output = output.join("\n");
430
431 let resp = Response {
432 id: req.id,
433 result: Some(serde_json::to_value(&tokens_output).unwrap()),
434 error: None,
435 };
436
437 connection.sender.send(Message::Response(resp))?;
438 Ok(())
439}