Skip to main content

squawk_server/
lib.rs

1use anyhow::{Context, Result};
2use etcetera::BaseStrategy;
3use line_index::LineIndex;
4use log::info;
5use lsp_server::{Connection, Message, Notification, Response};
6use lsp_types::{
7    CodeAction, CodeActionKind, CodeActionOptions, CodeActionOrCommand, CodeActionParams,
8    CodeActionProviderCapability, CodeActionResponse, Command, CompletionOptions, CompletionParams,
9    CompletionResponse, Diagnostic, DidChangeTextDocumentParams, DidCloseTextDocumentParams,
10    DidOpenTextDocumentParams, DocumentSymbol, DocumentSymbolParams, GotoDefinitionParams,
11    GotoDefinitionResponse, Hover, HoverContents, HoverParams, HoverProviderCapability,
12    InitializeParams, InlayHint, InlayHintKind, InlayHintLabel, InlayHintLabelPart,
13    InlayHintParams, LanguageString, Location, MarkedString, OneOf, PublishDiagnosticsParams,
14    ReferenceParams, SelectionRangeParams, SelectionRangeProviderCapability, ServerCapabilities,
15    SymbolKind, TextDocumentSyncCapability, TextDocumentSyncKind, Url, WorkDoneProgressOptions,
16    WorkspaceEdit,
17    notification::{
18        DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, Notification as _,
19        PublishDiagnostics,
20    },
21    request::{
22        CodeActionRequest, Completion, DocumentSymbolRequest, GotoDefinition, HoverRequest,
23        InlayHintRequest, References, Request, SelectionRangeRequest,
24    },
25};
26use rowan::TextRange;
27use squawk_ide::completion::completion;
28use squawk_ide::document_symbols::{DocumentSymbolKind, document_symbols};
29use squawk_ide::find_references::find_references;
30use squawk_ide::goto_definition::goto_definition;
31use squawk_ide::hover::hover;
32use squawk_ide::inlay_hints::inlay_hints;
33use squawk_ide::{builtins::BUILTINS_SQL, code_actions::code_actions};
34use squawk_syntax::SourceFile;
35use std::{collections::HashMap, fs, sync::OnceLock};
36
37use diagnostic::DIAGNOSTIC_NAME;
38
39use crate::diagnostic::AssociatedDiagnosticData;
40mod diagnostic;
41mod ignore;
42mod lint;
43mod lsp_utils;
44
45fn builtins_url() -> Option<Url> {
46    // TODO: once we get salsa setup, we can migrate this over
47    static BUILTINS_URL: OnceLock<Option<Url>> = OnceLock::new();
48    BUILTINS_URL
49        .get_or_init(|| {
50            let strategy = etcetera::base_strategy::choose_base_strategy().ok()?;
51            let config_dir = strategy.config_dir();
52            let cache_dir = config_dir.join("squawk/stubs");
53            let path = cache_dir.join("builtins.sql");
54            fs::create_dir_all(cache_dir).ok()?;
55            fs::write(&path, BUILTINS_SQL).ok()?;
56            Url::from_file_path(&path).ok()
57        })
58        .clone()
59}
60
61struct DocumentState {
62    content: String,
63    version: i32,
64}
65
66pub fn run() -> Result<()> {
67    info!("Starting Squawk LSP server");
68
69    let (connection, io_threads) = Connection::stdio();
70
71    let server_capabilities = serde_json::to_value(&ServerCapabilities {
72        text_document_sync: Some(TextDocumentSyncCapability::Kind(
73            TextDocumentSyncKind::INCREMENTAL,
74        )),
75        code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions {
76            code_action_kinds: Some(vec![
77                CodeActionKind::QUICKFIX,
78                CodeActionKind::REFACTOR_REWRITE,
79            ]),
80            work_done_progress_options: WorkDoneProgressOptions {
81                work_done_progress: None,
82            },
83            resolve_provider: None,
84        })),
85        selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)),
86        references_provider: Some(OneOf::Left(true)),
87        definition_provider: Some(OneOf::Left(true)),
88        hover_provider: Some(HoverProviderCapability::Simple(true)),
89        inlay_hint_provider: Some(OneOf::Left(true)),
90        document_symbol_provider: Some(OneOf::Left(true)),
91        completion_provider: Some(CompletionOptions {
92            resolve_provider: Some(false),
93            trigger_characters: Some(vec![".".to_owned()]),
94            all_commit_characters: None,
95            work_done_progress_options: WorkDoneProgressOptions {
96                work_done_progress: None,
97            },
98            completion_item: None,
99        }),
100        ..Default::default()
101    })
102    .unwrap();
103
104    info!("LSP server initializing connection...");
105    let initialization_params = connection.initialize(server_capabilities)?;
106    info!("LSP server initialized, entering main loop");
107
108    main_loop(connection, initialization_params)?;
109
110    info!("LSP server shutting down");
111
112    io_threads.join()?;
113    Ok(())
114}
115
116fn main_loop(connection: Connection, params: serde_json::Value) -> Result<()> {
117    info!("Server main loop");
118
119    let init_params: InitializeParams = serde_json::from_value(params).unwrap_or_default();
120    info!("Client process ID: {:?}", init_params.process_id);
121    let client_name = init_params.client_info.map(|x| x.name);
122    info!("Client name: {client_name:?}");
123
124    let mut documents: HashMap<Url, DocumentState> = HashMap::new();
125
126    for msg in &connection.receiver {
127        match msg {
128            Message::Request(req) => {
129                info!("Received request: method={}, id={:?}", req.method, req.id);
130
131                if connection.handle_shutdown(&req)? {
132                    info!("Received shutdown request, exiting");
133                    return Ok(());
134                }
135
136                match req.method.as_ref() {
137                    GotoDefinition::METHOD => {
138                        handle_goto_definition(&connection, req, &documents)?;
139                    }
140                    HoverRequest::METHOD => {
141                        handle_hover(&connection, req, &documents)?;
142                    }
143                    CodeActionRequest::METHOD => {
144                        handle_code_action(&connection, req, &documents)?;
145                    }
146                    SelectionRangeRequest::METHOD => {
147                        handle_selection_range(&connection, req, &documents)?;
148                    }
149                    InlayHintRequest::METHOD => {
150                        handle_inlay_hints(&connection, req, &documents)?;
151                    }
152                    DocumentSymbolRequest::METHOD => {
153                        handle_document_symbol(&connection, req, &documents)?;
154                    }
155                    Completion::METHOD => {
156                        handle_completion(&connection, req, &documents)?;
157                    }
158                    "squawk/syntaxTree" => {
159                        handle_syntax_tree(&connection, req, &documents)?;
160                    }
161                    "squawk/tokens" => {
162                        handle_tokens(&connection, req, &documents)?;
163                    }
164                    References::METHOD => {
165                        handle_references(&connection, req, &documents)?;
166                    }
167                    _ => {
168                        info!("Ignoring unhandled request: {}", req.method);
169                    }
170                }
171            }
172            Message::Response(resp) => {
173                info!("Received response: id={:?}", resp.id);
174            }
175            Message::Notification(notif) => {
176                info!("Received notification: method={}", notif.method);
177                match notif.method.as_ref() {
178                    DidOpenTextDocument::METHOD => {
179                        handle_did_open(&connection, notif, &mut documents)?;
180                    }
181                    DidChangeTextDocument::METHOD => {
182                        handle_did_change(&connection, notif, &mut documents)?;
183                    }
184                    DidCloseTextDocument::METHOD => {
185                        handle_did_close(&connection, notif, &mut documents)?;
186                    }
187                    _ => {
188                        info!("Ignoring unhandled notification: {}", notif.method);
189                    }
190                }
191            }
192        }
193    }
194    Ok(())
195}
196
197fn handle_goto_definition(
198    connection: &Connection,
199    req: lsp_server::Request,
200    documents: &HashMap<Url, DocumentState>,
201) -> Result<()> {
202    let params: GotoDefinitionParams = serde_json::from_value(req.params)?;
203    let uri = params.text_document_position_params.text_document.uri;
204    let position = params.text_document_position_params.position;
205
206    let content = documents.get(&uri).map_or("", |doc| &doc.content);
207    let parse = SourceFile::parse(content);
208    let file = parse.tree();
209    let line_index = LineIndex::new(content);
210    let offset = lsp_utils::offset(&line_index, position).unwrap();
211
212    let ranges = goto_definition(&file, offset)
213        .into_iter()
214        .filter_map(|location| {
215            debug_assert!(
216                !location.range.contains(offset),
217                "Our target destination range must not include the source range otherwise go to def won't work in vscode."
218            );
219
220            let uri = match location.file {
221                squawk_ide::goto_definition::FileId::Current => uri.clone(),
222                squawk_ide::goto_definition::FileId::Builtins => builtins_url()?,
223            };
224
225            let line_index = match location.file {
226                squawk_ide::goto_definition::FileId::Current => &line_index,
227                squawk_ide::goto_definition::FileId::Builtins => &LineIndex::new(BUILTINS_SQL),
228            };
229            let range = lsp_utils::range(line_index, location.range);
230
231            Some(Location {
232                uri,
233                range,
234            })
235        })
236        .collect();
237
238    let result = GotoDefinitionResponse::Array(ranges);
239    let resp = Response {
240        id: req.id,
241        result: Some(serde_json::to_value(&result).unwrap()),
242        error: None,
243    };
244
245    connection.sender.send(Message::Response(resp))?;
246    Ok(())
247}
248
249fn handle_hover(
250    connection: &Connection,
251    req: lsp_server::Request,
252    documents: &HashMap<Url, DocumentState>,
253) -> Result<()> {
254    let params: HoverParams = serde_json::from_value(req.params)?;
255    let uri = params.text_document_position_params.text_document.uri;
256    let position = params.text_document_position_params.position;
257
258    let content = documents.get(&uri).map_or("", |doc| &doc.content);
259    let parse = SourceFile::parse(content);
260    let file = parse.tree();
261    let line_index = LineIndex::new(content);
262    let offset = lsp_utils::offset(&line_index, position).unwrap();
263
264    let type_info = hover(&file, offset);
265
266    let result = type_info.map(|type_str| Hover {
267        contents: HoverContents::Scalar(MarkedString::LanguageString(LanguageString {
268            language: "sql".to_string(),
269            value: type_str,
270        })),
271        range: None,
272    });
273
274    let resp = Response {
275        id: req.id,
276        result: Some(serde_json::to_value(&result).unwrap()),
277        error: None,
278    };
279
280    connection.sender.send(Message::Response(resp))?;
281    Ok(())
282}
283
284fn handle_inlay_hints(
285    connection: &Connection,
286    req: lsp_server::Request,
287    documents: &HashMap<Url, DocumentState>,
288) -> Result<()> {
289    let params: InlayHintParams = serde_json::from_value(req.params)?;
290    let uri = params.text_document.uri;
291
292    let content = documents.get(&uri).map_or("", |doc| &doc.content);
293    let parse = SourceFile::parse(content);
294    let file = parse.tree();
295    let line_index = LineIndex::new(content);
296
297    let hints = inlay_hints(&file);
298
299    let lsp_hints: Vec<InlayHint> = hints
300        .into_iter()
301        .flat_map(|hint| {
302            let line_col = line_index.line_col(hint.position);
303            let position = lsp_types::Position::new(line_col.line, line_col.col);
304
305            let uri = match hint.file {
306                Some(squawk_ide::goto_definition::FileId::Current) | None => uri.clone(),
307                Some(squawk_ide::goto_definition::FileId::Builtins) => builtins_url()?,
308            };
309
310            let line_index = match hint.file {
311                Some(squawk_ide::goto_definition::FileId::Current) | None => &line_index,
312                Some(squawk_ide::goto_definition::FileId::Builtins) => {
313                    &LineIndex::new(BUILTINS_SQL)
314                }
315            };
316
317            let kind: InlayHintKind = match hint.kind {
318                squawk_ide::inlay_hints::InlayHintKind::Type => InlayHintKind::TYPE,
319                squawk_ide::inlay_hints::InlayHintKind::Parameter => InlayHintKind::PARAMETER,
320            };
321
322            let label = if let Some(target_range) = hint.target {
323                InlayHintLabel::LabelParts(vec![InlayHintLabelPart {
324                    value: hint.label,
325                    location: Some(Location {
326                        uri: uri.clone(),
327                        range: lsp_utils::range(line_index, target_range),
328                    }),
329                    tooltip: None,
330                    command: None,
331                }])
332            } else {
333                InlayHintLabel::String(hint.label)
334            };
335
336            Some(InlayHint {
337                position,
338                label,
339                kind: Some(kind),
340                text_edits: None,
341                tooltip: None,
342                padding_left: None,
343                padding_right: None,
344                data: None,
345            })
346        })
347        .collect();
348
349    let resp = Response {
350        id: req.id,
351        result: Some(serde_json::to_value(&lsp_hints).unwrap()),
352        error: None,
353    };
354
355    connection.sender.send(Message::Response(resp))?;
356    Ok(())
357}
358
359fn handle_document_symbol(
360    connection: &Connection,
361    req: lsp_server::Request,
362    documents: &HashMap<Url, DocumentState>,
363) -> Result<()> {
364    let params: DocumentSymbolParams = serde_json::from_value(req.params)?;
365    let uri = params.text_document.uri;
366
367    let content = documents.get(&uri).map_or("", |doc| &doc.content);
368    let parse = SourceFile::parse(content);
369    let file = parse.tree();
370    let line_index = LineIndex::new(content);
371
372    let symbols = document_symbols(&file);
373
374    fn convert_symbol(
375        sym: squawk_ide::document_symbols::DocumentSymbol,
376        line_index: &LineIndex,
377    ) -> DocumentSymbol {
378        let range = lsp_utils::range(line_index, sym.full_range);
379        let selection_range = lsp_utils::range(line_index, sym.focus_range);
380
381        let children = sym
382            .children
383            .into_iter()
384            .map(|child| convert_symbol(child, line_index))
385            .collect::<Vec<_>>();
386
387        let children = (!children.is_empty()).then_some(children);
388
389        DocumentSymbol {
390            name: sym.name,
391            detail: sym.detail,
392            kind: match sym.kind {
393                DocumentSymbolKind::Schema => SymbolKind::NAMESPACE,
394                DocumentSymbolKind::Table => SymbolKind::STRUCT,
395                DocumentSymbolKind::View => SymbolKind::STRUCT,
396                DocumentSymbolKind::MaterializedView => SymbolKind::STRUCT,
397                DocumentSymbolKind::Function => SymbolKind::FUNCTION,
398                DocumentSymbolKind::Aggregate => SymbolKind::FUNCTION,
399                DocumentSymbolKind::Procedure => SymbolKind::FUNCTION,
400                DocumentSymbolKind::Type => SymbolKind::CLASS,
401                DocumentSymbolKind::Enum => SymbolKind::ENUM,
402                DocumentSymbolKind::Index => SymbolKind::KEY,
403                DocumentSymbolKind::Domain => SymbolKind::CLASS,
404                DocumentSymbolKind::Sequence => SymbolKind::CONSTANT,
405                DocumentSymbolKind::Trigger => SymbolKind::EVENT,
406                DocumentSymbolKind::Tablespace => SymbolKind::NAMESPACE,
407                DocumentSymbolKind::Database => SymbolKind::MODULE,
408                DocumentSymbolKind::Server => SymbolKind::OBJECT,
409                DocumentSymbolKind::Extension => SymbolKind::PACKAGE,
410                DocumentSymbolKind::Column => SymbolKind::FIELD,
411                DocumentSymbolKind::Variant => SymbolKind::ENUM_MEMBER,
412                DocumentSymbolKind::Cursor => SymbolKind::VARIABLE,
413                DocumentSymbolKind::PreparedStatement => SymbolKind::VARIABLE,
414                DocumentSymbolKind::Channel => SymbolKind::EVENT,
415                DocumentSymbolKind::EventTrigger => SymbolKind::EVENT,
416                DocumentSymbolKind::Role => SymbolKind::CLASS,
417                DocumentSymbolKind::Policy => SymbolKind::VARIABLE,
418            },
419            tags: None,
420            range,
421            selection_range,
422            children,
423            #[allow(deprecated)]
424            deprecated: None,
425        }
426    }
427
428    let lsp_symbols: Vec<DocumentSymbol> = symbols
429        .into_iter()
430        .map(|sym| convert_symbol(sym, &line_index))
431        .collect();
432
433    let resp = Response {
434        id: req.id,
435        result: Some(serde_json::to_value(&lsp_symbols).unwrap()),
436        error: None,
437    };
438
439    connection.sender.send(Message::Response(resp))?;
440    Ok(())
441}
442
443fn handle_selection_range(
444    connection: &Connection,
445    req: lsp_server::Request,
446    documents: &HashMap<Url, DocumentState>,
447) -> Result<()> {
448    let params: SelectionRangeParams = serde_json::from_value(req.params)?;
449    let uri = params.text_document.uri;
450
451    let content = documents.get(&uri).map_or("", |doc| &doc.content);
452    let parse = SourceFile::parse(content);
453    let root = parse.syntax_node();
454    let line_index = LineIndex::new(content);
455
456    let mut selection_ranges = vec![];
457
458    for position in params.positions {
459        let Some(offset) = lsp_utils::offset(&line_index, position) else {
460            continue;
461        };
462
463        let mut ranges = Vec::new();
464        {
465            let mut range = TextRange::new(offset, offset);
466            loop {
467                ranges.push(range);
468                let next = squawk_ide::expand_selection::extend_selection(&root, range);
469                if next == range {
470                    break;
471                } else {
472                    range = next
473                }
474            }
475        }
476
477        let mut range = lsp_types::SelectionRange {
478            range: lsp_utils::range(&line_index, *ranges.last().unwrap()),
479            parent: None,
480        };
481        for &r in ranges.iter().rev().skip(1) {
482            range = lsp_types::SelectionRange {
483                range: lsp_utils::range(&line_index, r),
484                parent: Some(Box::new(range)),
485            }
486        }
487        selection_ranges.push(range);
488    }
489
490    let resp = Response {
491        id: req.id,
492        result: Some(serde_json::to_value(&selection_ranges).unwrap()),
493        error: None,
494    };
495
496    connection.sender.send(Message::Response(resp))?;
497    Ok(())
498}
499
500fn handle_references(
501    connection: &Connection,
502    req: lsp_server::Request,
503    documents: &HashMap<Url, DocumentState>,
504) -> Result<()> {
505    let params: ReferenceParams = serde_json::from_value(req.params)?;
506    let uri = params.text_document_position.text_document.uri;
507    let position = params.text_document_position.position;
508
509    let content = documents.get(&uri).map_or("", |doc| &doc.content);
510    let parse = SourceFile::parse(content);
511    let file = parse.tree();
512    let line_index = LineIndex::new(content);
513    let offset = lsp_utils::offset(&line_index, position).unwrap();
514
515    let refs = find_references(&file, offset);
516    let include_declaration = params.context.include_declaration;
517
518    let locations: Vec<Location> = refs
519        .into_iter()
520        .filter(|loc| include_declaration || !loc.range.contains(offset))
521        .filter_map(|loc| {
522            let uri = match loc.file {
523                squawk_ide::goto_definition::FileId::Current => uri.clone(),
524                squawk_ide::goto_definition::FileId::Builtins => builtins_url()?,
525            };
526            let line_index = match loc.file {
527                squawk_ide::goto_definition::FileId::Current => &line_index,
528                squawk_ide::goto_definition::FileId::Builtins => &LineIndex::new(BUILTINS_SQL),
529            };
530            Some(Location {
531                uri,
532                range: lsp_utils::range(line_index, loc.range),
533            })
534        })
535        .collect();
536
537    let resp = Response {
538        id: req.id,
539        result: Some(serde_json::to_value(&locations).unwrap()),
540        error: None,
541    };
542
543    connection.sender.send(Message::Response(resp))?;
544    Ok(())
545}
546
547fn handle_completion(
548    connection: &Connection,
549    req: lsp_server::Request,
550    documents: &HashMap<Url, DocumentState>,
551) -> Result<()> {
552    let params: CompletionParams = serde_json::from_value(req.params)?;
553    let uri = params.text_document_position.text_document.uri;
554    let position = params.text_document_position.position;
555
556    let content = documents.get(&uri).map_or("", |doc| &doc.content);
557    let parse = SourceFile::parse(content);
558    let file = parse.tree();
559    let line_index = LineIndex::new(content);
560
561    let Some(offset) = lsp_utils::offset(&line_index, position) else {
562        let resp = Response {
563            id: req.id,
564            result: Some(serde_json::to_value(CompletionResponse::Array(vec![])).unwrap()),
565            error: None,
566        };
567        connection.sender.send(Message::Response(resp))?;
568        return Ok(());
569    };
570
571    let completion_items = completion(&file, offset)
572        .into_iter()
573        .map(lsp_utils::completion_item)
574        .collect();
575
576    let result = CompletionResponse::Array(completion_items);
577
578    let resp = Response {
579        id: req.id,
580        result: Some(serde_json::to_value(&result).unwrap()),
581        error: None,
582    };
583
584    connection.sender.send(Message::Response(resp))?;
585    Ok(())
586}
587
588fn handle_code_action(
589    connection: &Connection,
590    req: lsp_server::Request,
591    documents: &HashMap<Url, DocumentState>,
592) -> Result<()> {
593    let params: CodeActionParams = serde_json::from_value(req.params)?;
594    let uri = params.text_document.uri;
595
596    let mut actions: CodeActionResponse = Vec::new();
597
598    let content = documents.get(&uri).map_or("", |doc| &doc.content);
599    let parse = SourceFile::parse(content);
600    let file = parse.tree();
601    let line_index = LineIndex::new(content);
602    let offset = lsp_utils::offset(&line_index, params.range.start).unwrap();
603
604    let ide_actions = code_actions(file, offset).unwrap_or_default();
605
606    for action in ide_actions {
607        let lsp_action = lsp_utils::code_action(&line_index, uri.clone(), action);
608        actions.push(CodeActionOrCommand::CodeAction(lsp_action));
609    }
610
611    for mut diagnostic in params
612        .context
613        .diagnostics
614        .into_iter()
615        .filter(|diagnostic| diagnostic.source.as_deref() == Some(DIAGNOSTIC_NAME))
616    {
617        let Some(rule_name) = diagnostic.code.as_ref().map(|x| match x {
618            lsp_types::NumberOrString::String(s) => s.clone(),
619            lsp_types::NumberOrString::Number(n) => n.to_string(),
620        }) else {
621            continue;
622        };
623        let Some(data) = diagnostic.data.take() else {
624            continue;
625        };
626
627        let associated_data: AssociatedDiagnosticData =
628            serde_json::from_value(data).context("deserializing diagnostic data")?;
629
630        if let Some(ignore_line_edit) = associated_data.ignore_line_edit {
631            let disable_line_action = CodeAction {
632                title: format!("Disable {rule_name} for this line"),
633                kind: Some(CodeActionKind::QUICKFIX),
634                diagnostics: Some(vec![diagnostic.clone()]),
635                edit: Some(WorkspaceEdit {
636                    changes: Some({
637                        let mut changes = HashMap::new();
638                        changes.insert(uri.clone(), vec![ignore_line_edit]);
639                        changes
640                    }),
641                    ..Default::default()
642                }),
643                command: None,
644                is_preferred: Some(false),
645                disabled: None,
646                data: None,
647            };
648            actions.push(CodeActionOrCommand::CodeAction(disable_line_action));
649        }
650        if let Some(ignore_file_edit) = associated_data.ignore_file_edit {
651            let disable_file_action = CodeAction {
652                title: format!("Disable {rule_name} for the entire file"),
653                kind: Some(CodeActionKind::QUICKFIX),
654                diagnostics: Some(vec![diagnostic.clone()]),
655                edit: Some(WorkspaceEdit {
656                    changes: Some({
657                        let mut changes = HashMap::new();
658                        changes.insert(uri.clone(), vec![ignore_file_edit]);
659                        changes
660                    }),
661                    ..Default::default()
662                }),
663                command: None,
664                is_preferred: Some(false),
665                disabled: None,
666                data: None,
667            };
668            actions.push(CodeActionOrCommand::CodeAction(disable_file_action));
669        }
670
671        let title = format!("Show documentation for {rule_name}");
672        let documentation_action = CodeAction {
673            title: title.clone(),
674            kind: Some(CodeActionKind::QUICKFIX),
675            diagnostics: Some(vec![diagnostic.clone()]),
676            edit: None,
677            command: Some(Command {
678                title,
679                command: "vscode.open".to_string(),
680                arguments: Some(vec![serde_json::to_value(format!(
681                    "https://squawkhq.com/docs/{rule_name}"
682                ))?]),
683            }),
684            is_preferred: Some(false),
685            disabled: None,
686            data: None,
687        };
688        actions.push(CodeActionOrCommand::CodeAction(documentation_action));
689
690        if !associated_data.title.is_empty() && !associated_data.edits.is_empty() {
691            let fix_action = CodeAction {
692                title: associated_data.title,
693                kind: Some(CodeActionKind::QUICKFIX),
694                diagnostics: Some(vec![diagnostic.clone()]),
695                edit: Some(WorkspaceEdit {
696                    changes: Some({
697                        let mut changes = HashMap::new();
698                        changes.insert(uri.clone(), associated_data.edits);
699                        changes
700                    }),
701                    ..Default::default()
702                }),
703                command: None,
704                is_preferred: Some(true),
705                disabled: None,
706                data: None,
707            };
708            actions.push(CodeActionOrCommand::CodeAction(fix_action));
709        }
710    }
711
712    let result: CodeActionResponse = actions;
713    let resp = Response {
714        id: req.id,
715        result: Some(serde_json::to_value(&result).unwrap()),
716        error: None,
717    };
718
719    connection.sender.send(Message::Response(resp))?;
720    Ok(())
721}
722
723fn publish_diagnostics(
724    connection: &Connection,
725    uri: Url,
726    version: i32,
727    diagnostics: Vec<Diagnostic>,
728) -> Result<()> {
729    let publish_params = PublishDiagnosticsParams {
730        uri,
731        diagnostics,
732        version: Some(version),
733    };
734
735    let notification = Notification {
736        method: PublishDiagnostics::METHOD.to_owned(),
737        params: serde_json::to_value(publish_params)?,
738    };
739
740    connection
741        .sender
742        .send(Message::Notification(notification))?;
743    Ok(())
744}
745
746fn handle_did_open(
747    connection: &Connection,
748    notif: lsp_server::Notification,
749    documents: &mut HashMap<Url, DocumentState>,
750) -> Result<()> {
751    let params: DidOpenTextDocumentParams = serde_json::from_value(notif.params)?;
752    let uri = params.text_document.uri;
753    let content = params.text_document.text;
754    let version = params.text_document.version;
755
756    documents.insert(uri.clone(), DocumentState { content, version });
757
758    let content = documents.get(&uri).map_or("", |doc| &doc.content);
759
760    // TODO: we need a better setup for "run func when input changed"
761    let diagnostics = lint::lint(content);
762    publish_diagnostics(connection, uri, version, diagnostics)?;
763
764    Ok(())
765}
766
767fn handle_did_change(
768    connection: &Connection,
769    notif: lsp_server::Notification,
770    documents: &mut HashMap<Url, DocumentState>,
771) -> Result<()> {
772    let params: DidChangeTextDocumentParams = serde_json::from_value(notif.params)?;
773    let uri = params.text_document.uri;
774    let version = params.text_document.version;
775
776    let Some(doc_state) = documents.get_mut(&uri) else {
777        return Ok(());
778    };
779
780    doc_state.content =
781        lsp_utils::apply_incremental_changes(&doc_state.content, params.content_changes);
782    doc_state.version = version;
783
784    let diagnostics = lint::lint(&doc_state.content);
785    publish_diagnostics(connection, uri, version, diagnostics)?;
786
787    Ok(())
788}
789
790fn handle_did_close(
791    connection: &Connection,
792    notif: lsp_server::Notification,
793    documents: &mut HashMap<Url, DocumentState>,
794) -> Result<()> {
795    let params: DidCloseTextDocumentParams = serde_json::from_value(notif.params)?;
796    let uri = params.text_document.uri;
797
798    documents.remove(&uri);
799
800    let publish_params = PublishDiagnosticsParams {
801        uri,
802        diagnostics: vec![],
803        version: None,
804    };
805
806    let notification = Notification {
807        method: PublishDiagnostics::METHOD.to_owned(),
808        params: serde_json::to_value(publish_params)?,
809    };
810
811    connection
812        .sender
813        .send(Message::Notification(notification))?;
814
815    Ok(())
816}
817
818#[derive(serde::Deserialize)]
819struct SyntaxTreeParams {
820    #[serde(rename = "textDocument")]
821    text_document: lsp_types::TextDocumentIdentifier,
822}
823
824fn handle_syntax_tree(
825    connection: &Connection,
826    req: lsp_server::Request,
827    documents: &HashMap<Url, DocumentState>,
828) -> Result<()> {
829    let params: SyntaxTreeParams = serde_json::from_value(req.params)?;
830    let uri = params.text_document.uri;
831
832    info!("Generating syntax tree for: {uri}");
833
834    let content = documents.get(&uri).map_or("", |doc| &doc.content);
835
836    let parse = SourceFile::parse(content);
837    let syntax_tree = format!("{:#?}", parse.syntax_node());
838
839    let resp = Response {
840        id: req.id,
841        result: Some(serde_json::to_value(&syntax_tree).unwrap()),
842        error: None,
843    };
844
845    connection.sender.send(Message::Response(resp))?;
846    Ok(())
847}
848
849#[derive(serde::Deserialize)]
850struct TokensParams {
851    #[serde(rename = "textDocument")]
852    text_document: lsp_types::TextDocumentIdentifier,
853}
854
855fn handle_tokens(
856    connection: &Connection,
857    req: lsp_server::Request,
858    documents: &HashMap<Url, DocumentState>,
859) -> Result<()> {
860    let params: TokensParams = serde_json::from_value(req.params)?;
861    let uri = params.text_document.uri;
862
863    info!("Generating tokens for: {uri}");
864
865    let content = documents.get(&uri).map_or("", |doc| &doc.content);
866
867    let tokens = squawk_lexer::tokenize(content);
868
869    let mut output = Vec::new();
870    let mut char_pos = 0;
871    for token in tokens {
872        let token_start = char_pos;
873        let token_end = token_start + token.len as usize;
874        let token_text = &content[token_start..token_end];
875        output.push(format!(
876            "{:?}@{}..{} {:?}",
877            token.kind, token_start, token_end, token_text
878        ));
879        char_pos = token_end;
880    }
881
882    let tokens_output = output.join("\n");
883
884    let resp = Response {
885        id: req.id,
886        result: Some(serde_json::to_value(&tokens_output).unwrap()),
887        error: None,
888    };
889
890    connection.sender.send(Message::Response(resp))?;
891    Ok(())
892}