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