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