@@ -5,18 +5,128 @@ use squawk_syntax::{
55 ast:: { self , AstNode } ,
66} ;
77
8+ #[ derive( Debug , Clone ) ]
9+ pub enum ActionKind {
10+ QuickFix ,
11+ RefactorRewrite ,
12+ }
13+
814#[ derive( Debug , Clone ) ]
915pub struct CodeAction {
1016 pub title : String ,
1117 pub edits : Vec < Edit > ,
18+ pub kind : ActionKind ,
1219}
1320
1421pub fn code_actions ( file : ast:: SourceFile , offset : TextSize ) -> Option < Vec < CodeAction > > {
1522 let mut actions = vec ! [ ] ;
23+ rewrite_as_regular_string ( & mut actions, & file, offset) ;
24+ rewrite_as_dollar_quoted_string ( & mut actions, & file, offset) ;
1625 remove_else_clause ( & mut actions, & file, offset) ;
1726 Some ( actions)
1827}
1928
29+ fn rewrite_as_regular_string (
30+ actions : & mut Vec < CodeAction > ,
31+ file : & ast:: SourceFile ,
32+ offset : TextSize ,
33+ ) -> Option < ( ) > {
34+ let dollar_string = file
35+ . syntax ( )
36+ . token_at_offset ( offset)
37+ . find ( |token| token. kind ( ) == SyntaxKind :: DOLLAR_QUOTED_STRING ) ?;
38+
39+ let replacement = dollar_quoted_to_string ( dollar_string. text ( ) ) ?;
40+ actions. push ( CodeAction {
41+ title : "Rewrite as regular string" . to_owned ( ) ,
42+ edits : vec ! [ Edit :: replace( dollar_string. text_range( ) , replacement) ] ,
43+ kind : ActionKind :: RefactorRewrite ,
44+ } ) ;
45+
46+ Some ( ( ) )
47+ }
48+
49+ fn rewrite_as_dollar_quoted_string (
50+ actions : & mut Vec < CodeAction > ,
51+ file : & ast:: SourceFile ,
52+ offset : TextSize ,
53+ ) -> Option < ( ) > {
54+ let string = file
55+ . syntax ( )
56+ . token_at_offset ( offset)
57+ . find ( |token| token. kind ( ) == SyntaxKind :: STRING ) ?;
58+
59+ let replacement = string_to_dollar_quoted ( string. text ( ) ) ?;
60+ actions. push ( CodeAction {
61+ title : "Rewrite as dollar-quoted string" . to_owned ( ) ,
62+ edits : vec ! [ Edit :: replace( string. text_range( ) , replacement) ] ,
63+ kind : ActionKind :: RefactorRewrite ,
64+ } ) ;
65+
66+ Some ( ( ) )
67+ }
68+
69+ fn string_to_dollar_quoted ( text : & str ) -> Option < String > {
70+ let normalized = normalize_single_quoted_string ( text) ?;
71+ let delimiter = dollar_delimiter ( & normalized) ?;
72+ let boundary = format ! ( "${}$" , delimiter) ;
73+ Some ( format ! ( "{boundary}{normalized}{boundary}" ) )
74+ }
75+
76+ fn dollar_quoted_to_string ( text : & str ) -> Option < String > {
77+ debug_assert ! ( text. starts_with( '$' ) ) ;
78+ let ( delimiter, content) = split_dollar_quoted ( text) ?;
79+ let boundary = format ! ( "${}$" , delimiter) ;
80+
81+ if !text. starts_with ( & boundary) || !text. ends_with ( & boundary) {
82+ return None ;
83+ }
84+
85+ // quotes are escaped by using two of them in Postgres
86+ let escaped = content. replace ( '\'' , "''" ) ;
87+ Some ( format ! ( "'{}'" , escaped) )
88+ }
89+
90+ fn split_dollar_quoted ( text : & str ) -> Option < ( String , & str ) > {
91+ debug_assert ! ( text. starts_with( '$' ) ) ;
92+ let second_dollar = text[ 1 ..] . find ( '$' ) ?;
93+ // the `foo` in `select $foo$bar$foo$`
94+ let delimiter = & text[ 1 ..=second_dollar] ;
95+ let boundary = format ! ( "${}$" , delimiter) ;
96+
97+ if !text. ends_with ( & boundary) {
98+ return None ;
99+ }
100+
101+ let start = boundary. len ( ) ;
102+ let end = text. len ( ) . checked_sub ( boundary. len ( ) ) ?;
103+ let content = text. get ( start..end) ?;
104+ Some ( ( delimiter. to_owned ( ) , content) )
105+ }
106+
107+ fn normalize_single_quoted_string ( text : & str ) -> Option < String > {
108+ let body = text. strip_prefix ( '\'' ) ?. strip_suffix ( '\'' ) ?;
109+ return Some ( body. replace ( "''" , "'" ) ) ;
110+ }
111+
112+ fn dollar_delimiter ( content : & str ) -> Option < String > {
113+ // We can't safely transform a trailing `$` i.e., `select 'foo $'` with an
114+ // empty delim, because we'll `select $$foo $$$` which isn't valid.
115+ if !content. contains ( "$$" ) && !content. ends_with ( '$' ) {
116+ return Some ( "" . to_owned ( ) ) ;
117+ }
118+
119+ let mut delim = "q" . to_owned ( ) ;
120+ // don't want to just loop forever
121+ for idx in 0 ..10 {
122+ if !content. contains ( & format ! ( "${}$" , delim) ) {
123+ return Some ( delim) ;
124+ }
125+ delim. push_str ( & idx. to_string ( ) ) ;
126+ }
127+ None
128+ }
129+
20130fn remove_else_clause (
21131 actions : & mut Vec < CodeAction > ,
22132 file : & ast:: SourceFile ,
@@ -40,6 +150,7 @@ fn remove_else_clause(
40150 actions. push ( CodeAction {
41151 title : "Remove `else` clause" . to_owned ( ) ,
42152 edits,
153+ kind : ActionKind :: RefactorRewrite ,
43154 } ) ;
44155 Some ( ( ) )
45156}
@@ -151,4 +262,91 @@ mod test {
151262 "select case x when true then 1 else 2 end$0;"
152263 ) ) ;
153264 }
265+
266+ #[ test]
267+ fn rewrite_string ( ) {
268+ assert_snapshot ! ( apply_code_action(
269+ rewrite_as_dollar_quoted_string,
270+ "select 'fo$0o';" ) ,
271+ @"select $$foo$$;"
272+ ) ;
273+ }
274+
275+ #[ test]
276+ fn rewrite_string_with_single_quote ( ) {
277+ assert_snapshot ! ( apply_code_action(
278+ rewrite_as_dollar_quoted_string,
279+ "select 'it''s$0 nice';" ) ,
280+ @"select $$it's nice$$;"
281+ ) ;
282+ }
283+
284+ #[ test]
285+ fn rewrite_string_with_dollar_signs ( ) {
286+ assert_snapshot ! ( apply_code_action(
287+ rewrite_as_dollar_quoted_string,
288+ "select 'foo $$ ba$0r';" ) ,
289+ @"select $q$foo $$ bar$q$;"
290+ ) ;
291+ }
292+
293+ #[ test]
294+ fn rewrite_string_when_trailing_dollar ( ) {
295+ assert_snapshot ! ( apply_code_action(
296+ rewrite_as_dollar_quoted_string,
297+ "select 'foo $'$0;" ) ,
298+ @"select $q$foo $$q$;"
299+ ) ;
300+ }
301+
302+ #[ test]
303+ fn rewrite_string_not_applicable ( ) {
304+ assert ! ( code_action_not_applicable(
305+ rewrite_as_dollar_quoted_string,
306+ "select 1 + $0 2;"
307+ ) ) ;
308+ }
309+
310+ #[ test]
311+ fn rewrite_prefix_string_not_applicable ( ) {
312+ assert ! ( code_action_not_applicable(
313+ rewrite_as_dollar_quoted_string,
314+ "select b'foo$0';"
315+ ) ) ;
316+ }
317+
318+ #[ test]
319+ fn rewrite_dollar_string ( ) {
320+ assert_snapshot ! ( apply_code_action(
321+ rewrite_as_regular_string,
322+ "select $$fo$0o$$;" ) ,
323+ @"select 'foo';"
324+ ) ;
325+ }
326+
327+ #[ test]
328+ fn rewrite_dollar_string_with_tag ( ) {
329+ assert_snapshot ! ( apply_code_action(
330+ rewrite_as_regular_string,
331+ "select $tag$fo$0o$tag$;" ) ,
332+ @"select 'foo';"
333+ ) ;
334+ }
335+
336+ #[ test]
337+ fn rewrite_dollar_string_with_quote ( ) {
338+ assert_snapshot ! ( apply_code_action(
339+ rewrite_as_regular_string,
340+ "select $$it'$0s fine$$;" ) ,
341+ @"select 'it''s fine';"
342+ ) ;
343+ }
344+
345+ #[ test]
346+ fn rewrite_dollar_string_not_applicable ( ) {
347+ assert ! ( code_action_not_applicable(
348+ rewrite_as_regular_string,
349+ "select 'foo$0';"
350+ ) ) ;
351+ }
154352}
0 commit comments