From c39680c04cbc98e31ecb5a39a022595015d29b7f Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sun, 22 Feb 2026 20:45:26 -0500 Subject: [PATCH] ide: code action - rewrite between as binary expression --- crates/squawk_ide/src/code_actions.rs | 95 +++++++++++++++++++ .../squawk_syntax/src/ast/generated/nodes.rs | 8 ++ crates/squawk_syntax/src/postgresql.ungram | 2 +- 3 files changed, 104 insertions(+), 1 deletion(-) diff --git a/crates/squawk_ide/src/code_actions.rs b/crates/squawk_ide/src/code_actions.rs index 94a3f606..6cf8b92a 100644 --- a/crates/squawk_ide/src/code_actions.rs +++ b/crates/squawk_ide/src/code_actions.rs @@ -46,6 +46,7 @@ pub fn code_actions(file: ast::SourceFile, offset: TextSize) -> Option, + file: &ast::SourceFile, + offset: TextSize, +) -> Option<()> { + let token = token_from_offset(file, offset)?; + let between_expr = token.parent_ancestors().find_map(ast::BetweenExpr::cast)?; + + let target = between_expr.target()?; + let start = between_expr.start()?; + let end = between_expr.end()?; + + let is_not = between_expr.not_token().is_some(); + let is_symmetric = between_expr.symmetric_token().is_some(); + + let target_text = target.syntax().text(); + let start_text = start.syntax().text(); + let end_text = end.syntax().text(); + + let replacement = match (is_not, is_symmetric) { + (false, false) => { + format!("{target_text} >= {start_text} and {target_text} <= {end_text}") + } + (true, false) => { + format!("({target_text} < {start_text} or {target_text} > {end_text})") + } + (false, true) => format!( + "{target_text} >= least({start_text}, {end_text}) and {target_text} <= greatest({start_text}, {end_text})" + ), + (true, true) => format!( + "({target_text} < least({start_text}, {end_text}) or {target_text} > greatest({start_text}, {end_text}))" + ), + }; + + actions.push(CodeAction { + title: "Rewrite as binary expression".to_owned(), + edits: vec![Edit::replace( + between_expr.syntax().text_range(), + replacement, + )], + kind: ActionKind::RefactorRewrite, + }); + + Some(()) +} + fn rewrite_values_as_select( actions: &mut Vec, file: &ast::SourceFile, @@ -1857,6 +1904,54 @@ select myschema.f$0();" )); } + #[test] + fn rewrite_between_as_binary_expression_simple() { + assert_snapshot!(apply_code_action( + rewrite_between_as_binary_expression, + "select 2 betw$0een 1 and 3;" + ), + @"select 2 >= 1 and 2 <= 3;" + ); + } + + #[test] + fn rewrite_not_between_as_binary_expression() { + assert_snapshot!(apply_code_action( + rewrite_between_as_binary_expression, + "select 2 no$0t between 1 and 3;" + ), + @"select (2 < 1 or 2 > 3);" + ); + } + + #[test] + fn rewrite_between_symmetric_as_binary_expression() { + assert_snapshot!(apply_code_action( + rewrite_between_as_binary_expression, + "select 2 between symme$0tric 3 and 1;" + ), + @"select 2 >= least(3, 1) and 2 <= greatest(3, 1);" + ); + } + + #[test] + fn rewrite_not_between_symmetric_as_binary_expression() { + assert_snapshot!(apply_code_action( + rewrite_between_as_binary_expression, + "select 2 not between symme$0tric 3 and 1;" + ), + @"select (2 < least(3, 1) or 2 > greatest(3, 1));" + ); + } + + #[test] + fn rewrite_between_as_binary_expression_not_applicable() { + assert!(code_action_not_applicable( + rewrite_between_as_binary_expression, + "select 1 +$0 2;" + )); + } + #[test] fn rewrite_values_as_select_simple() { assert_snapshot!( diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index 3fb79184..89cfd91c 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -2121,6 +2121,14 @@ impl BetweenExpr { pub fn between_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::BETWEEN_KW) } + #[inline] + pub fn not_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::NOT_KW) + } + #[inline] + pub fn symmetric_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::SYMMETRIC_KW) + } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] diff --git a/crates/squawk_syntax/src/postgresql.ungram b/crates/squawk_syntax/src/postgresql.ungram index 3353096b..903c0eff 100644 --- a/crates/squawk_syntax/src/postgresql.ungram +++ b/crates/squawk_syntax/src/postgresql.ungram @@ -1505,7 +1505,7 @@ SliceExpr = base:Expr '[' start:Expr? ':' end:Expr? ']' BetweenExpr = - target:Expr 'between' start:Expr 'and' end:Expr + target:Expr 'not'? 'between' 'symmetric'? start:Expr 'and' end:Expr JsonTableColumn = Name 'for' 'ordinality'