diff --git a/crates/squawk_server/src/handlers.rs b/crates/squawk_server/src/handlers.rs new file mode 100644 index 00000000..44f2de87 --- /dev/null +++ b/crates/squawk_server/src/handlers.rs @@ -0,0 +1,25 @@ +mod code_action; +mod completion; +mod document_symbol; +mod folding_range; +mod goto_definition; +mod hover; +mod inlay_hints; +mod notifications; +mod references; +mod selection_range; +mod syntax_tree; +mod tokens; + +pub(crate) use code_action::handle_code_action; +pub(crate) use completion::handle_completion; +pub(crate) use document_symbol::handle_document_symbol; +pub(crate) use folding_range::handle_folding_range; +pub(crate) use goto_definition::handle_goto_definition; +pub(crate) use hover::handle_hover; +pub(crate) use inlay_hints::handle_inlay_hints; +pub(crate) use notifications::{handle_did_change, handle_did_close, handle_did_open}; +pub(crate) use references::handle_references; +pub(crate) use selection_range::handle_selection_range; +pub(crate) use syntax_tree::handle_syntax_tree; +pub(crate) use tokens::handle_tokens; diff --git a/crates/squawk_server/src/handlers/code_action.rs b/crates/squawk_server/src/handlers/code_action.rs new file mode 100644 index 00000000..80557300 --- /dev/null +++ b/crates/squawk_server/src/handlers/code_action.rs @@ -0,0 +1,147 @@ +use anyhow::{Context, Result}; +use lsp_server::{Connection, Message, Response}; +use lsp_types::{ + CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams, CodeActionResponse, Command, + WorkspaceEdit, +}; +use squawk_ide::code_actions::code_actions; +use squawk_ide::db::line_index; +use std::collections::HashMap; + +use crate::diagnostic::{AssociatedDiagnosticData, DIAGNOSTIC_NAME}; +use crate::lsp_utils; +use crate::system::System; + +pub(crate) fn handle_code_action( + connection: &Connection, + req: lsp_server::Request, + system: &impl System, +) -> Result<()> { + let params: CodeActionParams = serde_json::from_value(req.params)?; + let uri = params.text_document.uri; + + let mut actions: CodeActionResponse = vec![]; + + let db = system.db(); + let file = system.file(&uri).unwrap(); + let line_index = line_index(db, file); + let offset = lsp_utils::offset(&line_index, params.range.start).unwrap(); + + let ide_actions = code_actions(db, file, offset).unwrap_or_default(); + + for action in ide_actions { + let lsp_action = lsp_utils::code_action(&line_index, uri.clone(), action); + actions.push(CodeActionOrCommand::CodeAction(lsp_action)); + } + + for mut diagnostic in params + .context + .diagnostics + .into_iter() + .filter(|diagnostic| diagnostic.source.as_deref() == Some(DIAGNOSTIC_NAME)) + { + let Some(rule_name) = diagnostic.code.as_ref().map(|x| match x { + lsp_types::NumberOrString::String(s) => s.clone(), + lsp_types::NumberOrString::Number(n) => n.to_string(), + }) else { + continue; + }; + let Some(data) = diagnostic.data.take() else { + continue; + }; + + let associated_data: AssociatedDiagnosticData = + serde_json::from_value(data).context("deserializing diagnostic data")?; + + if let Some(ignore_line_edit) = associated_data.ignore_line_edit { + let disable_line_action = CodeAction { + title: format!("Disable {rule_name} for this line"), + kind: Some(CodeActionKind::QUICKFIX), + diagnostics: Some(vec![diagnostic.clone()]), + edit: Some(WorkspaceEdit { + changes: Some({ + let mut changes = HashMap::new(); + changes.insert(uri.clone(), vec![ignore_line_edit]); + changes + }), + ..Default::default() + }), + command: None, + is_preferred: Some(false), + disabled: None, + data: None, + }; + actions.push(CodeActionOrCommand::CodeAction(disable_line_action)); + } + if let Some(ignore_file_edit) = associated_data.ignore_file_edit { + let disable_file_action = CodeAction { + title: format!("Disable {rule_name} for the entire file"), + kind: Some(CodeActionKind::QUICKFIX), + diagnostics: Some(vec![diagnostic.clone()]), + edit: Some(WorkspaceEdit { + changes: Some({ + let mut changes = HashMap::new(); + changes.insert(uri.clone(), vec![ignore_file_edit]); + changes + }), + ..Default::default() + }), + command: None, + is_preferred: Some(false), + disabled: None, + data: None, + }; + actions.push(CodeActionOrCommand::CodeAction(disable_file_action)); + } + + let title = format!("Show documentation for {rule_name}"); + let documentation_action = CodeAction { + title: title.clone(), + kind: Some(CodeActionKind::QUICKFIX), + diagnostics: Some(vec![diagnostic.clone()]), + edit: None, + command: Some(Command { + title, + command: "vscode.open".to_string(), + arguments: Some(vec![serde_json::to_value(format!( + "https://squawkhq.com/docs/{rule_name}" + ))?]), + }), + is_preferred: Some(false), + disabled: None, + data: None, + }; + actions.push(CodeActionOrCommand::CodeAction(documentation_action)); + + if !associated_data.title.is_empty() && !associated_data.edits.is_empty() { + let fix_action = CodeAction { + title: associated_data.title, + kind: Some(CodeActionKind::QUICKFIX), + diagnostics: Some(vec![diagnostic.clone()]), + edit: Some(WorkspaceEdit { + changes: Some({ + let mut changes = HashMap::new(); + changes.insert(uri.clone(), associated_data.edits); + changes + }), + ..Default::default() + }), + command: None, + is_preferred: Some(true), + disabled: None, + data: None, + }; + actions.push(CodeActionOrCommand::CodeAction(fix_action)); + } + } + + let result: CodeActionResponse = actions; + let resp = Response { + id: req.id, + result: Some(serde_json::to_value(&result).unwrap()), + error: None, + }; + + connection.sender.send(Message::Response(resp))?; + Ok(()) +} diff --git a/crates/squawk_server/src/handlers/completion.rs b/crates/squawk_server/src/handlers/completion.rs new file mode 100644 index 00000000..6bd2f2a0 --- /dev/null +++ b/crates/squawk_server/src/handlers/completion.rs @@ -0,0 +1,48 @@ +use anyhow::Result; +use lsp_server::{Connection, Message, Response}; +use lsp_types::{CompletionParams, CompletionResponse}; +use squawk_ide::completion::completion; +use squawk_ide::db::line_index; + +use crate::lsp_utils; +use crate::system::System; + +pub(crate) fn handle_completion( + connection: &Connection, + req: lsp_server::Request, + system: &impl System, +) -> Result<()> { + let params: CompletionParams = serde_json::from_value(req.params)?; + let uri = params.text_document_position.text_document.uri; + let position = params.text_document_position.position; + + let db = system.db(); + let file = system.file(&uri).unwrap(); + let line_index = line_index(db, file); + + let Some(offset) = lsp_utils::offset(&line_index, position) else { + let resp = Response { + id: req.id, + result: Some(serde_json::to_value(CompletionResponse::Array(vec![])).unwrap()), + error: None, + }; + connection.sender.send(Message::Response(resp))?; + return Ok(()); + }; + + let completion_items = completion(db, file, offset) + .into_iter() + .map(lsp_utils::completion_item) + .collect(); + + let result = CompletionResponse::Array(completion_items); + + let resp = Response { + id: req.id, + result: Some(serde_json::to_value(&result).unwrap()), + error: None, + }; + + connection.sender.send(Message::Response(resp))?; + Ok(()) +} diff --git a/crates/squawk_server/src/handlers/document_symbol.rs b/crates/squawk_server/src/handlers/document_symbol.rs new file mode 100644 index 00000000..e628d6f3 --- /dev/null +++ b/crates/squawk_server/src/handlers/document_symbol.rs @@ -0,0 +1,92 @@ +use ::line_index::LineIndex; +use anyhow::Result; +use lsp_server::{Connection, Message, Response}; +use lsp_types::{DocumentSymbol, DocumentSymbolParams, SymbolKind}; +use squawk_ide::db::line_index; +use squawk_ide::document_symbols::{DocumentSymbolKind, document_symbols}; + +use crate::lsp_utils; +use crate::system::System; + +pub(crate) fn handle_document_symbol( + connection: &Connection, + req: lsp_server::Request, + system: &impl System, +) -> Result<()> { + let params: DocumentSymbolParams = serde_json::from_value(req.params)?; + let uri = params.text_document.uri; + + let db = system.db(); + let file = system.file(&uri).unwrap(); + let line_index = line_index(db, file); + + let symbols = document_symbols(db, file); + + fn convert_symbol( + sym: squawk_ide::document_symbols::DocumentSymbol, + line_index: &LineIndex, + ) -> DocumentSymbol { + let range = lsp_utils::range(line_index, sym.full_range); + let selection_range = lsp_utils::range(line_index, sym.focus_range); + + let children = sym + .children + .into_iter() + .map(|child| convert_symbol(child, line_index)) + .collect::>(); + + let children = (!children.is_empty()).then_some(children); + + DocumentSymbol { + name: sym.name, + detail: sym.detail, + kind: match sym.kind { + DocumentSymbolKind::Schema => SymbolKind::NAMESPACE, + DocumentSymbolKind::Table => SymbolKind::STRUCT, + DocumentSymbolKind::View => SymbolKind::STRUCT, + DocumentSymbolKind::MaterializedView => SymbolKind::STRUCT, + DocumentSymbolKind::Function => SymbolKind::FUNCTION, + DocumentSymbolKind::Aggregate => SymbolKind::FUNCTION, + DocumentSymbolKind::Procedure => SymbolKind::FUNCTION, + DocumentSymbolKind::Type => SymbolKind::CLASS, + DocumentSymbolKind::Enum => SymbolKind::ENUM, + DocumentSymbolKind::Index => SymbolKind::KEY, + DocumentSymbolKind::Domain => SymbolKind::CLASS, + DocumentSymbolKind::Sequence => SymbolKind::CONSTANT, + DocumentSymbolKind::Trigger => SymbolKind::EVENT, + DocumentSymbolKind::Tablespace => SymbolKind::NAMESPACE, + DocumentSymbolKind::Database => SymbolKind::MODULE, + DocumentSymbolKind::Server => SymbolKind::OBJECT, + DocumentSymbolKind::Extension => SymbolKind::PACKAGE, + DocumentSymbolKind::Column => SymbolKind::FIELD, + DocumentSymbolKind::Variant => SymbolKind::ENUM_MEMBER, + DocumentSymbolKind::Cursor => SymbolKind::VARIABLE, + DocumentSymbolKind::PreparedStatement => SymbolKind::VARIABLE, + DocumentSymbolKind::Channel => SymbolKind::EVENT, + DocumentSymbolKind::EventTrigger => SymbolKind::EVENT, + DocumentSymbolKind::Role => SymbolKind::CLASS, + DocumentSymbolKind::Policy => SymbolKind::VARIABLE, + }, + tags: None, + range, + selection_range, + children, + #[allow(deprecated)] + deprecated: None, + } + } + + let lsp_symbols: Vec = symbols + .into_iter() + .map(|sym| convert_symbol(sym, &line_index)) + .collect(); + + let resp = Response { + id: req.id, + result: Some(serde_json::to_value(&lsp_symbols).unwrap()), + error: None, + }; + + connection.sender.send(Message::Response(resp))?; + Ok(()) +} diff --git a/crates/squawk_server/src/handlers/folding_range.rs b/crates/squawk_server/src/handlers/folding_range.rs new file mode 100644 index 00000000..042377a2 --- /dev/null +++ b/crates/squawk_server/src/handlers/folding_range.rs @@ -0,0 +1,35 @@ +use anyhow::Result; +use lsp_server::{Connection, Message, Response}; +use lsp_types::FoldingRange; +use squawk_ide::db::line_index; +use squawk_ide::folding_ranges::folding_ranges; + +use crate::lsp_utils; +use crate::system::System; + +pub(crate) fn handle_folding_range( + connection: &Connection, + req: lsp_server::Request, + system: &impl System, +) -> Result<()> { + let params: lsp_types::FoldingRangeParams = serde_json::from_value(req.params)?; + let uri = params.text_document.uri; + + let db = system.db(); + let file = system.file(&uri).unwrap(); + let line_idx = line_index(db, file); + + let lsp_folds: Vec = folding_ranges(db, file) + .into_iter() + .map(|fold| lsp_utils::folding_range(&line_idx, fold)) + .collect(); + + let resp = Response { + id: req.id, + result: Some(serde_json::to_value(&lsp_folds).unwrap()), + error: None, + }; + + connection.sender.send(Message::Response(resp))?; + Ok(()) +} diff --git a/crates/squawk_server/src/handlers/goto_definition.rs b/crates/squawk_server/src/handlers/goto_definition.rs new file mode 100644 index 00000000..06b5df3c --- /dev/null +++ b/crates/squawk_server/src/handlers/goto_definition.rs @@ -0,0 +1,44 @@ +use anyhow::Result; +use lsp_server::{Connection, Message, Response}; +use lsp_types::{GotoDefinitionParams, GotoDefinitionResponse}; +use squawk_ide::db::line_index; +use squawk_ide::goto_definition::goto_definition; + +use crate::lsp_utils::{self, to_location}; +use crate::system::System; + +pub(crate) fn handle_goto_definition( + connection: &Connection, + req: lsp_server::Request, + system: &impl System, +) -> Result<()> { + let params: GotoDefinitionParams = serde_json::from_value(req.params)?; + let uri = params.text_document_position_params.text_document.uri; + let position = params.text_document_position_params.position; + + let db = system.db(); + let file = system.file(&uri).unwrap(); + let line_index = line_index(db, file); + let offset = lsp_utils::offset(&line_index, position).unwrap(); + + let ranges = goto_definition(db, file, offset) + .into_iter() + .filter_map(|location| { + debug_assert!( + !location.range.contains(offset), + "Our target destination range must not include the source range otherwise go to def won't work in vscode." + ); + to_location(db, system, &uri, location) + }) + .collect(); + + let result = GotoDefinitionResponse::Array(ranges); + let resp = Response { + id: req.id, + result: Some(serde_json::to_value(&result).unwrap()), + error: None, + }; + + connection.sender.send(Message::Response(resp))?; + Ok(()) +} diff --git a/crates/squawk_server/src/handlers/hover.rs b/crates/squawk_server/src/handlers/hover.rs new file mode 100644 index 00000000..5e9ce165 --- /dev/null +++ b/crates/squawk_server/src/handlers/hover.rs @@ -0,0 +1,42 @@ +use anyhow::Result; +use lsp_server::{Connection, Message, Response}; +use lsp_types::{Hover, HoverContents, HoverParams, LanguageString, MarkedString}; +use squawk_ide::db::line_index; +use squawk_ide::hover::hover; + +use crate::lsp_utils; +use crate::system::System; + +pub(crate) fn handle_hover( + connection: &Connection, + req: lsp_server::Request, + system: &impl System, +) -> Result<()> { + let params: HoverParams = serde_json::from_value(req.params)?; + let uri = params.text_document_position_params.text_document.uri; + let position = params.text_document_position_params.position; + + let db = system.db(); + let file = system.file(&uri).unwrap(); + let line_index = line_index(db, file); + let offset = lsp_utils::offset(&line_index, position).unwrap(); + + let type_info = hover(db, file, offset); + + let result = type_info.map(|type_str| Hover { + contents: HoverContents::Scalar(MarkedString::LanguageString(LanguageString { + language: "sql".to_string(), + value: type_str, + })), + range: None, + }); + + let resp = Response { + id: req.id, + result: Some(serde_json::to_value(&result).unwrap()), + error: None, + }; + + connection.sender.send(Message::Response(resp))?; + Ok(()) +} diff --git a/crates/squawk_server/src/handlers/inlay_hints.rs b/crates/squawk_server/src/handlers/inlay_hints.rs new file mode 100644 index 00000000..becf9d86 --- /dev/null +++ b/crates/squawk_server/src/handlers/inlay_hints.rs @@ -0,0 +1,83 @@ +use anyhow::Result; +use lsp_server::{Connection, Message, Response}; +use lsp_types::{ + InlayHint, InlayHintKind, InlayHintLabel, InlayHintLabelPart, InlayHintParams, Location, +}; +use squawk_ide::builtins::{builtins_line_index, builtins_url}; +use squawk_ide::db::line_index; +use squawk_ide::inlay_hints::inlay_hints; + +use crate::lsp_utils; +use crate::system::System; + +pub(crate) fn handle_inlay_hints( + connection: &Connection, + req: lsp_server::Request, + system: &impl System, +) -> Result<()> { + let params: InlayHintParams = serde_json::from_value(req.params)?; + let uri = params.text_document.uri; + + let db = system.db(); + let file = system.file(&uri).unwrap(); + let line_index = line_index(db, file); + + let hints = inlay_hints(db, file); + + let lsp_hints: Vec = hints + .into_iter() + .flat_map(|hint| { + let line_col = line_index.line_col(hint.position); + let position = lsp_types::Position::new(line_col.line, line_col.col); + + let uri = match hint.file { + Some(squawk_ide::goto_definition::FileId::Current) | None => uri.clone(), + Some(squawk_ide::goto_definition::FileId::Builtins) => builtins_url(db)?, + }; + + let line_index = match hint.file { + Some(squawk_ide::goto_definition::FileId::Current) | None => &line_index, + Some(squawk_ide::goto_definition::FileId::Builtins) => &builtins_line_index(db), + }; + + let kind: InlayHintKind = match hint.kind { + squawk_ide::inlay_hints::InlayHintKind::Type => InlayHintKind::TYPE, + squawk_ide::inlay_hints::InlayHintKind::Parameter => InlayHintKind::PARAMETER, + }; + + let label = if let Some(target_range) = hint.target { + InlayHintLabel::LabelParts(vec![InlayHintLabelPart { + value: hint.label, + location: Some(Location { + uri: uri.clone(), + range: lsp_utils::range(line_index, target_range), + }), + tooltip: None, + command: None, + }]) + } else { + InlayHintLabel::String(hint.label) + }; + + Some(InlayHint { + position, + label, + kind: Some(kind), + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }) + }) + .collect(); + + let resp = Response { + id: req.id, + result: Some(serde_json::to_value(&lsp_hints).unwrap()), + error: None, + }; + + connection.sender.send(Message::Response(resp))?; + Ok(()) +} diff --git a/crates/squawk_server/src/handlers/notifications.rs b/crates/squawk_server/src/handlers/notifications.rs new file mode 100644 index 00000000..b262c6d5 --- /dev/null +++ b/crates/squawk_server/src/handlers/notifications.rs @@ -0,0 +1,112 @@ +use anyhow::Result; +use lsp_server::{Connection, Message, Notification}; +use lsp_types::{ + Diagnostic, DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, + PublishDiagnosticsParams, Url, + notification::{Notification as _, PublishDiagnostics}, +}; + +use crate::lsp_utils; +use crate::system::{Document, System}; + +fn publish_diagnostics( + connection: &Connection, + uri: Url, + version: i32, + diagnostics: Vec, +) -> Result<()> { + let publish_params = PublishDiagnosticsParams { + uri, + diagnostics, + version: Some(version), + }; + + let notification = Notification { + method: PublishDiagnostics::METHOD.to_owned(), + params: serde_json::to_value(publish_params)?, + }; + + connection + .sender + .send(Message::Notification(notification))?; + Ok(()) +} + +pub(crate) fn handle_did_open( + connection: &Connection, + notif: lsp_server::Notification, + system: &mut impl System, +) -> Result<()> { + let params: DidOpenTextDocumentParams = serde_json::from_value(notif.params)?; + let uri = params.text_document.uri; + let content = params.text_document.text; + let version = params.text_document.version; + + system.set(uri.clone(), Document { content, version }); + let db = system.db(); + let file = system.file(&uri).unwrap(); + let diagnostics = crate::lint::lint(db, file); + + // TODO: we need a better setup for "run func when input changed" + publish_diagnostics(connection, uri, version, diagnostics)?; + + Ok(()) +} + +pub(crate) fn handle_did_change( + connection: &Connection, + notif: lsp_server::Notification, + system: &mut impl System, +) -> Result<()> { + let params: DidChangeTextDocumentParams = serde_json::from_value(notif.params)?; + let uri = params.text_document.uri; + let version = params.text_document.version; + + let db = system.db(); + let file = system.file(&uri).unwrap(); + let content = file.content(db); + + let updated_content = lsp_utils::apply_incremental_changes(content, params.content_changes); + + system.set( + uri.clone(), + Document { + content: updated_content, + version, + }, + ); + let db = system.db(); + let file = system.file(&uri).unwrap(); + let diagnostics = crate::lint::lint(db, file); + publish_diagnostics(connection, uri, version, diagnostics)?; + + Ok(()) +} + +pub(crate) fn handle_did_close( + connection: &Connection, + notif: lsp_server::Notification, + system: &mut impl System, +) -> Result<()> { + let params: DidCloseTextDocumentParams = serde_json::from_value(notif.params)?; + let uri = params.text_document.uri; + + system.remove(&uri); + + let publish_params = PublishDiagnosticsParams { + uri, + diagnostics: vec![], + version: None, + }; + + let notification = Notification { + method: PublishDiagnostics::METHOD.to_owned(), + params: serde_json::to_value(publish_params)?, + }; + + connection + .sender + .send(Message::Notification(notification))?; + + Ok(()) +} diff --git a/crates/squawk_server/src/handlers/references.rs b/crates/squawk_server/src/handlers/references.rs new file mode 100644 index 00000000..fae7f24e --- /dev/null +++ b/crates/squawk_server/src/handlers/references.rs @@ -0,0 +1,41 @@ +use anyhow::Result; +use lsp_server::{Connection, Message, Response}; +use lsp_types::{Location, ReferenceParams}; +use squawk_ide::db::line_index; +use squawk_ide::find_references::find_references; + +use crate::lsp_utils::{self, to_location}; +use crate::system::System; + +pub(crate) fn handle_references( + connection: &Connection, + req: lsp_server::Request, + system: &impl System, +) -> Result<()> { + let params: ReferenceParams = serde_json::from_value(req.params)?; + let uri = params.text_document_position.text_document.uri; + let position = params.text_document_position.position; + + let db = system.db(); + let file = system.file(&uri).unwrap(); + let line_index = line_index(db, file); + let offset = lsp_utils::offset(&line_index, position).unwrap(); + + let refs = find_references(db, file, offset); + let include_declaration = params.context.include_declaration; + + let locations: Vec = refs + .into_iter() + .filter(|loc| include_declaration || !loc.range.contains(offset)) + .filter_map(|loc| to_location(db, system, &uri, loc)) + .collect(); + + let resp = Response { + id: req.id, + result: Some(serde_json::to_value(&locations).unwrap()), + error: None, + }; + + connection.sender.send(Message::Response(resp))?; + Ok(()) +} diff --git a/crates/squawk_server/src/handlers/selection_range.rs b/crates/squawk_server/src/handlers/selection_range.rs new file mode 100644 index 00000000..d5a100bd --- /dev/null +++ b/crates/squawk_server/src/handlers/selection_range.rs @@ -0,0 +1,66 @@ +use anyhow::Result; +use lsp_server::{Connection, Message, Response}; +use lsp_types::SelectionRangeParams; +use rowan::TextRange; +use squawk_ide::db::{line_index, parse}; + +use crate::lsp_utils; +use crate::system::System; + +pub(crate) fn handle_selection_range( + connection: &Connection, + req: lsp_server::Request, + system: &impl System, +) -> Result<()> { + let params: SelectionRangeParams = serde_json::from_value(req.params)?; + let uri = params.text_document.uri; + + let db = system.db(); + let file = system.file(&uri).unwrap(); + let parse = parse(db, file); + let root = parse.syntax_node(); + let line_index = line_index(db, file); + + let mut selection_ranges = vec![]; + + for position in params.positions { + let Some(offset) = lsp_utils::offset(&line_index, position) else { + continue; + }; + + let mut ranges = vec![]; + { + let mut range = TextRange::new(offset, offset); + loop { + ranges.push(range); + let next = squawk_ide::expand_selection::extend_selection(&root, range); + if next == range { + break; + } else { + range = next + } + } + } + + let mut range = lsp_types::SelectionRange { + range: lsp_utils::range(&line_index, *ranges.last().unwrap()), + parent: None, + }; + for &r in ranges.iter().rev().skip(1) { + range = lsp_types::SelectionRange { + range: lsp_utils::range(&line_index, r), + parent: Some(Box::new(range)), + } + } + selection_ranges.push(range); + } + + let resp = Response { + id: req.id, + result: Some(serde_json::to_value(&selection_ranges).unwrap()), + error: None, + }; + + connection.sender.send(Message::Response(resp))?; + Ok(()) +} diff --git a/crates/squawk_server/src/handlers/syntax_tree.rs b/crates/squawk_server/src/handlers/syntax_tree.rs new file mode 100644 index 00000000..45228848 --- /dev/null +++ b/crates/squawk_server/src/handlers/syntax_tree.rs @@ -0,0 +1,37 @@ +use anyhow::Result; +use log::info; +use lsp_server::{Connection, Message, Response}; +use squawk_ide::db::parse; + +use crate::system::System; + +#[derive(serde::Deserialize)] +pub(crate) struct SyntaxTreeParams { + #[serde(rename = "textDocument")] + text_document: lsp_types::TextDocumentIdentifier, +} + +pub(crate) fn handle_syntax_tree( + connection: &Connection, + req: lsp_server::Request, + system: &impl System, +) -> Result<()> { + let params: SyntaxTreeParams = serde_json::from_value(req.params)?; + let uri = params.text_document.uri; + + info!("Generating syntax tree for: {uri}"); + + let db = system.db(); + let file = system.file(&uri).unwrap(); + let parse = parse(db, file); + let syntax_tree = format!("{:#?}", parse.syntax_node()); + + let resp = Response { + id: req.id, + result: Some(serde_json::to_value(&syntax_tree).unwrap()), + error: None, + }; + + connection.sender.send(Message::Response(resp))?; + Ok(()) +} diff --git a/crates/squawk_server/src/handlers/tokens.rs b/crates/squawk_server/src/handlers/tokens.rs new file mode 100644 index 00000000..7460035f --- /dev/null +++ b/crates/squawk_server/src/handlers/tokens.rs @@ -0,0 +1,53 @@ +use anyhow::Result; +use log::info; +use lsp_server::{Connection, Message, Response}; + +use crate::system::System; + +#[derive(serde::Deserialize)] +pub(crate) struct TokensParams { + #[serde(rename = "textDocument")] + text_document: lsp_types::TextDocumentIdentifier, +} + +pub(crate) fn handle_tokens( + connection: &Connection, + req: lsp_server::Request, + system: &impl System, +) -> Result<()> { + let params: TokensParams = serde_json::from_value(req.params)?; + let uri = params.text_document.uri; + + info!("Generating tokens for: {uri}"); + + let db = system.db(); + let file = system.file(&uri).unwrap(); + let content = file.content(db); + + // TODO: move this to a tracked function + let tokens = squawk_lexer::tokenize(content); + + let mut output = vec![]; + let mut char_pos = 0; + for token in tokens { + let token_start = char_pos; + let token_end = token_start + token.len as usize; + let token_text = &content[token_start..token_end]; + output.push(format!( + "{:?}@{}..{} {:?}", + token.kind, token_start, token_end, token_text + )); + char_pos = token_end; + } + + let tokens_output = output.join("\n"); + + let resp = Response { + id: req.id, + result: Some(serde_json::to_value(&tokens_output).unwrap()), + error: None, + }; + + connection.sender.send(Message::Response(resp))?; + Ok(()) +} diff --git a/crates/squawk_server/src/lib.rs b/crates/squawk_server/src/lib.rs index 8fa1eec5..195c28e7 100644 --- a/crates/squawk_server/src/lib.rs +++ b/crates/squawk_server/src/lib.rs @@ -1,948 +1,9 @@ -use ::line_index::LineIndex; -use anyhow::{Context, Result}; -use log::info; -use lsp_server::{Connection, Message, Notification, Response}; -use lsp_types::{ - CodeAction, CodeActionKind, CodeActionOptions, CodeActionOrCommand, CodeActionParams, - CodeActionProviderCapability, CodeActionResponse, Command, CompletionOptions, CompletionParams, - CompletionResponse, Diagnostic, DidChangeTextDocumentParams, DidCloseTextDocumentParams, - DidOpenTextDocumentParams, DocumentSymbol, DocumentSymbolParams, FoldingRange, - FoldingRangeProviderCapability, GotoDefinitionParams, GotoDefinitionResponse, Hover, - HoverContents, HoverParams, HoverProviderCapability, InitializeParams, InlayHint, - InlayHintKind, InlayHintLabel, InlayHintLabelPart, InlayHintParams, LanguageString, Location, - MarkedString, OneOf, PublishDiagnosticsParams, ReferenceParams, SelectionRangeParams, - SelectionRangeProviderCapability, ServerCapabilities, SymbolKind, TextDocumentSyncCapability, - TextDocumentSyncKind, Url, WorkDoneProgressOptions, WorkspaceEdit, - notification::{ - DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, Notification as _, - PublishDiagnostics, - }, - request::{ - CodeActionRequest, Completion, DocumentSymbolRequest, FoldingRangeRequest, GotoDefinition, - HoverRequest, InlayHintRequest, References, Request, SelectionRangeRequest, - }, -}; -use rowan::TextRange; -use salsa::Setter; -use squawk_ide::builtins::{builtins_line_index, builtins_url}; -use squawk_ide::code_actions::code_actions; -use squawk_ide::completion::completion; -use squawk_ide::db::{Database, File, line_index, parse}; -use squawk_ide::document_symbols::{DocumentSymbolKind, document_symbols}; -use squawk_ide::find_references::find_references; -use squawk_ide::folding_ranges::folding_ranges; -use squawk_ide::goto_definition::goto_definition; -use squawk_ide::hover::hover; -use squawk_ide::inlay_hints::inlay_hints; -use std::collections::HashMap; - -use diagnostic::DIAGNOSTIC_NAME; - -use crate::diagnostic::AssociatedDiagnosticData; mod diagnostic; +mod handlers; mod ignore; mod lint; mod lsp_utils; +mod server; +mod system; -struct DocumentState { - content: String, - #[allow(dead_code)] - version: i32, -} - -trait FileSystem { - fn db(&self) -> &Database; - fn file(&self, uri: &Url) -> Option; - fn set(&mut self, uri: Url, state: DocumentState); - fn remove(&mut self, uri: &Url); -} - -struct FileDatabase { - pub db: Database, - files: HashMap, -} - -impl FileDatabase { - fn new() -> Self { - Self { - db: Database::default(), - files: HashMap::new(), - } - } -} - -impl FileSystem for FileDatabase { - fn db(&self) -> &Database { - return &self.db; - } - - fn file(&self, uri: &Url) -> Option { - self.files.get(uri).copied() - } - - fn set(&mut self, uri: Url, state: DocumentState) { - if let Some(file) = self.files.get(&uri).copied() { - file.set_content(&mut self.db).to(state.content); - file.set_version(&mut self.db).to(state.version); - } else { - let file = File::new(&self.db, state.content, state.version); - self.files.insert(uri, file); - } - } - - fn remove(&mut self, uri: &Url) { - self.files.remove(uri); - } -} - -pub fn run() -> Result<()> { - info!("Starting Squawk LSP server"); - - let (connection, io_threads) = Connection::stdio(); - - let server_capabilities = serde_json::to_value(&ServerCapabilities { - text_document_sync: Some(TextDocumentSyncCapability::Kind( - TextDocumentSyncKind::INCREMENTAL, - )), - code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions { - code_action_kinds: Some(vec![ - CodeActionKind::QUICKFIX, - CodeActionKind::REFACTOR_REWRITE, - ]), - work_done_progress_options: WorkDoneProgressOptions { - work_done_progress: None, - }, - resolve_provider: None, - })), - selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)), - references_provider: Some(OneOf::Left(true)), - definition_provider: Some(OneOf::Left(true)), - hover_provider: Some(HoverProviderCapability::Simple(true)), - inlay_hint_provider: Some(OneOf::Left(true)), - document_symbol_provider: Some(OneOf::Left(true)), - folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)), - completion_provider: Some(CompletionOptions { - resolve_provider: Some(false), - trigger_characters: Some(vec![".".to_owned()]), - all_commit_characters: None, - work_done_progress_options: WorkDoneProgressOptions { - work_done_progress: None, - }, - completion_item: None, - }), - ..Default::default() - }) - .unwrap(); - - info!("LSP server initializing connection..."); - let initialization_params = connection.initialize(server_capabilities)?; - info!("LSP server initialized, entering main loop"); - - main_loop(connection, initialization_params)?; - - info!("LSP server shutting down"); - - io_threads.join()?; - Ok(()) -} - -fn main_loop(connection: Connection, params: serde_json::Value) -> Result<()> { - info!("Server main loop"); - - let init_params: InitializeParams = serde_json::from_value(params).unwrap_or_default(); - info!("Client process ID: {:?}", init_params.process_id); - let client_name = init_params.client_info.map(|x| x.name); - info!("Client name: {client_name:?}"); - - let mut file_system = FileDatabase::new(); - - for msg in &connection.receiver { - match msg { - Message::Request(req) => { - info!("Received request: method={}, id={:?}", req.method, req.id); - - if connection.handle_shutdown(&req)? { - info!("Received shutdown request, exiting"); - return Ok(()); - } - - match req.method.as_ref() { - GotoDefinition::METHOD => { - handle_goto_definition(&connection, req, &file_system)?; - } - HoverRequest::METHOD => { - handle_hover(&connection, req, &file_system)?; - } - CodeActionRequest::METHOD => { - handle_code_action(&connection, req, &file_system)?; - } - SelectionRangeRequest::METHOD => { - handle_selection_range(&connection, req, &file_system)?; - } - InlayHintRequest::METHOD => { - handle_inlay_hints(&connection, req, &file_system)?; - } - DocumentSymbolRequest::METHOD => { - handle_document_symbol(&connection, req, &file_system)?; - } - FoldingRangeRequest::METHOD => { - handle_folding_range(&connection, req, &file_system)?; - } - Completion::METHOD => { - handle_completion(&connection, req, &file_system)?; - } - "squawk/syntaxTree" => { - handle_syntax_tree(&connection, req, &file_system)?; - } - "squawk/tokens" => { - handle_tokens(&connection, req, &file_system)?; - } - References::METHOD => { - handle_references(&connection, req, &file_system)?; - } - _ => { - info!("Ignoring unhandled request: {}", req.method); - } - } - } - Message::Response(resp) => { - info!("Received response: id={:?}", resp.id); - } - Message::Notification(notif) => { - info!("Received notification: method={}", notif.method); - match notif.method.as_ref() { - DidOpenTextDocument::METHOD => { - handle_did_open(&connection, notif, &mut file_system)?; - } - DidChangeTextDocument::METHOD => { - handle_did_change(&connection, notif, &mut file_system)?; - } - DidCloseTextDocument::METHOD => { - handle_did_close(&connection, notif, &mut file_system)?; - } - _ => { - info!("Ignoring unhandled notification: {}", notif.method); - } - } - } - } - } - Ok(()) -} - -fn handle_goto_definition( - connection: &Connection, - req: lsp_server::Request, - file_system: &impl FileSystem, -) -> Result<()> { - let params: GotoDefinitionParams = serde_json::from_value(req.params)?; - let uri = params.text_document_position_params.text_document.uri; - let position = params.text_document_position_params.position; - - let db = file_system.db(); - let file = file_system.file(&uri).unwrap(); - let line_index = line_index(db, file); - let offset = lsp_utils::offset(&line_index, position).unwrap(); - - let ranges = goto_definition(db, file, offset) - .into_iter() - .filter_map(|location| { - debug_assert!( - !location.range.contains(offset), - "Our target destination range must not include the source range otherwise go to def won't work in vscode." - ); - to_location(db, file_system, &uri, location) - }) - .collect(); - - let result = GotoDefinitionResponse::Array(ranges); - let resp = Response { - id: req.id, - result: Some(serde_json::to_value(&result).unwrap()), - error: None, - }; - - connection.sender.send(Message::Response(resp))?; - Ok(()) -} - -fn handle_hover( - connection: &Connection, - req: lsp_server::Request, - file_system: &impl FileSystem, -) -> Result<()> { - let params: HoverParams = serde_json::from_value(req.params)?; - let uri = params.text_document_position_params.text_document.uri; - let position = params.text_document_position_params.position; - - let db = file_system.db(); - let file = file_system.file(&uri).unwrap(); - let line_index = line_index(db, file); - let offset = lsp_utils::offset(&line_index, position).unwrap(); - - let type_info = hover(db, file, offset); - - let result = type_info.map(|type_str| Hover { - contents: HoverContents::Scalar(MarkedString::LanguageString(LanguageString { - language: "sql".to_string(), - value: type_str, - })), - range: None, - }); - - let resp = Response { - id: req.id, - result: Some(serde_json::to_value(&result).unwrap()), - error: None, - }; - - connection.sender.send(Message::Response(resp))?; - Ok(()) -} - -fn handle_inlay_hints( - connection: &Connection, - req: lsp_server::Request, - file_system: &impl FileSystem, -) -> Result<()> { - let params: InlayHintParams = serde_json::from_value(req.params)?; - let uri = params.text_document.uri; - - let db = file_system.db(); - let file = file_system.file(&uri).unwrap(); - let line_index = line_index(db, file); - - let hints = inlay_hints(db, file); - - let lsp_hints: Vec = hints - .into_iter() - .flat_map(|hint| { - let line_col = line_index.line_col(hint.position); - let position = lsp_types::Position::new(line_col.line, line_col.col); - - let uri = match hint.file { - Some(squawk_ide::goto_definition::FileId::Current) | None => uri.clone(), - Some(squawk_ide::goto_definition::FileId::Builtins) => builtins_url(db)?, - }; - - let line_index = match hint.file { - Some(squawk_ide::goto_definition::FileId::Current) | None => &line_index, - Some(squawk_ide::goto_definition::FileId::Builtins) => &builtins_line_index(db), - }; - - let kind: InlayHintKind = match hint.kind { - squawk_ide::inlay_hints::InlayHintKind::Type => InlayHintKind::TYPE, - squawk_ide::inlay_hints::InlayHintKind::Parameter => InlayHintKind::PARAMETER, - }; - - let label = if let Some(target_range) = hint.target { - InlayHintLabel::LabelParts(vec![InlayHintLabelPart { - value: hint.label, - location: Some(Location { - uri: uri.clone(), - range: lsp_utils::range(line_index, target_range), - }), - tooltip: None, - command: None, - }]) - } else { - InlayHintLabel::String(hint.label) - }; - - Some(InlayHint { - position, - label, - kind: Some(kind), - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }) - }) - .collect(); - - let resp = Response { - id: req.id, - result: Some(serde_json::to_value(&lsp_hints).unwrap()), - error: None, - }; - - connection.sender.send(Message::Response(resp))?; - Ok(()) -} - -fn handle_document_symbol( - connection: &Connection, - req: lsp_server::Request, - file_system: &impl FileSystem, -) -> Result<()> { - let params: DocumentSymbolParams = serde_json::from_value(req.params)?; - let uri = params.text_document.uri; - - let db = file_system.db(); - let file = file_system.file(&uri).unwrap(); - let line_index = line_index(db, file); - - let symbols = document_symbols(db, file); - - fn convert_symbol( - sym: squawk_ide::document_symbols::DocumentSymbol, - line_index: &LineIndex, - ) -> DocumentSymbol { - let range = lsp_utils::range(line_index, sym.full_range); - let selection_range = lsp_utils::range(line_index, sym.focus_range); - - let children = sym - .children - .into_iter() - .map(|child| convert_symbol(child, line_index)) - .collect::>(); - - let children = (!children.is_empty()).then_some(children); - - DocumentSymbol { - name: sym.name, - detail: sym.detail, - kind: match sym.kind { - DocumentSymbolKind::Schema => SymbolKind::NAMESPACE, - DocumentSymbolKind::Table => SymbolKind::STRUCT, - DocumentSymbolKind::View => SymbolKind::STRUCT, - DocumentSymbolKind::MaterializedView => SymbolKind::STRUCT, - DocumentSymbolKind::Function => SymbolKind::FUNCTION, - DocumentSymbolKind::Aggregate => SymbolKind::FUNCTION, - DocumentSymbolKind::Procedure => SymbolKind::FUNCTION, - DocumentSymbolKind::Type => SymbolKind::CLASS, - DocumentSymbolKind::Enum => SymbolKind::ENUM, - DocumentSymbolKind::Index => SymbolKind::KEY, - DocumentSymbolKind::Domain => SymbolKind::CLASS, - DocumentSymbolKind::Sequence => SymbolKind::CONSTANT, - DocumentSymbolKind::Trigger => SymbolKind::EVENT, - DocumentSymbolKind::Tablespace => SymbolKind::NAMESPACE, - DocumentSymbolKind::Database => SymbolKind::MODULE, - DocumentSymbolKind::Server => SymbolKind::OBJECT, - DocumentSymbolKind::Extension => SymbolKind::PACKAGE, - DocumentSymbolKind::Column => SymbolKind::FIELD, - DocumentSymbolKind::Variant => SymbolKind::ENUM_MEMBER, - DocumentSymbolKind::Cursor => SymbolKind::VARIABLE, - DocumentSymbolKind::PreparedStatement => SymbolKind::VARIABLE, - DocumentSymbolKind::Channel => SymbolKind::EVENT, - DocumentSymbolKind::EventTrigger => SymbolKind::EVENT, - DocumentSymbolKind::Role => SymbolKind::CLASS, - DocumentSymbolKind::Policy => SymbolKind::VARIABLE, - }, - tags: None, - range, - selection_range, - children, - #[allow(deprecated)] - deprecated: None, - } - } - - let lsp_symbols: Vec = symbols - .into_iter() - .map(|sym| convert_symbol(sym, &line_index)) - .collect(); - - let resp = Response { - id: req.id, - result: Some(serde_json::to_value(&lsp_symbols).unwrap()), - error: None, - }; - - connection.sender.send(Message::Response(resp))?; - Ok(()) -} - -fn handle_folding_range( - connection: &Connection, - req: lsp_server::Request, - file_system: &impl FileSystem, -) -> Result<()> { - let params: lsp_types::FoldingRangeParams = serde_json::from_value(req.params)?; - let uri = params.text_document.uri; - - let db = file_system.db(); - let file = file_system.file(&uri).unwrap(); - let line_idx = line_index(db, file); - - let lsp_folds: Vec = folding_ranges(db, file) - .into_iter() - .map(|fold| lsp_utils::folding_range(&line_idx, fold)) - .collect(); - - let resp = Response { - id: req.id, - result: Some(serde_json::to_value(&lsp_folds).unwrap()), - error: None, - }; - - connection.sender.send(Message::Response(resp))?; - Ok(()) -} - -fn handle_selection_range( - connection: &Connection, - req: lsp_server::Request, - file_system: &impl FileSystem, -) -> Result<()> { - let params: SelectionRangeParams = serde_json::from_value(req.params)?; - let uri = params.text_document.uri; - - let db = file_system.db(); - let file = file_system.file(&uri).unwrap(); - let parse = parse(db, file); - let root = parse.syntax_node(); - let line_index = line_index(db, file); - - let mut selection_ranges = vec![]; - - for position in params.positions { - let Some(offset) = lsp_utils::offset(&line_index, position) else { - continue; - }; - - let mut ranges = Vec::new(); - { - let mut range = TextRange::new(offset, offset); - loop { - ranges.push(range); - let next = squawk_ide::expand_selection::extend_selection(&root, range); - if next == range { - break; - } else { - range = next - } - } - } - - let mut range = lsp_types::SelectionRange { - range: lsp_utils::range(&line_index, *ranges.last().unwrap()), - parent: None, - }; - for &r in ranges.iter().rev().skip(1) { - range = lsp_types::SelectionRange { - range: lsp_utils::range(&line_index, r), - parent: Some(Box::new(range)), - } - } - selection_ranges.push(range); - } - - let resp = Response { - id: req.id, - result: Some(serde_json::to_value(&selection_ranges).unwrap()), - error: None, - }; - - connection.sender.send(Message::Response(resp))?; - Ok(()) -} - -fn to_location( - db: &dyn salsa::Database, - file_system: &impl FileSystem, - uri: &Url, - loc: squawk_ide::goto_definition::Location, -) -> Option { - let file = file_system.file(uri).unwrap(); - let uri = match loc.file { - squawk_ide::goto_definition::FileId::Current => uri.clone(), - squawk_ide::goto_definition::FileId::Builtins => builtins_url(db)?, - }; - let line_index = match loc.file { - squawk_ide::goto_definition::FileId::Current => &line_index(db, file), - squawk_ide::goto_definition::FileId::Builtins => &builtins_line_index(db), - }; - let range = lsp_utils::range(line_index, loc.range); - Some(Location { uri, range }) -} - -fn handle_references( - connection: &Connection, - req: lsp_server::Request, - file_system: &impl FileSystem, -) -> Result<()> { - let params: ReferenceParams = serde_json::from_value(req.params)?; - let uri = params.text_document_position.text_document.uri; - let position = params.text_document_position.position; - - let db = file_system.db(); - let file = file_system.file(&uri).unwrap(); - let line_index = line_index(db, file); - let offset = lsp_utils::offset(&line_index, position).unwrap(); - - let refs = find_references(db, file, offset); - let include_declaration = params.context.include_declaration; - - let locations: Vec = refs - .into_iter() - .filter(|loc| include_declaration || !loc.range.contains(offset)) - .filter_map(|loc| to_location(db, file_system, &uri, loc)) - .collect(); - - let resp = Response { - id: req.id, - result: Some(serde_json::to_value(&locations).unwrap()), - error: None, - }; - - connection.sender.send(Message::Response(resp))?; - Ok(()) -} - -fn handle_completion( - connection: &Connection, - req: lsp_server::Request, - file_system: &impl FileSystem, -) -> Result<()> { - let params: CompletionParams = serde_json::from_value(req.params)?; - let uri = params.text_document_position.text_document.uri; - let position = params.text_document_position.position; - - let db = file_system.db(); - let file = file_system.file(&uri).unwrap(); - let line_index = line_index(db, file); - - let Some(offset) = lsp_utils::offset(&line_index, position) else { - let resp = Response { - id: req.id, - result: Some(serde_json::to_value(CompletionResponse::Array(vec![])).unwrap()), - error: None, - }; - connection.sender.send(Message::Response(resp))?; - return Ok(()); - }; - - let completion_items = completion(db, file, offset) - .into_iter() - .map(lsp_utils::completion_item) - .collect(); - - let result = CompletionResponse::Array(completion_items); - - let resp = Response { - id: req.id, - result: Some(serde_json::to_value(&result).unwrap()), - error: None, - }; - - connection.sender.send(Message::Response(resp))?; - Ok(()) -} - -fn handle_code_action( - connection: &Connection, - req: lsp_server::Request, - file_system: &impl FileSystem, -) -> Result<()> { - let params: CodeActionParams = serde_json::from_value(req.params)?; - let uri = params.text_document.uri; - - let mut actions: CodeActionResponse = Vec::new(); - - let db = file_system.db(); - let file = file_system.file(&uri).unwrap(); - let line_index = line_index(db, file); - let offset = lsp_utils::offset(&line_index, params.range.start).unwrap(); - - let ide_actions = code_actions(db, file, offset).unwrap_or_default(); - - for action in ide_actions { - let lsp_action = lsp_utils::code_action(&line_index, uri.clone(), action); - actions.push(CodeActionOrCommand::CodeAction(lsp_action)); - } - - for mut diagnostic in params - .context - .diagnostics - .into_iter() - .filter(|diagnostic| diagnostic.source.as_deref() == Some(DIAGNOSTIC_NAME)) - { - let Some(rule_name) = diagnostic.code.as_ref().map(|x| match x { - lsp_types::NumberOrString::String(s) => s.clone(), - lsp_types::NumberOrString::Number(n) => n.to_string(), - }) else { - continue; - }; - let Some(data) = diagnostic.data.take() else { - continue; - }; - - let associated_data: AssociatedDiagnosticData = - serde_json::from_value(data).context("deserializing diagnostic data")?; - - if let Some(ignore_line_edit) = associated_data.ignore_line_edit { - let disable_line_action = CodeAction { - title: format!("Disable {rule_name} for this line"), - kind: Some(CodeActionKind::QUICKFIX), - diagnostics: Some(vec![diagnostic.clone()]), - edit: Some(WorkspaceEdit { - changes: Some({ - let mut changes = HashMap::new(); - changes.insert(uri.clone(), vec![ignore_line_edit]); - changes - }), - ..Default::default() - }), - command: None, - is_preferred: Some(false), - disabled: None, - data: None, - }; - actions.push(CodeActionOrCommand::CodeAction(disable_line_action)); - } - if let Some(ignore_file_edit) = associated_data.ignore_file_edit { - let disable_file_action = CodeAction { - title: format!("Disable {rule_name} for the entire file"), - kind: Some(CodeActionKind::QUICKFIX), - diagnostics: Some(vec![diagnostic.clone()]), - edit: Some(WorkspaceEdit { - changes: Some({ - let mut changes = HashMap::new(); - changes.insert(uri.clone(), vec![ignore_file_edit]); - changes - }), - ..Default::default() - }), - command: None, - is_preferred: Some(false), - disabled: None, - data: None, - }; - actions.push(CodeActionOrCommand::CodeAction(disable_file_action)); - } - - let title = format!("Show documentation for {rule_name}"); - let documentation_action = CodeAction { - title: title.clone(), - kind: Some(CodeActionKind::QUICKFIX), - diagnostics: Some(vec![diagnostic.clone()]), - edit: None, - command: Some(Command { - title, - command: "vscode.open".to_string(), - arguments: Some(vec![serde_json::to_value(format!( - "https://squawkhq.com/docs/{rule_name}" - ))?]), - }), - is_preferred: Some(false), - disabled: None, - data: None, - }; - actions.push(CodeActionOrCommand::CodeAction(documentation_action)); - - if !associated_data.title.is_empty() && !associated_data.edits.is_empty() { - let fix_action = CodeAction { - title: associated_data.title, - kind: Some(CodeActionKind::QUICKFIX), - diagnostics: Some(vec![diagnostic.clone()]), - edit: Some(WorkspaceEdit { - changes: Some({ - let mut changes = HashMap::new(); - changes.insert(uri.clone(), associated_data.edits); - changes - }), - ..Default::default() - }), - command: None, - is_preferred: Some(true), - disabled: None, - data: None, - }; - actions.push(CodeActionOrCommand::CodeAction(fix_action)); - } - } - - let result: CodeActionResponse = actions; - let resp = Response { - id: req.id, - result: Some(serde_json::to_value(&result).unwrap()), - error: None, - }; - - connection.sender.send(Message::Response(resp))?; - Ok(()) -} - -fn publish_diagnostics( - connection: &Connection, - uri: Url, - version: i32, - diagnostics: Vec, -) -> Result<()> { - let publish_params = PublishDiagnosticsParams { - uri, - diagnostics, - version: Some(version), - }; - - let notification = Notification { - method: PublishDiagnostics::METHOD.to_owned(), - params: serde_json::to_value(publish_params)?, - }; - - connection - .sender - .send(Message::Notification(notification))?; - Ok(()) -} - -fn handle_did_open( - connection: &Connection, - notif: lsp_server::Notification, - file_system: &mut impl FileSystem, -) -> Result<()> { - let params: DidOpenTextDocumentParams = serde_json::from_value(notif.params)?; - let uri = params.text_document.uri; - let content = params.text_document.text; - let version = params.text_document.version; - - file_system.set(uri.clone(), DocumentState { content, version }); - let db = file_system.db(); - let file = file_system.file(&uri).unwrap(); - let diagnostics = lint::lint(db, file); - - // TODO: we need a better setup for "run func when input changed" - publish_diagnostics(connection, uri, version, diagnostics)?; - - Ok(()) -} - -fn handle_did_change( - connection: &Connection, - notif: lsp_server::Notification, - file_system: &mut impl FileSystem, -) -> Result<()> { - let params: DidChangeTextDocumentParams = serde_json::from_value(notif.params)?; - let uri = params.text_document.uri; - let version = params.text_document.version; - - let db = file_system.db(); - let file = file_system.file(&uri).unwrap(); - let content = file.content(db); - - let updated_content = lsp_utils::apply_incremental_changes(content, params.content_changes); - - file_system.set( - uri.clone(), - DocumentState { - content: updated_content, - version, - }, - ); - let db = file_system.db(); - let file = file_system.file(&uri).unwrap(); - let diagnostics = lint::lint(db, file); - publish_diagnostics(connection, uri, version, diagnostics)?; - - Ok(()) -} - -fn handle_did_close( - connection: &Connection, - notif: lsp_server::Notification, - file_system: &mut impl FileSystem, -) -> Result<()> { - let params: DidCloseTextDocumentParams = serde_json::from_value(notif.params)?; - let uri = params.text_document.uri; - - file_system.remove(&uri); - - let publish_params = PublishDiagnosticsParams { - uri, - diagnostics: vec![], - version: None, - }; - - let notification = Notification { - method: PublishDiagnostics::METHOD.to_owned(), - params: serde_json::to_value(publish_params)?, - }; - - connection - .sender - .send(Message::Notification(notification))?; - - Ok(()) -} - -#[derive(serde::Deserialize)] -struct SyntaxTreeParams { - #[serde(rename = "textDocument")] - text_document: lsp_types::TextDocumentIdentifier, -} - -fn handle_syntax_tree( - connection: &Connection, - req: lsp_server::Request, - file_system: &impl FileSystem, -) -> Result<()> { - let params: SyntaxTreeParams = serde_json::from_value(req.params)?; - let uri = params.text_document.uri; - - info!("Generating syntax tree for: {uri}"); - - let db = file_system.db(); - let file = file_system.file(&uri).unwrap(); - let parse = parse(db, file); - let syntax_tree = format!("{:#?}", parse.syntax_node()); - - let resp = Response { - id: req.id, - result: Some(serde_json::to_value(&syntax_tree).unwrap()), - error: None, - }; - - connection.sender.send(Message::Response(resp))?; - Ok(()) -} - -#[derive(serde::Deserialize)] -struct TokensParams { - #[serde(rename = "textDocument")] - text_document: lsp_types::TextDocumentIdentifier, -} - -fn handle_tokens( - connection: &Connection, - req: lsp_server::Request, - file_system: &impl FileSystem, -) -> Result<()> { - let params: TokensParams = serde_json::from_value(req.params)?; - let uri = params.text_document.uri; - - info!("Generating tokens for: {uri}"); - - let db = file_system.db(); - let file = file_system.file(&uri).unwrap(); - let content = file.content(db); - - // TODO: move this to a tracked function - let tokens = squawk_lexer::tokenize(content); - - let mut output = Vec::new(); - let mut char_pos = 0; - for token in tokens { - let token_start = char_pos; - let token_end = token_start + token.len as usize; - let token_text = &content[token_start..token_end]; - output.push(format!( - "{:?}@{}..{} {:?}", - token.kind, token_start, token_end, token_text - )); - char_pos = token_end; - } - - let tokens_output = output.join("\n"); - - let resp = Response { - id: req.id, - result: Some(serde_json::to_value(&tokens_output).unwrap()), - error: None, - }; - - connection.sender.send(Message::Response(resp))?; - Ok(()) -} +pub use server::run; diff --git a/crates/squawk_server/src/lint.rs b/crates/squawk_server/src/lint.rs index 71f1d2b9..ab52490b 100644 --- a/crates/squawk_server/src/lint.rs +++ b/crates/squawk_server/src/lint.rs @@ -5,8 +5,7 @@ use squawk_ide::db::{File, line_index as file_line_index, parse}; use squawk_linter::{Edit, Linter}; use crate::{ - DIAGNOSTIC_NAME, - diagnostic::AssociatedDiagnosticData, + diagnostic::{AssociatedDiagnosticData, DIAGNOSTIC_NAME}, ignore::{ignore_file_edit, ignore_line_edit}, }; diff --git a/crates/squawk_server/src/lsp_utils.rs b/crates/squawk_server/src/lsp_utils.rs index 7198bda7..49b7ae43 100644 --- a/crates/squawk_server/src/lsp_utils.rs +++ b/crates/squawk_server/src/lsp_utils.rs @@ -1,14 +1,18 @@ use std::{collections::HashMap, ops::Range}; -use line_index::{LineIndex, TextRange, TextSize}; +use ::line_index::{LineIndex, TextRange, TextSize}; use log::warn; use lsp_types::{ - CodeAction, CodeActionKind, FoldingRange, FoldingRangeKind as LspFoldingRangeKind, Url, - WorkspaceEdit, + CodeAction, CodeActionKind, FoldingRange, FoldingRangeKind as LspFoldingRangeKind, Location, + Url, WorkspaceEdit, }; +use squawk_ide::builtins::{builtins_line_index, builtins_url}; use squawk_ide::code_actions::ActionKind; +use squawk_ide::db::line_index; use squawk_ide::folding_ranges::{Fold, FoldKind}; +use crate::system::System; + fn text_range(index: &LineIndex, range: lsp_types::Range) -> Option { let start = offset(index, range.start)?; let end = offset(index, range.end)?; @@ -210,6 +214,25 @@ pub(crate) fn apply_incremental_changes( text } +pub(crate) fn to_location( + db: &dyn salsa::Database, + system: &impl System, + uri: &Url, + loc: squawk_ide::goto_definition::Location, +) -> Option { + let file = system.file(uri).unwrap(); + let uri = match loc.file { + squawk_ide::goto_definition::FileId::Current => uri.clone(), + squawk_ide::goto_definition::FileId::Builtins => builtins_url(db)?, + }; + let line_index = match loc.file { + squawk_ide::goto_definition::FileId::Current => &line_index(db, file), + squawk_ide::goto_definition::FileId::Builtins => &builtins_line_index(db), + }; + let range = range(line_index, loc.range); + Some(Location { uri, range }) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/squawk_server/src/server.rs b/crates/squawk_server/src/server.rs new file mode 100644 index 00000000..c6a13992 --- /dev/null +++ b/crates/squawk_server/src/server.rs @@ -0,0 +1,159 @@ +use anyhow::Result; +use log::info; +use lsp_server::{Connection, Message}; +use lsp_types::{ + CodeActionKind, CodeActionOptions, CodeActionProviderCapability, CompletionOptions, + FoldingRangeProviderCapability, HoverProviderCapability, InitializeParams, OneOf, + SelectionRangeProviderCapability, ServerCapabilities, TextDocumentSyncCapability, + TextDocumentSyncKind, WorkDoneProgressOptions, + notification::{ + DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, Notification as _, + }, + request::{ + CodeActionRequest, Completion, DocumentSymbolRequest, FoldingRangeRequest, GotoDefinition, + HoverRequest, InlayHintRequest, References, Request, SelectionRangeRequest, + }, +}; + +use crate::handlers::{ + handle_code_action, handle_completion, handle_did_change, handle_did_close, handle_did_open, + handle_document_symbol, handle_folding_range, handle_goto_definition, handle_hover, + handle_inlay_hints, handle_references, handle_selection_range, handle_syntax_tree, + handle_tokens, +}; +use crate::system::GlobalState; + +pub fn run() -> Result<()> { + info!("Starting Squawk LSP server"); + + let (connection, io_threads) = Connection::stdio(); + + let server_capabilities = serde_json::to_value(&ServerCapabilities { + text_document_sync: Some(TextDocumentSyncCapability::Kind( + TextDocumentSyncKind::INCREMENTAL, + )), + code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions { + code_action_kinds: Some(vec![ + CodeActionKind::QUICKFIX, + CodeActionKind::REFACTOR_REWRITE, + ]), + work_done_progress_options: WorkDoneProgressOptions { + work_done_progress: None, + }, + resolve_provider: None, + })), + selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)), + references_provider: Some(OneOf::Left(true)), + definition_provider: Some(OneOf::Left(true)), + hover_provider: Some(HoverProviderCapability::Simple(true)), + inlay_hint_provider: Some(OneOf::Left(true)), + document_symbol_provider: Some(OneOf::Left(true)), + folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)), + completion_provider: Some(CompletionOptions { + resolve_provider: Some(false), + trigger_characters: Some(vec![".".to_owned()]), + all_commit_characters: None, + work_done_progress_options: WorkDoneProgressOptions { + work_done_progress: None, + }, + completion_item: None, + }), + ..Default::default() + }) + .unwrap(); + + info!("LSP server initializing connection..."); + let initialization_params = connection.initialize(server_capabilities)?; + info!("LSP server initialized, entering main loop"); + + main_loop(connection, initialization_params)?; + + info!("LSP server shutting down"); + + io_threads.join()?; + Ok(()) +} + +fn main_loop(connection: Connection, params: serde_json::Value) -> Result<()> { + info!("Server main loop"); + + let init_params: InitializeParams = serde_json::from_value(params).unwrap_or_default(); + info!("Client process ID: {:?}", init_params.process_id); + let client_name = init_params.client_info.map(|x| x.name); + info!("Client name: {client_name:?}"); + + let mut system = GlobalState::new(); + + for msg in &connection.receiver { + match msg { + Message::Request(req) => { + info!("Received request: method={}, id={:?}", req.method, req.id); + + if connection.handle_shutdown(&req)? { + info!("Received shutdown request, exiting"); + return Ok(()); + } + + match req.method.as_ref() { + GotoDefinition::METHOD => { + handle_goto_definition(&connection, req, &system)?; + } + HoverRequest::METHOD => { + handle_hover(&connection, req, &system)?; + } + CodeActionRequest::METHOD => { + handle_code_action(&connection, req, &system)?; + } + SelectionRangeRequest::METHOD => { + handle_selection_range(&connection, req, &system)?; + } + InlayHintRequest::METHOD => { + handle_inlay_hints(&connection, req, &system)?; + } + DocumentSymbolRequest::METHOD => { + handle_document_symbol(&connection, req, &system)?; + } + FoldingRangeRequest::METHOD => { + handle_folding_range(&connection, req, &system)?; + } + Completion::METHOD => { + handle_completion(&connection, req, &system)?; + } + "squawk/syntaxTree" => { + handle_syntax_tree(&connection, req, &system)?; + } + "squawk/tokens" => { + handle_tokens(&connection, req, &system)?; + } + References::METHOD => { + handle_references(&connection, req, &system)?; + } + _ => { + info!("Ignoring unhandled request: {}", req.method); + } + } + } + Message::Response(resp) => { + info!("Received response: id={:?}", resp.id); + } + Message::Notification(notif) => { + info!("Received notification: method={}", notif.method); + match notif.method.as_ref() { + DidOpenTextDocument::METHOD => { + handle_did_open(&connection, notif, &mut system)?; + } + DidChangeTextDocument::METHOD => { + handle_did_change(&connection, notif, &mut system)?; + } + DidCloseTextDocument::METHOD => { + handle_did_close(&connection, notif, &mut system)?; + } + _ => { + info!("Ignoring unhandled notification: {}", notif.method); + } + } + } + } + } + Ok(()) +} diff --git a/crates/squawk_server/src/system.rs b/crates/squawk_server/src/system.rs new file mode 100644 index 00000000..77f4677f --- /dev/null +++ b/crates/squawk_server/src/system.rs @@ -0,0 +1,55 @@ +use lsp_types::Url; +use salsa::Setter; +use squawk_ide::db::{Database, File}; +use std::collections::HashMap; + +pub(crate) struct Document { + pub(crate) content: String, + #[allow(dead_code)] + pub(crate) version: i32, +} + +pub(crate) trait System { + fn db(&self) -> &Database; + fn file(&self, uri: &Url) -> Option; + fn set(&mut self, uri: Url, doc: Document); + fn remove(&mut self, uri: &Url); +} + +pub(super) struct GlobalState { + pub db: Database, + files: HashMap, +} + +impl GlobalState { + pub(super) fn new() -> Self { + Self { + db: Database::default(), + files: HashMap::new(), + } + } +} + +impl System for GlobalState { + fn db(&self) -> &Database { + return &self.db; + } + + fn file(&self, uri: &Url) -> Option { + self.files.get(uri).copied() + } + + fn set(&mut self, uri: Url, doc: Document) { + if let Some(file) = self.files.get(&uri).copied() { + file.set_content(&mut self.db).to(doc.content); + file.set_version(&mut self.db).to(doc.version); + } else { + let file = File::new(&self.db, doc.content, doc.version); + self.files.insert(uri, file); + } + } + + fn remove(&mut self, uri: &Url) { + self.files.remove(uri); + } +} diff --git a/crates/squawk_thread/src/intent.rs b/crates/squawk_thread/src/intent.rs index 1203bfc3..77ef46bf 100644 --- a/crates/squawk_thread/src/intent.rs +++ b/crates/squawk_thread/src/intent.rs @@ -50,6 +50,7 @@ fn thread_intent_to_qos_class(intent: ThreadIntent) -> QoSClass { // All Apple platforms use XNU as their kernel // and thus have the concept of QoS. #[cfg(target_vendor = "apple")] +#[allow(clippy::doc_markdown)] mod imp { use super::ThreadIntent;