squawk_server/
lib.rs

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