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