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 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 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}