squawk_server/
lib.rs

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        // definition_provider: Some(OneOf::Left(true)),
54        ..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    // TODO: we need a better setup for "run func when input changed"
372    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}