diff --git a/PLAN.md b/PLAN.md index 0f95c8ad..1cdf9c8b 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1056,8 +1056,6 @@ select x from "t"; gives: -Note: we have to be mindful of casing here, - ```sql select "x" from "t"; ``` diff --git a/crates/squawk_ide/src/goto_definition.rs b/crates/squawk_ide/src/goto_definition.rs new file mode 100644 index 00000000..2d26c9cc --- /dev/null +++ b/crates/squawk_ide/src/goto_definition.rs @@ -0,0 +1,162 @@ +use rowan::{TextRange, TextSize}; +use squawk_syntax::{ + SyntaxKind, SyntaxToken, + ast::{self, AstNode}, +}; + +pub fn goto_definition(file: ast::SourceFile, offset: TextSize) -> Option { + let token = token_from_offset(&file, offset)?; + let parent = token.parent()?; + + // goto def on case exprs + if (token.kind() == SyntaxKind::WHEN_KW && parent.kind() == SyntaxKind::WHEN_CLAUSE) + || (token.kind() == SyntaxKind::ELSE_KW && parent.kind() == SyntaxKind::ELSE_CLAUSE) + || (token.kind() == SyntaxKind::END_KW && parent.kind() == SyntaxKind::CASE_EXPR) + { + for parent in token.parent_ancestors() { + if let Some(case_expr) = ast::CaseExpr::cast(parent) { + if let Some(case_token) = case_expr.case_token() { + return Some(case_token.text_range()); + } + } + } + } + + return None; +} + +fn token_from_offset(file: &ast::SourceFile, offset: TextSize) -> Option { + let mut token = file.syntax().token_at_offset(offset).right_biased()?; + // want to be lenient in case someone clicks the trailing `;` of a line + // instead of an identifier + if token.kind() == SyntaxKind::SEMICOLON { + token = token.prev_token()?; + } + return Some(token); +} + +#[cfg(test)] +mod test { + use crate::goto_definition::goto_definition; + use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet, renderer::DecorStyle}; + use insta::assert_snapshot; + use log::info; + use rowan::TextSize; + use squawk_syntax::ast; + + // TODO: we should probably use something else since `$0` is valid syntax, maybe `%0`? + const MARKER: &str = "$0"; + + fn fixture(sql: &str) -> (Option, String) { + if let Some(pos) = sql.find(MARKER) { + return ( + Some(TextSize::new((pos - 1) as u32)), + sql.replace(MARKER, ""), + ); + } + (None, sql.to_string()) + } + + #[track_caller] + fn goto(sql: &str) -> String { + goto_(sql).expect("should always find a definition") + } + + #[track_caller] + fn goto_(sql: &str) -> Option { + info!("starting"); + let (offset, sql) = fixture(sql); + let parse = ast::SourceFile::parse(&sql); + assert_eq!(parse.errors(), vec![]); + let file: ast::SourceFile = parse.tree(); + let Some(offset) = offset else { + info!("offset not found, did you put a marker `$0` in the sql?"); + return None; + }; + if let Some(result) = goto_definition(file, offset) { + let offset: usize = offset.into(); + let group = Level::INFO.primary_title("definition").element( + Snippet::source(&sql) + .fold(true) + .annotation( + AnnotationKind::Context + .span(result.into()) + .label("2. destination"), + ) + .annotation( + AnnotationKind::Context + .span(offset..offset + 1) + .label("1. source"), + ), + ); + let renderer = Renderer::plain().decor_style(DecorStyle::Unicode); + return Some( + renderer + .render(&[group]) + .to_string() + // hacky cleanup to make the text shorter + .replace("info: definition", ""), + ); + } + None + } + + fn goto_not_found(sql: &str) { + assert!(goto_(sql).is_none(), "Should not find a definition"); + } + + #[test] + fn goto_case_when() { + assert_snapshot!(goto(" +select case when$0 x > 1 then 1 else 2 end; +"), @r" + ╭▸ + 2 │ select case when x > 1 then 1 else 2 end; + │ ┬─── ─ 1. source + │ │ + ╰╴ 2. destination + "); + } + + #[test] + fn goto_case_else() { + assert_snapshot!(goto(" +select case when x > 1 then 1 else$0 2 end; +"), @r" + ╭▸ + 2 │ select case when x > 1 then 1 else 2 end; + ╰╴ ──── 2. destination ─ 1. source + "); + } + + #[test] + fn goto_case_end() { + assert_snapshot!(goto(" +select case when x > 1 then 1 else 2 end$0; +"), @r" + ╭▸ + 2 │ select case when x > 1 then 1 else 2 end; + ╰╴ ──── 2. destination ─ 1. source + "); + } + + #[test] + fn goto_case_end_trailing_semi() { + assert_snapshot!(goto(" +select case when x > 1 then 1 else 2 end;$0 +"), @r" + ╭▸ + 2 │ select case when x > 1 then 1 else 2 end; + ╰╴ ──── 2. destination ─ 1. source + "); + } + + #[test] + fn goto_case_then_not_found() { + goto_not_found( + " +select case when x > 1 then$0 1 else 2 end; +", + ) + } +} diff --git a/crates/squawk_ide/src/lib.rs b/crates/squawk_ide/src/lib.rs index 0466be5f..040df2fa 100644 --- a/crates/squawk_ide/src/lib.rs +++ b/crates/squawk_ide/src/lib.rs @@ -1 +1,2 @@ pub mod expand_selection; +pub mod goto_definition; diff --git a/crates/squawk_parser/src/generated/syntax_kind.rs b/crates/squawk_parser/src/generated/syntax_kind.rs index 9155e38f..a0dead47 100644 --- a/crates/squawk_parser/src/generated/syntax_kind.rs +++ b/crates/squawk_parser/src/generated/syntax_kind.rs @@ -1041,6 +1041,7 @@ pub enum SyntaxKind { WHEN_CLAUSE, WHEN_CLAUSE_LIST, WHERE_CLAUSE, + WHERE_CURRENT_OF, WINDOW_CLAUSE, WINDOW_DEF, WINDOW_FUNC_OPTION, diff --git a/crates/squawk_parser/src/grammar.rs b/crates/squawk_parser/src/grammar.rs index c708a97b..fcb636f5 100644 --- a/crates/squawk_parser/src/grammar.rs +++ b/crates/squawk_parser/src/grammar.rs @@ -4993,10 +4993,14 @@ fn partition_option(p: &mut Parser<'_>) { } fn opt_inherits_tables(p: &mut Parser<'_>) { + let m = p.start(); if p.eat(INHERITS_KW) { p.expect(L_PAREN); path_name_ref_list(p); p.expect(R_PAREN); + m.complete(p, INHERITS); + } else { + m.abandon(p); } } @@ -12295,6 +12299,13 @@ fn update(p: &mut Parser<'_>, m: Option) -> CompletedMarker { // [ FROM from_item [, ...] ] opt_from_clause(p); // [ WHERE condition | WHERE CURRENT OF cursor_name ] + opt_where_or_current_of(p); + // [ RETURNING { * | output_expression [ [ AS ] output_name ] } [, ...] ] + opt_returning_clause(p); + m.complete(p, UPDATE) +} + +fn opt_where_or_current_of(p: &mut Parser<'_>) { if p.at(WHERE_KW) { if p.nth_at(1, CURRENT_KW) { opt_where_current_of(p); @@ -12302,9 +12313,6 @@ fn update(p: &mut Parser<'_>, m: Option) -> CompletedMarker { opt_where_clause(p); } } - // [ RETURNING { * | output_expression [ [ AS ] output_name ] } [, ...] ] - opt_returning_clause(p); - m.complete(p, UPDATE) } fn with(p: &mut Parser<'_>, m: Option) -> Option { @@ -12355,24 +12363,22 @@ fn delete(p: &mut Parser<'_>, m: Option) -> CompletedMarker { } } // [ WHERE condition | WHERE CURRENT OF cursor_name ] - if p.at(WHERE_KW) { - if p.nth_at(1, CURRENT_KW) { - opt_where_current_of(p); - } else { - opt_where_clause(p); - } - } + opt_where_or_current_of(p); opt_returning_clause(p); m.complete(p, DELETE) } // WHERE CURRENT OF cursor_name fn opt_where_current_of(p: &mut Parser<'_>) { + let m = p.start(); if p.eat(WHERE_KW) { if p.eat(CURRENT_KW) { p.expect(OF_KW); name_ref(p); } + m.complete(p, WHERE_CURRENT_OF); + } else { + m.abandon(p); } } diff --git a/crates/squawk_parser/tests/snapshots/tests__create_foreign_table_ok.snap b/crates/squawk_parser/tests/snapshots/tests__create_foreign_table_ok.snap index cce22006..b697133b 100644 --- a/crates/squawk_parser/tests/snapshots/tests__create_foreign_table_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__create_foreign_table_ok.snap @@ -189,25 +189,26 @@ SOURCE_FILE WHITESPACE "\n" R_PAREN ")" WHITESPACE "\n " - INHERITS_KW "inherits" - WHITESPACE " " - L_PAREN "(" - PATH + INHERITS + INHERITS_KW "inherits" + WHITESPACE " " + L_PAREN "(" PATH + PATH + PATH_SEGMENT + NAME_REF + IDENT "foo" + DOT "." PATH_SEGMENT NAME_REF - IDENT "foo" - DOT "." - PATH_SEGMENT - NAME_REF - IDENT "bar" - COMMA "," - WHITESPACE " " - PATH - PATH_SEGMENT - NAME_REF - IDENT "bar" - R_PAREN ")" + IDENT "bar" + COMMA "," + WHITESPACE " " + PATH + PATH_SEGMENT + NAME_REF + IDENT "bar" + R_PAREN ")" WHITESPACE "\n " SERVER_KW "server" WHITESPACE " " diff --git a/crates/squawk_parser/tests/snapshots/tests__create_table_ok.snap b/crates/squawk_parser/tests/snapshots/tests__create_table_ok.snap index 6b6cc374..a4dfde57 100644 --- a/crates/squawk_parser/tests/snapshots/tests__create_table_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__create_table_ok.snap @@ -394,31 +394,32 @@ SOURCE_FILE INT_KW "int" R_PAREN ")" WHITESPACE "\n" - INHERITS_KW "inherits" - WHITESPACE " " - L_PAREN "(" - PATH + INHERITS + INHERITS_KW "inherits" + WHITESPACE " " + L_PAREN "(" PATH + PATH + PATH_SEGMENT + NAME_REF + IDENT "foo" + DOT "." PATH_SEGMENT NAME_REF - IDENT "foo" - DOT "." - PATH_SEGMENT - NAME_REF - IDENT "bar" - COMMA "," - WHITESPACE " " - PATH - PATH_SEGMENT - NAME_REF - IDENT "bar" - COMMA "," - WHITESPACE " " - PATH - PATH_SEGMENT - NAME_REF - IDENT "buzz" - R_PAREN ")" + IDENT "bar" + COMMA "," + WHITESPACE " " + PATH + PATH_SEGMENT + NAME_REF + IDENT "bar" + COMMA "," + WHITESPACE " " + PATH + PATH_SEGMENT + NAME_REF + IDENT "buzz" + R_PAREN ")" SEMICOLON ";" WHITESPACE "\n\n" CREATE_TABLE diff --git a/crates/squawk_parser/tests/snapshots/tests__delete_ok.snap b/crates/squawk_parser/tests/snapshots/tests__delete_ok.snap index b6e9efff..c2a9a583 100644 --- a/crates/squawk_parser/tests/snapshots/tests__delete_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__delete_ok.snap @@ -477,14 +477,15 @@ SOURCE_FILE NAME_REF IDENT "invoices" WHITESPACE " \n" - WHERE_KW "where" - WHITESPACE " " - CURRENT_KW "current" - WHITESPACE " " - OF_KW "of" - WHITESPACE " " - NAME_REF - IDENT "invoice_cursor" + WHERE_CURRENT_OF + WHERE_KW "where" + WHITESPACE " " + CURRENT_KW "current" + WHITESPACE " " + OF_KW "of" + WHITESPACE " " + NAME_REF + IDENT "invoice_cursor" SEMICOLON ";" WHITESPACE "\n\n" COMMENT "-- returning" @@ -931,14 +932,15 @@ SOURCE_FILE NAME_REF IDENT "tasks" WHITESPACE " " - WHERE_KW "WHERE" - WHITESPACE " " - CURRENT_KW "CURRENT" - WHITESPACE " " - OF_KW "OF" - WHITESPACE " " - NAME_REF - IDENT "c_tasks" + WHERE_CURRENT_OF + WHERE_KW "WHERE" + WHITESPACE " " + CURRENT_KW "CURRENT" + WHITESPACE " " + OF_KW "OF" + WHITESPACE " " + NAME_REF + IDENT "c_tasks" SEMICOLON ";" WHITESPACE "\n\n" DELETE diff --git a/crates/squawk_parser/tests/snapshots/tests__update_ok.snap b/crates/squawk_parser/tests/snapshots/tests__update_ok.snap index 90da1ebd..324c71fb 100644 --- a/crates/squawk_parser/tests/snapshots/tests__update_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__update_ok.snap @@ -1226,14 +1226,15 @@ SOURCE_FILE LITERAL STRING "'Dramatic'" WHITESPACE " " - WHERE_KW "WHERE" - WHITESPACE " " - CURRENT_KW "CURRENT" - WHITESPACE " " - OF_KW "OF" - WHITESPACE " " - NAME_REF - IDENT "c_films" + WHERE_CURRENT_OF + WHERE_KW "WHERE" + WHITESPACE " " + CURRENT_KW "CURRENT" + WHITESPACE " " + OF_KW "OF" + WHITESPACE " " + NAME_REF + IDENT "c_films" SEMICOLON ";" WHITESPACE "\n\n" UPDATE diff --git a/crates/squawk_server/src/lib.rs b/crates/squawk_server/src/lib.rs index 5b3dc255..0f8997fc 100644 --- a/crates/squawk_server/src/lib.rs +++ b/crates/squawk_server/src/lib.rs @@ -6,8 +6,8 @@ use lsp_types::{ CodeAction, CodeActionKind, CodeActionOptions, CodeActionOrCommand, CodeActionParams, CodeActionProviderCapability, CodeActionResponse, Command, Diagnostic, DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, - GotoDefinitionParams, GotoDefinitionResponse, InitializeParams, Location, Position, - PublishDiagnosticsParams, Range, SelectionRangeParams, SelectionRangeProviderCapability, + GotoDefinitionParams, GotoDefinitionResponse, InitializeParams, Location, OneOf, + PublishDiagnosticsParams, SelectionRangeParams, SelectionRangeProviderCapability, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, Url, WorkDoneProgressOptions, WorkspaceEdit, notification::{ @@ -17,6 +17,7 @@ use lsp_types::{ request::{CodeActionRequest, GotoDefinition, Request, SelectionRangeRequest}, }; use rowan::TextRange; +use squawk_ide::goto_definition::goto_definition; use squawk_syntax::{Parse, SourceFile}; use std::collections::HashMap; @@ -50,7 +51,7 @@ pub fn run() -> Result<()> { resolve_provider: None, })), selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)), - // definition_provider: Some(OneOf::Left(true)), + definition_provider: Some(OneOf::Left(true)), ..Default::default() }) .unwrap(); @@ -89,7 +90,7 @@ fn main_loop(connection: Connection, params: serde_json::Value) -> Result<()> { match req.method.as_ref() { GotoDefinition::METHOD => { - handle_goto_definition(&connection, req)?; + handle_goto_definition(&connection, req, &documents)?; } CodeActionRequest::METHOD => { handle_code_action(&connection, req, &documents)?; @@ -133,15 +134,37 @@ fn main_loop(connection: Connection, params: serde_json::Value) -> Result<()> { Ok(()) } -fn handle_goto_definition(connection: &Connection, req: lsp_server::Request) -> Result<()> { +fn handle_goto_definition( + connection: &Connection, + req: lsp_server::Request, + documents: &HashMap, +) -> 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 location = Location { - uri: params.text_document_position_params.text_document.uri, - range: Range::new(Position::new(1, 2), Position::new(1, 3)), + let content = documents.get(&uri).map_or("", |doc| &doc.content); + let parse: Parse = SourceFile::parse(content); + let file = parse.tree(); + let line_index = LineIndex::new(content); + let offset = lsp_utils::offset(&line_index, position).unwrap(); + + let range = goto_definition(file, offset); + + let result = match range { + Some(target_range) => { + debug_assert!( + !target_range.contains(offset), + "Our target destination range must not include the source range otherwise go to def won't work in vscode." + ); + GotoDefinitionResponse::Scalar(Location { + uri: uri.clone(), + range: lsp_utils::range(&line_index, target_range), + }) + } + None => GotoDefinitionResponse::Array(vec![]), }; - let result = GotoDefinitionResponse::Scalar(location); let resp = Response { id: req.id, result: Some(serde_json::to_value(&result).unwrap()), diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index f6cbd31b..381316d0 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -3187,30 +3187,26 @@ impl Delete { support::child(&self.syntax) } #[inline] - pub fn expr(&self) -> Option { + pub fn relation_name(&self) -> Option { support::child(&self.syntax) } #[inline] - pub fn name_ref(&self) -> Option { + pub fn returning_clause(&self) -> Option { support::child(&self.syntax) } #[inline] - pub fn relation_name(&self) -> Option { + pub fn using_clause(&self) -> Option { support::child(&self.syntax) } #[inline] - pub fn returning_clause(&self) -> Option { + pub fn where_clause(&self) -> Option { support::child(&self.syntax) } #[inline] - pub fn using_clause(&self) -> Option { + pub fn where_current_of(&self) -> Option { support::child(&self.syntax) } #[inline] - pub fn current_token(&self) -> Option { - support::token(&self.syntax, SyntaxKind::CURRENT_KW) - } - #[inline] pub fn delete_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::DELETE_KW) } @@ -3218,14 +3214,6 @@ impl Delete { pub fn from_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::FROM_KW) } - #[inline] - pub fn of_token(&self) -> Option { - support::token(&self.syntax, SyntaxKind::OF_KW) - } - #[inline] - pub fn where_token(&self) -> Option { - support::token(&self.syntax, SyntaxKind::WHERE_KW) - } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -10371,6 +10359,29 @@ impl WhereClause { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct WhereCurrentOf { + pub(crate) syntax: SyntaxNode, +} +impl WhereCurrentOf { + #[inline] + pub fn name_ref(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn current_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::CURRENT_KW) + } + #[inline] + pub fn of_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::OF_KW) + } + #[inline] + pub fn where_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::WHERE_KW) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct WindowClause { pub(crate) syntax: SyntaxNode, @@ -20210,6 +20221,24 @@ impl AstNode for WhereClause { &self.syntax } } +impl AstNode for WhereCurrentOf { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::WHERE_CURRENT_OF + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} impl AstNode for WindowClause { #[inline] fn can_cast(kind: SyntaxKind) -> bool { diff --git a/crates/squawk_syntax/src/postgresql.ungram b/crates/squawk_syntax/src/postgresql.ungram index 1e01d989..95057aa6 100644 --- a/crates/squawk_syntax/src/postgresql.ungram +++ b/crates/squawk_syntax/src/postgresql.ungram @@ -974,9 +974,12 @@ ReturningClause = Delete = 'delete' 'from' RelationName Alias? UsingClause? - ('where' Expr | 'where' 'current' 'of' NameRef)? + (WhereClause | WhereCurrentOf)? ReturningClause? +WhereCurrentOf = + 'where' 'current' 'of' NameRef + Notify = 'notify'