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 range = goto_definition(file, offset);
182
183    let result = match range {
184        Some(target_range) => {
185            debug_assert!(
186                !target_range.contains(offset),
187                "Our target destination range must not include the source range otherwise go to def won't work in vscode."
188            );
189            GotoDefinitionResponse::Scalar(Location {
190                uri: uri.clone(),
191                range: lsp_utils::range(&line_index, target_range),
192            })
193        }
194        None => GotoDefinitionResponse::Array(vec![]),
195    };
196
197    let resp = Response {
198        id: req.id,
199        result: Some(serde_json::to_value(&result).unwrap()),
200        error: None,
201    };
202
203    connection.sender.send(Message::Response(resp))?;
204    Ok(())
205}
206
207fn handle_hover(
208    connection: &Connection,
209    req: lsp_server::Request,
210    documents: &HashMap<Url, DocumentState>,
211) -> Result<()> {
212    let params: HoverParams = serde_json::from_value(req.params)?;
213    let uri = params.text_document_position_params.text_document.uri;
214    let position = params.text_document_position_params.position;
215
216    let content = documents.get(&uri).map_or("", |doc| &doc.content);
217    let parse = SourceFile::parse(content);
218    let file = parse.tree();
219    let line_index = LineIndex::new(content);
220    let offset = lsp_utils::offset(&line_index, position).unwrap();
221
222    let type_info = hover(&file, offset);
223
224    let result = type_info.map(|type_str| Hover {
225        contents: HoverContents::Scalar(MarkedString::LanguageString(LanguageString {
226            language: "sql".to_string(),
227            value: type_str,
228        })),
229        range: None,
230    });
231
232    let resp = Response {
233        id: req.id,
234        result: Some(serde_json::to_value(&result).unwrap()),
235        error: None,
236    };
237
238    connection.sender.send(Message::Response(resp))?;
239    Ok(())
240}
241
242fn handle_inlay_hints(
243    connection: &Connection,
244    req: lsp_server::Request,
245    documents: &HashMap<Url, DocumentState>,
246) -> Result<()> {
247    let params: InlayHintParams = serde_json::from_value(req.params)?;
248    let uri = params.text_document.uri;
249
250    let content = documents.get(&uri).map_or("", |doc| &doc.content);
251    let parse = SourceFile::parse(content);
252    let file = parse.tree();
253    let line_index = LineIndex::new(content);
254
255    let hints = inlay_hints(&file);
256
257    let lsp_hints: Vec<InlayHint> = hints
258        .into_iter()
259        .map(|hint| {
260            let line_col = line_index.line_col(hint.position);
261            let position = lsp_types::Position::new(line_col.line, line_col.col);
262            let kind = match hint.kind {
263                squawk_ide::inlay_hints::InlayHintKind::Type => InlayHintKind::TYPE,
264                squawk_ide::inlay_hints::InlayHintKind::Parameter => InlayHintKind::PARAMETER,
265            };
266
267            let label = if let Some(target_range) = hint.target {
268                InlayHintLabel::LabelParts(vec![InlayHintLabelPart {
269                    value: hint.label,
270                    location: Some(Location {
271                        uri: uri.clone(),
272                        range: lsp_utils::range(&line_index, target_range),
273                    }),
274                    tooltip: None,
275                    command: None,
276                }])
277            } else {
278                InlayHintLabel::String(hint.label)
279            };
280
281            InlayHint {
282                position,
283                label,
284                kind: Some(kind),
285                text_edits: None,
286                tooltip: None,
287                padding_left: None,
288                padding_right: None,
289                data: None,
290            }
291        })
292        .collect();
293
294    let resp = Response {
295        id: req.id,
296        result: Some(serde_json::to_value(&lsp_hints).unwrap()),
297        error: None,
298    };
299
300    connection.sender.send(Message::Response(resp))?;
301    Ok(())
302}
303
304fn handle_document_symbol(
305    connection: &Connection,
306    req: lsp_server::Request,
307    documents: &HashMap<Url, DocumentState>,
308) -> Result<()> {
309    let params: DocumentSymbolParams = serde_json::from_value(req.params)?;
310    let uri = params.text_document.uri;
311
312    let content = documents.get(&uri).map_or("", |doc| &doc.content);
313    let parse = SourceFile::parse(content);
314    let file = parse.tree();
315    let line_index = LineIndex::new(content);
316
317    let symbols = document_symbols(&file);
318
319    fn convert_symbol(
320        sym: squawk_ide::document_symbols::DocumentSymbol,
321        line_index: &LineIndex,
322    ) -> DocumentSymbol {
323        let range = lsp_utils::range(line_index, sym.full_range);
324        let selection_range = lsp_utils::range(line_index, sym.focus_range);
325
326        let children = sym
327            .children
328            .into_iter()
329            .map(|child| convert_symbol(child, line_index))
330            .collect::<Vec<_>>();
331
332        let children = (!children.is_empty()).then_some(children);
333
334        DocumentSymbol {
335            name: sym.name,
336            detail: sym.detail,
337            kind: match sym.kind {
338                DocumentSymbolKind::Table => SymbolKind::STRUCT,
339                DocumentSymbolKind::Function => SymbolKind::FUNCTION,
340                DocumentSymbolKind::Type => SymbolKind::CLASS,
341                DocumentSymbolKind::Column => SymbolKind::FIELD,
342                DocumentSymbolKind::Variant => SymbolKind::ENUM_MEMBER,
343            },
344            tags: None,
345            range,
346            selection_range,
347            children,
348            #[allow(deprecated)]
349            deprecated: None,
350        }
351    }
352
353    let lsp_symbols: Vec<DocumentSymbol> = symbols
354        .into_iter()
355        .map(|sym| convert_symbol(sym, &line_index))
356        .collect();
357
358    let resp = Response {
359        id: req.id,
360        result: Some(serde_json::to_value(&lsp_symbols).unwrap()),
361        error: None,
362    };
363
364    connection.sender.send(Message::Response(resp))?;
365    Ok(())
366}
367
368fn handle_selection_range(
369    connection: &Connection,
370    req: lsp_server::Request,
371    documents: &HashMap<Url, DocumentState>,
372) -> Result<()> {
373    let params: SelectionRangeParams = serde_json::from_value(req.params)?;
374    let uri = params.text_document.uri;
375
376    let content = documents.get(&uri).map_or("", |doc| &doc.content);
377    let parse = SourceFile::parse(content);
378    let root = parse.syntax_node();
379    let line_index = LineIndex::new(content);
380
381    let mut selection_ranges = vec![];
382
383    for position in params.positions {
384        let Some(offset) = lsp_utils::offset(&line_index, position) else {
385            continue;
386        };
387
388        let mut ranges = Vec::new();
389        {
390            let mut range = TextRange::new(offset, offset);
391            loop {
392                ranges.push(range);
393                let next = squawk_ide::expand_selection::extend_selection(&root, range);
394                if next == range {
395                    break;
396                } else {
397                    range = next
398                }
399            }
400        }
401
402        let mut range = lsp_types::SelectionRange {
403            range: lsp_utils::range(&line_index, *ranges.last().unwrap()),
404            parent: None,
405        };
406        for &r in ranges.iter().rev().skip(1) {
407            range = lsp_types::SelectionRange {
408                range: lsp_utils::range(&line_index, r),
409                parent: Some(Box::new(range)),
410            }
411        }
412        selection_ranges.push(range);
413    }
414
415    let resp = Response {
416        id: req.id,
417        result: Some(serde_json::to_value(&selection_ranges).unwrap()),
418        error: None,
419    };
420
421    connection.sender.send(Message::Response(resp))?;
422    Ok(())
423}
424
425fn handle_references(
426    connection: &Connection,
427    req: lsp_server::Request,
428    documents: &HashMap<Url, DocumentState>,
429) -> Result<()> {
430    let params: ReferenceParams = serde_json::from_value(req.params)?;
431    let uri = params.text_document_position.text_document.uri;
432    let position = params.text_document_position.position;
433
434    let content = documents.get(&uri).map_or("", |doc| &doc.content);
435    let parse = SourceFile::parse(content);
436    let file = parse.tree();
437    let line_index = LineIndex::new(content);
438    let offset = lsp_utils::offset(&line_index, position).unwrap();
439
440    let ranges = find_references(&file, offset);
441    let include_declaration = params.context.include_declaration;
442
443    let locations: Vec<Location> = ranges
444        .into_iter()
445        .filter(|range| include_declaration || !range.contains(offset))
446        .map(|range| Location {
447            uri: uri.clone(),
448            range: lsp_utils::range(&line_index, range),
449        })
450        .collect();
451
452    let resp = Response {
453        id: req.id,
454        result: Some(serde_json::to_value(&locations).unwrap()),
455        error: None,
456    };
457
458    connection.sender.send(Message::Response(resp))?;
459    Ok(())
460}
461
462fn handle_code_action(
463    connection: &Connection,
464    req: lsp_server::Request,
465    documents: &HashMap<Url, DocumentState>,
466) -> Result<()> {
467    let params: CodeActionParams = serde_json::from_value(req.params)?;
468    let uri = params.text_document.uri;
469
470    let mut actions: CodeActionResponse = Vec::new();
471
472    let content = documents.get(&uri).map_or("", |doc| &doc.content);
473    let parse = SourceFile::parse(content);
474    let file = parse.tree();
475    let line_index = LineIndex::new(content);
476    let offset = lsp_utils::offset(&line_index, params.range.start).unwrap();
477
478    let ide_actions = code_actions(file, offset).unwrap_or_default();
479
480    for action in ide_actions {
481        let lsp_action = lsp_utils::code_action(&line_index, uri.clone(), action);
482        actions.push(CodeActionOrCommand::CodeAction(lsp_action));
483    }
484
485    for mut diagnostic in params
486        .context
487        .diagnostics
488        .into_iter()
489        .filter(|diagnostic| diagnostic.source.as_deref() == Some(DIAGNOSTIC_NAME))
490    {
491        let Some(rule_name) = diagnostic.code.as_ref().map(|x| match x {
492            lsp_types::NumberOrString::String(s) => s.clone(),
493            lsp_types::NumberOrString::Number(n) => n.to_string(),
494        }) else {
495            continue;
496        };
497        let Some(data) = diagnostic.data.take() else {
498            continue;
499        };
500
501        let associated_data: AssociatedDiagnosticData =
502            serde_json::from_value(data).context("deserializing diagnostic data")?;
503
504        if let Some(ignore_line_edit) = associated_data.ignore_line_edit {
505            let disable_line_action = CodeAction {
506                title: format!("Disable {rule_name} for this line"),
507                kind: Some(CodeActionKind::QUICKFIX),
508                diagnostics: Some(vec![diagnostic.clone()]),
509                edit: Some(WorkspaceEdit {
510                    changes: Some({
511                        let mut changes = HashMap::new();
512                        changes.insert(uri.clone(), vec![ignore_line_edit]);
513                        changes
514                    }),
515                    ..Default::default()
516                }),
517                command: None,
518                is_preferred: Some(false),
519                disabled: None,
520                data: None,
521            };
522            actions.push(CodeActionOrCommand::CodeAction(disable_line_action));
523        }
524        if let Some(ignore_file_edit) = associated_data.ignore_file_edit {
525            let disable_file_action = CodeAction {
526                title: format!("Disable {rule_name} for the entire file"),
527                kind: Some(CodeActionKind::QUICKFIX),
528                diagnostics: Some(vec![diagnostic.clone()]),
529                edit: Some(WorkspaceEdit {
530                    changes: Some({
531                        let mut changes = HashMap::new();
532                        changes.insert(uri.clone(), vec![ignore_file_edit]);
533                        changes
534                    }),
535                    ..Default::default()
536                }),
537                command: None,
538                is_preferred: Some(false),
539                disabled: None,
540                data: None,
541            };
542            actions.push(CodeActionOrCommand::CodeAction(disable_file_action));
543        }
544
545        let title = format!("Show documentation for {rule_name}");
546        let documentation_action = CodeAction {
547            title: title.clone(),
548            kind: Some(CodeActionKind::QUICKFIX),
549            diagnostics: Some(vec![diagnostic.clone()]),
550            edit: None,
551            command: Some(Command {
552                title,
553                command: "vscode.open".to_string(),
554                arguments: Some(vec![serde_json::to_value(format!(
555                    "https://squawkhq.com/docs/{rule_name}"
556                ))?]),
557            }),
558            is_preferred: Some(false),
559            disabled: None,
560            data: None,
561        };
562        actions.push(CodeActionOrCommand::CodeAction(documentation_action));
563
564        if !associated_data.title.is_empty() && !associated_data.edits.is_empty() {
565            let fix_action = CodeAction {
566                title: associated_data.title,
567                kind: Some(CodeActionKind::QUICKFIX),
568                diagnostics: Some(vec![diagnostic.clone()]),
569                edit: Some(WorkspaceEdit {
570                    changes: Some({
571                        let mut changes = HashMap::new();
572                        changes.insert(uri.clone(), associated_data.edits);
573                        changes
574                    }),
575                    ..Default::default()
576                }),
577                command: None,
578                is_preferred: Some(true),
579                disabled: None,
580                data: None,
581            };
582            actions.push(CodeActionOrCommand::CodeAction(fix_action));
583        }
584    }
585
586    let result: CodeActionResponse = actions;
587    let resp = Response {
588        id: req.id,
589        result: Some(serde_json::to_value(&result).unwrap()),
590        error: None,
591    };
592
593    connection.sender.send(Message::Response(resp))?;
594    Ok(())
595}
596
597fn publish_diagnostics(
598    connection: &Connection,
599    uri: Url,
600    version: i32,
601    diagnostics: Vec<Diagnostic>,
602) -> Result<()> {
603    let publish_params = PublishDiagnosticsParams {
604        uri,
605        diagnostics,
606        version: Some(version),
607    };
608
609    let notification = Notification {
610        method: PublishDiagnostics::METHOD.to_owned(),
611        params: serde_json::to_value(publish_params)?,
612    };
613
614    connection
615        .sender
616        .send(Message::Notification(notification))?;
617    Ok(())
618}
619
620fn handle_did_open(
621    connection: &Connection,
622    notif: lsp_server::Notification,
623    documents: &mut HashMap<Url, DocumentState>,
624) -> Result<()> {
625    let params: DidOpenTextDocumentParams = serde_json::from_value(notif.params)?;
626    let uri = params.text_document.uri;
627    let content = params.text_document.text;
628    let version = params.text_document.version;
629
630    documents.insert(uri.clone(), DocumentState { content, version });
631
632    let content = documents.get(&uri).map_or("", |doc| &doc.content);
633
634    // TODO: we need a better setup for "run func when input changed"
635    let diagnostics = lint::lint(content);
636    publish_diagnostics(connection, uri, version, diagnostics)?;
637
638    Ok(())
639}
640
641fn handle_did_change(
642    connection: &Connection,
643    notif: lsp_server::Notification,
644    documents: &mut HashMap<Url, DocumentState>,
645) -> Result<()> {
646    let params: DidChangeTextDocumentParams = serde_json::from_value(notif.params)?;
647    let uri = params.text_document.uri;
648    let version = params.text_document.version;
649
650    let Some(doc_state) = documents.get_mut(&uri) else {
651        return Ok(());
652    };
653
654    doc_state.content =
655        lsp_utils::apply_incremental_changes(&doc_state.content, params.content_changes);
656    doc_state.version = version;
657
658    let diagnostics = lint::lint(&doc_state.content);
659    publish_diagnostics(connection, uri, version, diagnostics)?;
660
661    Ok(())
662}
663
664fn handle_did_close(
665    connection: &Connection,
666    notif: lsp_server::Notification,
667    documents: &mut HashMap<Url, DocumentState>,
668) -> Result<()> {
669    let params: DidCloseTextDocumentParams = serde_json::from_value(notif.params)?;
670    let uri = params.text_document.uri;
671
672    documents.remove(&uri);
673
674    let publish_params = PublishDiagnosticsParams {
675        uri,
676        diagnostics: vec![],
677        version: None,
678    };
679
680    let notification = Notification {
681        method: PublishDiagnostics::METHOD.to_owned(),
682        params: serde_json::to_value(publish_params)?,
683    };
684
685    connection
686        .sender
687        .send(Message::Notification(notification))?;
688
689    Ok(())
690}
691
692#[derive(serde::Deserialize)]
693struct SyntaxTreeParams {
694    #[serde(rename = "textDocument")]
695    text_document: lsp_types::TextDocumentIdentifier,
696}
697
698fn handle_syntax_tree(
699    connection: &Connection,
700    req: lsp_server::Request,
701    documents: &HashMap<Url, DocumentState>,
702) -> Result<()> {
703    let params: SyntaxTreeParams = serde_json::from_value(req.params)?;
704    let uri = params.text_document.uri;
705
706    info!("Generating syntax tree for: {uri}");
707
708    let content = documents.get(&uri).map_or("", |doc| &doc.content);
709
710    let parse = SourceFile::parse(content);
711    let syntax_tree = format!("{:#?}", parse.syntax_node());
712
713    let resp = Response {
714        id: req.id,
715        result: Some(serde_json::to_value(&syntax_tree).unwrap()),
716        error: None,
717    };
718
719    connection.sender.send(Message::Response(resp))?;
720    Ok(())
721}
722
723#[derive(serde::Deserialize)]
724struct TokensParams {
725    #[serde(rename = "textDocument")]
726    text_document: lsp_types::TextDocumentIdentifier,
727}
728
729fn handle_tokens(
730    connection: &Connection,
731    req: lsp_server::Request,
732    documents: &HashMap<Url, DocumentState>,
733) -> Result<()> {
734    let params: TokensParams = serde_json::from_value(req.params)?;
735    let uri = params.text_document.uri;
736
737    info!("Generating tokens for: {uri}");
738
739    let content = documents.get(&uri).map_or("", |doc| &doc.content);
740
741    let tokens = squawk_lexer::tokenize(content);
742
743    let mut output = Vec::new();
744    let mut char_pos = 0;
745    for token in tokens {
746        let token_start = char_pos;
747        let token_end = token_start + token.len as usize;
748        let token_text = &content[token_start..token_end];
749        output.push(format!(
750            "{:?}@{}..{} {:?}",
751            token.kind, token_start, token_end, token_text
752        ));
753        char_pos = token_end;
754    }
755
756    let tokens_output = output.join("\n");
757
758    let resp = Response {
759        id: req.id,
760        result: Some(serde_json::to_value(&tokens_output).unwrap()),
761        error: None,
762    };
763
764    connection.sender.send(Message::Response(resp))?;
765    Ok(())
766}