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