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