1use rowan::TextSize;
2use squawk_linter::Edit;
3use squawk_syntax::{
4 SyntaxKind, SyntaxNode,
5 ast::{self, AstNode},
6};
7
8use crate::{generated::keywords::RESERVED_KEYWORDS, offsets::token_from_offset};
9
10#[derive(Debug, Clone)]
11pub enum ActionKind {
12 QuickFix,
13 RefactorRewrite,
14}
15
16#[derive(Debug, Clone)]
17pub struct CodeAction {
18 pub title: String,
19 pub edits: Vec<Edit>,
20 pub kind: ActionKind,
21}
22
23pub fn code_actions(file: ast::SourceFile, offset: TextSize) -> Option<Vec<CodeAction>> {
24 let mut actions = vec![];
25 rewrite_as_regular_string(&mut actions, &file, offset);
26 rewrite_as_dollar_quoted_string(&mut actions, &file, offset);
27 remove_else_clause(&mut actions, &file, offset);
28 rewrite_table_as_select(&mut actions, &file, offset);
29 rewrite_select_as_table(&mut actions, &file, offset);
30 quote_identifier(&mut actions, &file, offset);
31 unquote_identifier(&mut actions, &file, offset);
32 Some(actions)
33}
34
35fn rewrite_as_regular_string(
36 actions: &mut Vec<CodeAction>,
37 file: &ast::SourceFile,
38 offset: TextSize,
39) -> Option<()> {
40 let dollar_string = file
41 .syntax()
42 .token_at_offset(offset)
43 .find(|token| token.kind() == SyntaxKind::DOLLAR_QUOTED_STRING)?;
44
45 let replacement = dollar_quoted_to_string(dollar_string.text())?;
46 actions.push(CodeAction {
47 title: "Rewrite as regular string".to_owned(),
48 edits: vec![Edit::replace(dollar_string.text_range(), replacement)],
49 kind: ActionKind::RefactorRewrite,
50 });
51
52 Some(())
53}
54
55fn rewrite_as_dollar_quoted_string(
56 actions: &mut Vec<CodeAction>,
57 file: &ast::SourceFile,
58 offset: TextSize,
59) -> Option<()> {
60 let string = file
61 .syntax()
62 .token_at_offset(offset)
63 .find(|token| token.kind() == SyntaxKind::STRING)?;
64
65 let replacement = string_to_dollar_quoted(string.text())?;
66 actions.push(CodeAction {
67 title: "Rewrite as dollar-quoted string".to_owned(),
68 edits: vec![Edit::replace(string.text_range(), replacement)],
69 kind: ActionKind::RefactorRewrite,
70 });
71
72 Some(())
73}
74
75fn string_to_dollar_quoted(text: &str) -> Option<String> {
76 let normalized = normalize_single_quoted_string(text)?;
77 let delimiter = dollar_delimiter(&normalized)?;
78 let boundary = format!("${}$", delimiter);
79 Some(format!("{boundary}{normalized}{boundary}"))
80}
81
82fn dollar_quoted_to_string(text: &str) -> Option<String> {
83 debug_assert!(text.starts_with('$'));
84 let (delimiter, content) = split_dollar_quoted(text)?;
85 let boundary = format!("${}$", delimiter);
86
87 if !text.starts_with(&boundary) || !text.ends_with(&boundary) {
88 return None;
89 }
90
91 let escaped = content.replace('\'', "''");
93 Some(format!("'{}'", escaped))
94}
95
96fn split_dollar_quoted(text: &str) -> Option<(String, &str)> {
97 debug_assert!(text.starts_with('$'));
98 let second_dollar = text[1..].find('$')?;
99 let delimiter = &text[1..=second_dollar];
101 let boundary = format!("${}$", delimiter);
102
103 if !text.ends_with(&boundary) {
104 return None;
105 }
106
107 let start = boundary.len();
108 let end = text.len().checked_sub(boundary.len())?;
109 let content = text.get(start..end)?;
110 Some((delimiter.to_owned(), content))
111}
112
113fn normalize_single_quoted_string(text: &str) -> Option<String> {
114 let body = text.strip_prefix('\'')?.strip_suffix('\'')?;
115 return Some(body.replace("''", "'"));
116}
117
118fn dollar_delimiter(content: &str) -> Option<String> {
119 if !content.contains("$$") && !content.ends_with('$') {
122 return Some("".to_owned());
123 }
124
125 let mut delim = "q".to_owned();
126 for idx in 0..10 {
128 if !content.contains(&format!("${}$", delim)) {
129 return Some(delim);
130 }
131 delim.push_str(&idx.to_string());
132 }
133 None
134}
135
136fn remove_else_clause(
137 actions: &mut Vec<CodeAction>,
138 file: &ast::SourceFile,
139 offset: TextSize,
140) -> Option<()> {
141 let else_token = file
142 .syntax()
143 .token_at_offset(offset)
144 .find(|x| x.kind() == SyntaxKind::ELSE_KW)?;
145 let parent = else_token.parent()?;
146 let else_clause = ast::ElseClause::cast(parent)?;
147
148 let mut edits = vec![];
149 edits.push(Edit::delete(else_clause.syntax().text_range()));
150 if let Some(token) = else_token.prev_token() {
151 if token.kind() == SyntaxKind::WHITESPACE {
152 edits.push(Edit::delete(token.text_range()));
153 }
154 }
155
156 actions.push(CodeAction {
157 title: "Remove `else` clause".to_owned(),
158 edits,
159 kind: ActionKind::RefactorRewrite,
160 });
161 Some(())
162}
163
164fn rewrite_table_as_select(
165 actions: &mut Vec<CodeAction>,
166 file: &ast::SourceFile,
167 offset: TextSize,
168) -> Option<()> {
169 let token = token_from_offset(file, offset)?;
170 let table = token.parent_ancestors().find_map(ast::Table::cast)?;
171
172 let relation_name = table.relation_name()?;
173 let table_name = relation_name.syntax().text();
174
175 let replacement = format!("select * from {}", table_name);
176
177 actions.push(CodeAction {
178 title: "Rewrite as `select`".to_owned(),
179 edits: vec![Edit::replace(table.syntax().text_range(), replacement)],
180 kind: ActionKind::RefactorRewrite,
181 });
182
183 Some(())
184}
185
186fn rewrite_select_as_table(
187 actions: &mut Vec<CodeAction>,
188 file: &ast::SourceFile,
189 offset: TextSize,
190) -> Option<()> {
191 let token = token_from_offset(file, offset)?;
192 let select = token.parent_ancestors().find_map(ast::Select::cast)?;
193
194 if !can_transform_select_to_table(&select) {
195 return None;
196 }
197
198 let from_clause = select.from_clause()?;
199 let from_item = from_clause.from_items().next()?;
200
201 let table_name = if let Some(name_ref) = from_item.name_ref() {
202 name_ref.syntax().text().to_string()
203 } else if let Some(field_expr) = from_item.field_expr() {
204 field_expr.syntax().text().to_string()
205 } else {
206 return None;
207 };
208
209 let replacement = format!("table {}", table_name);
210
211 actions.push(CodeAction {
212 title: "Rewrite as `table`".to_owned(),
213 edits: vec![Edit::replace(select.syntax().text_range(), replacement)],
214 kind: ActionKind::RefactorRewrite,
215 });
216
217 Some(())
218}
219
220fn can_transform_select_to_table(select: &ast::Select) -> bool {
227 if select.with_clause().is_some()
228 || select.where_clause().is_some()
229 || select.group_by_clause().is_some()
230 || select.having_clause().is_some()
231 || select.window_clause().is_some()
232 || select.order_by_clause().is_some()
233 || select.limit_clause().is_some()
234 || select.fetch_clause().is_some()
235 || select.offset_clause().is_some()
236 || select.filter_clause().is_some()
237 || select.locking_clauses().next().is_some()
238 {
239 return false;
240 }
241
242 let Some(select_clause) = select.select_clause() else {
243 return false;
244 };
245
246 if select_clause.distinct_clause().is_some() {
247 return false;
248 }
249
250 let Some(target_list) = select_clause.target_list() else {
251 return false;
252 };
253
254 let mut targets = target_list.targets();
255 let Some(target) = targets.next() else {
256 return false;
257 };
258
259 if targets.next().is_some() {
260 return false;
261 }
262
263 if target.expr().is_some() || target.star_token().is_none() {
265 return false;
266 }
267
268 let Some(from_clause) = select.from_clause() else {
269 return false;
270 };
271
272 let mut from_items = from_clause.from_items();
273 let Some(from_item) = from_items.next() else {
274 return false;
275 };
276
277 if from_items.next().is_some() || from_clause.join_exprs().next().is_some() {
279 return false;
280 }
281
282 if from_item.alias().is_some()
283 || from_item.tablesample_clause().is_some()
284 || from_item.only_token().is_some()
285 || from_item.lateral_token().is_some()
286 || from_item.star_token().is_some()
287 || from_item.call_expr().is_some()
288 || from_item.paren_select().is_some()
289 || from_item.json_table().is_some()
290 || from_item.xml_table().is_some()
291 || from_item.cast_expr().is_some()
292 {
293 return false;
294 }
295
296 from_item.name_ref().is_some() || from_item.field_expr().is_some()
298}
299
300fn quote_identifier(
301 actions: &mut Vec<CodeAction>,
302 file: &ast::SourceFile,
303 offset: TextSize,
304) -> Option<()> {
305 let token = token_from_offset(file, offset)?;
306 let parent = token.parent()?;
307
308 let name_node = if let Some(name) = ast::Name::cast(parent.clone()) {
309 name.syntax().clone()
310 } else if let Some(name_ref) = ast::NameRef::cast(parent) {
311 name_ref.syntax().clone()
312 } else {
313 return None;
314 };
315
316 let text = name_node.text().to_string();
317
318 if text.starts_with('"') {
319 return None;
320 }
321
322 let quoted = format!(r#""{}""#, text.to_lowercase());
323
324 actions.push(CodeAction {
325 title: "Quote identifier".to_owned(),
326 edits: vec![Edit::replace(name_node.text_range(), quoted)],
327 kind: ActionKind::RefactorRewrite,
328 });
329
330 Some(())
331}
332
333fn unquote_identifier(
334 actions: &mut Vec<CodeAction>,
335 file: &ast::SourceFile,
336 offset: TextSize,
337) -> Option<()> {
338 let token = token_from_offset(file, offset)?;
339 let parent = token.parent()?;
340
341 let name_node = if let Some(name) = ast::Name::cast(parent.clone()) {
342 name.syntax().clone()
343 } else if let Some(name_ref) = ast::NameRef::cast(parent) {
344 name_ref.syntax().clone()
345 } else {
346 return None;
347 };
348
349 let unquoted = unquote(&name_node)?;
350
351 actions.push(CodeAction {
352 title: "Unquote identifier".to_owned(),
353 edits: vec![Edit::replace(name_node.text_range(), unquoted)],
354 kind: ActionKind::RefactorRewrite,
355 });
356
357 Some(())
358}
359
360fn unquote(node: &SyntaxNode) -> Option<String> {
361 let text = node.text().to_string();
362
363 if !text.starts_with('"') || !text.ends_with('"') {
364 return None;
365 }
366
367 let text = &text[1..text.len() - 1];
368
369 if is_reserved_word(text) {
370 return None;
371 }
372
373 if text.is_empty() {
374 return None;
375 }
376
377 let mut chars = text.chars();
378
379 match chars.next() {
381 Some(c) if c.is_lowercase() || c == '_' => {}
382 _ => return None,
383 }
384
385 for c in chars {
386 if c.is_lowercase() || c.is_ascii_digit() || c == '_' || c == '$' {
387 continue;
388 }
389 return None;
390 }
391
392 Some(text.to_string())
393}
394
395fn is_reserved_word(text: &str) -> bool {
396 RESERVED_KEYWORDS
397 .binary_search(&text.to_lowercase().as_str())
398 .is_ok()
399}
400
401#[cfg(test)]
402mod test {
403 use super::*;
404 use crate::test_utils::fixture;
405 use insta::assert_snapshot;
406 use rowan::TextSize;
407 use squawk_syntax::ast;
408
409 fn apply_code_action(
410 f: impl Fn(&mut Vec<CodeAction>, &ast::SourceFile, TextSize) -> Option<()>,
411 sql: &str,
412 ) -> String {
413 let (mut offset, sql) = fixture(sql);
414 let parse = ast::SourceFile::parse(&sql);
415 assert_eq!(parse.errors(), vec![]);
416 let file: ast::SourceFile = parse.tree();
417
418 offset = offset.checked_sub(1.into()).unwrap_or_default();
419
420 let mut actions = vec![];
421 f(&mut actions, &file, offset);
422
423 assert!(
424 !actions.is_empty(),
425 "We should always have actions for `apply_code_action`. If you want to ensure there are no actions, use `code_action_not_applicable` instead."
426 );
427
428 let action = &actions[0];
429 let mut result = sql.clone();
430
431 let mut edits = action.edits.clone();
432 edits.sort_by_key(|e| e.text_range.start());
433 check_overlap(&edits);
434 edits.reverse();
435
436 for edit in edits {
437 let start: usize = edit.text_range.start().into();
438 let end: usize = edit.text_range.end().into();
439 let replacement = edit.text.as_deref().unwrap_or("");
440 result.replace_range(start..end, replacement);
441 }
442
443 let reparse = ast::SourceFile::parse(&result);
444 assert_eq!(
445 reparse.errors(),
446 vec![],
447 "Code actions shouldn't cause syntax errors"
448 );
449
450 result
451 }
452
453 fn check_overlap(edits: &[Edit]) {
458 for (edit_i, edit_j) in edits.iter().zip(edits.iter().skip(1)) {
459 if let Some(intersection) = edit_i.text_range.intersect(edit_j.text_range) {
460 assert!(
461 intersection.is_empty(),
462 "Edit ranges must not overlap: {:?} and {:?} intersect at {:?}",
463 edit_i.text_range,
464 edit_j.text_range,
465 intersection
466 );
467 }
468 }
469 }
470
471 fn code_action_not_applicable(
472 f: impl Fn(&mut Vec<CodeAction>, &ast::SourceFile, TextSize) -> Option<()>,
473 sql: &str,
474 ) -> bool {
475 let (offset, sql) = fixture(sql);
476 let parse = ast::SourceFile::parse(&sql);
477 assert_eq!(parse.errors(), vec![]);
478 let file: ast::SourceFile = parse.tree();
479
480 let mut actions = vec![];
481 f(&mut actions, &file, offset);
482 actions.is_empty()
483 }
484
485 #[test]
486 fn remove_else_clause_() {
487 assert_snapshot!(apply_code_action(
488 remove_else_clause,
489 "select case x when true then 1 else$0 2 end;"),
490 @"select case x when true then 1 end;"
491 );
492 }
493
494 #[test]
495 fn remove_else_clause_before_token() {
496 assert_snapshot!(apply_code_action(
497 remove_else_clause,
498 "select case x when true then 1 e$0lse 2 end;"),
499 @"select case x when true then 1 end;"
500 );
501 }
502
503 #[test]
504 fn remove_else_clause_not_applicable() {
505 assert!(code_action_not_applicable(
506 remove_else_clause,
507 "select case x when true then 1 else 2 end$0;"
508 ));
509 }
510
511 #[test]
512 fn rewrite_string() {
513 assert_snapshot!(apply_code_action(
514 rewrite_as_dollar_quoted_string,
515 "select 'fo$0o';"),
516 @"select $$foo$$;"
517 );
518 }
519
520 #[test]
521 fn rewrite_string_with_single_quote() {
522 assert_snapshot!(apply_code_action(
523 rewrite_as_dollar_quoted_string,
524 "select 'it''s$0 nice';"),
525 @"select $$it's nice$$;"
526 );
527 }
528
529 #[test]
530 fn rewrite_string_with_dollar_signs() {
531 assert_snapshot!(apply_code_action(
532 rewrite_as_dollar_quoted_string,
533 "select 'foo $$ ba$0r';"),
534 @"select $q$foo $$ bar$q$;"
535 );
536 }
537
538 #[test]
539 fn rewrite_string_when_trailing_dollar() {
540 assert_snapshot!(apply_code_action(
541 rewrite_as_dollar_quoted_string,
542 "select 'foo $'$0;"),
543 @"select $q$foo $$q$;"
544 );
545 }
546
547 #[test]
548 fn rewrite_string_not_applicable() {
549 assert!(code_action_not_applicable(
550 rewrite_as_dollar_quoted_string,
551 "select 1 + $0 2;"
552 ));
553 }
554
555 #[test]
556 fn rewrite_prefix_string_not_applicable() {
557 assert!(code_action_not_applicable(
558 rewrite_as_dollar_quoted_string,
559 "select b'foo$0';"
560 ));
561 }
562
563 #[test]
564 fn rewrite_dollar_string() {
565 assert_snapshot!(apply_code_action(
566 rewrite_as_regular_string,
567 "select $$fo$0o$$;"),
568 @"select 'foo';"
569 );
570 }
571
572 #[test]
573 fn rewrite_dollar_string_with_tag() {
574 assert_snapshot!(apply_code_action(
575 rewrite_as_regular_string,
576 "select $tag$fo$0o$tag$;"),
577 @"select 'foo';"
578 );
579 }
580
581 #[test]
582 fn rewrite_dollar_string_with_quote() {
583 assert_snapshot!(apply_code_action(
584 rewrite_as_regular_string,
585 "select $$it'$0s fine$$;"),
586 @"select 'it''s fine';"
587 );
588 }
589
590 #[test]
591 fn rewrite_dollar_string_not_applicable() {
592 assert!(code_action_not_applicable(
593 rewrite_as_regular_string,
594 "select 'foo$0';"
595 ));
596 }
597
598 #[test]
599 fn rewrite_table_as_select_simple() {
600 assert_snapshot!(apply_code_action(
601 rewrite_table_as_select,
602 "tab$0le foo;"),
603 @"select * from foo;"
604 );
605 }
606
607 #[test]
608 fn rewrite_table_as_select_qualified() {
609 assert_snapshot!(apply_code_action(
610 rewrite_table_as_select,
611 "ta$0ble schema.foo;"),
612 @"select * from schema.foo;"
613 );
614 }
615
616 #[test]
617 fn rewrite_table_as_select_after_keyword() {
618 assert_snapshot!(apply_code_action(
619 rewrite_table_as_select,
620 "table$0 bar;"),
621 @"select * from bar;"
622 );
623 }
624
625 #[test]
626 fn rewrite_table_as_select_on_table_name() {
627 assert_snapshot!(apply_code_action(
628 rewrite_table_as_select,
629 "table fo$0o;"),
630 @"select * from foo;"
631 );
632 }
633
634 #[test]
635 fn rewrite_table_as_select_not_applicable() {
636 assert!(code_action_not_applicable(
637 rewrite_table_as_select,
638 "select * from foo$0;"
639 ));
640 }
641
642 #[test]
643 fn rewrite_select_as_table_simple() {
644 assert_snapshot!(apply_code_action(
645 rewrite_select_as_table,
646 "sel$0ect * from foo;"),
647 @"table foo;"
648 );
649 }
650
651 #[test]
652 fn rewrite_select_as_table_qualified() {
653 assert_snapshot!(apply_code_action(
654 rewrite_select_as_table,
655 "select * from sch$0ema.foo;"),
656 @"table schema.foo;"
657 );
658 }
659
660 #[test]
661 fn rewrite_select_as_table_on_star() {
662 assert_snapshot!(apply_code_action(
663 rewrite_select_as_table,
664 "select $0* from bar;"),
665 @"table bar;"
666 );
667 }
668
669 #[test]
670 fn rewrite_select_as_table_on_from() {
671 assert_snapshot!(apply_code_action(
672 rewrite_select_as_table,
673 "select * fr$0om baz;"),
674 @"table baz;"
675 );
676 }
677
678 #[test]
679 fn rewrite_select_as_table_not_applicable_with_where() {
680 assert!(code_action_not_applicable(
681 rewrite_select_as_table,
682 "select * from foo$0 where x = 1;"
683 ));
684 }
685
686 #[test]
687 fn rewrite_select_as_table_not_applicable_with_order_by() {
688 assert!(code_action_not_applicable(
689 rewrite_select_as_table,
690 "select * from foo$0 order by x;"
691 ));
692 }
693
694 #[test]
695 fn rewrite_select_as_table_not_applicable_with_limit() {
696 assert!(code_action_not_applicable(
697 rewrite_select_as_table,
698 "select * from foo$0 limit 10;"
699 ));
700 }
701
702 #[test]
703 fn rewrite_select_as_table_not_applicable_with_distinct() {
704 assert!(code_action_not_applicable(
705 rewrite_select_as_table,
706 "select distinct * from foo$0;"
707 ));
708 }
709
710 #[test]
711 fn rewrite_select_as_table_not_applicable_with_columns() {
712 assert!(code_action_not_applicable(
713 rewrite_select_as_table,
714 "select id, name from foo$0;"
715 ));
716 }
717
718 #[test]
719 fn rewrite_select_as_table_not_applicable_with_join() {
720 assert!(code_action_not_applicable(
721 rewrite_select_as_table,
722 "select * from foo$0 join bar on foo.id = bar.id;"
723 ));
724 }
725
726 #[test]
727 fn rewrite_select_as_table_not_applicable_with_alias() {
728 assert!(code_action_not_applicable(
729 rewrite_select_as_table,
730 "select * from foo$0 f;"
731 ));
732 }
733
734 #[test]
735 fn rewrite_select_as_table_not_applicable_with_multiple_tables() {
736 assert!(code_action_not_applicable(
737 rewrite_select_as_table,
738 "select * from foo$0, bar;"
739 ));
740 }
741
742 #[test]
743 fn rewrite_select_as_table_not_applicable_on_table() {
744 assert!(code_action_not_applicable(
745 rewrite_select_as_table,
746 "table foo$0;"
747 ));
748 }
749
750 #[test]
751 fn quote_identifier_on_name_ref() {
752 assert_snapshot!(apply_code_action(
753 quote_identifier,
754 "select x$0 from t;"),
755 @r#"select "x" from t;"#
756 );
757 }
758
759 #[test]
760 fn quote_identifier_on_name() {
761 assert_snapshot!(apply_code_action(
762 quote_identifier,
763 "create table T(X$0 int);"),
764 @r#"create table T("x" int);"#
765 );
766 }
767
768 #[test]
769 fn quote_identifier_lowercases() {
770 assert_snapshot!(apply_code_action(
771 quote_identifier,
772 "create table T(COL$0 int);"),
773 @r#"create table T("col" int);"#
774 );
775 }
776
777 #[test]
778 fn quote_identifier_not_applicable_when_already_quoted() {
779 assert!(code_action_not_applicable(
780 quote_identifier,
781 r#"select "x"$0 from t;"#
782 ));
783 }
784
785 #[test]
786 fn quote_identifier_not_applicable_on_select_keyword() {
787 assert!(code_action_not_applicable(
788 quote_identifier,
789 "sel$0ect x from t;"
790 ));
791 }
792
793 #[test]
794 fn quote_identifier_on_keyword_column_name() {
795 assert_snapshot!(apply_code_action(
796 quote_identifier,
797 "select te$0xt from t;"),
798 @r#"select "text" from t;"#
799 );
800 }
801
802 #[test]
803 fn quote_identifier_example_select() {
804 assert_snapshot!(apply_code_action(
805 quote_identifier,
806 "select x$0 from t;"),
807 @r#"select "x" from t;"#
808 );
809 }
810
811 #[test]
812 fn quote_identifier_example_create_table() {
813 assert_snapshot!(apply_code_action(
814 quote_identifier,
815 "create table T(X$0 int);"),
816 @r#"create table T("x" int);"#
817 );
818 }
819
820 #[test]
821 fn unquote_identifier_simple() {
822 assert_snapshot!(apply_code_action(
823 unquote_identifier,
824 r#"select "x"$0 from t;"#),
825 @"select x from t;"
826 );
827 }
828
829 #[test]
830 fn unquote_identifier_with_underscore() {
831 assert_snapshot!(apply_code_action(
832 unquote_identifier,
833 r#"select "user_id"$0 from t;"#),
834 @"select user_id from t;"
835 );
836 }
837
838 #[test]
839 fn unquote_identifier_with_digits() {
840 assert_snapshot!(apply_code_action(
841 unquote_identifier,
842 r#"select "x123"$0 from t;"#),
843 @"select x123 from t;"
844 );
845 }
846
847 #[test]
848 fn unquote_identifier_with_dollar() {
849 assert_snapshot!(apply_code_action(
850 unquote_identifier,
851 r#"select "my_table$1"$0 from t;"#),
852 @"select my_table$1 from t;"
853 );
854 }
855
856 #[test]
857 fn unquote_identifier_starts_with_underscore() {
858 assert_snapshot!(apply_code_action(
859 unquote_identifier,
860 r#"select "_col"$0 from t;"#),
861 @"select _col from t;"
862 );
863 }
864
865 #[test]
866 fn unquote_identifier_starts_with_unicode() {
867 assert_snapshot!(apply_code_action(
868 unquote_identifier,
869 r#"select "é"$0 from t;"#),
870 @"select é from t;"
871 );
872 }
873
874 #[test]
875 fn unquote_identifier_not_applicable() {
876 assert!(code_action_not_applicable(
878 unquote_identifier,
879 r#"select "X"$0 from t;"#
880 ));
881 assert!(code_action_not_applicable(
883 unquote_identifier,
884 r#"select "Foo"$0 from t;"#
885 ));
886 assert!(code_action_not_applicable(
888 unquote_identifier,
889 r#"select "my-col"$0 from t;"#
890 ));
891 assert!(code_action_not_applicable(
893 unquote_identifier,
894 r#"select "123"$0 from t;"#
895 ));
896 assert!(code_action_not_applicable(
898 unquote_identifier,
899 r#"select "foo bar"$0 from t;"#
900 ));
901 assert!(code_action_not_applicable(
903 unquote_identifier,
904 r#"select "foo""bar"$0 from t;"#
905 ));
906 assert!(code_action_not_applicable(
908 unquote_identifier,
909 "select x$0 from t;"
910 ));
911 assert!(code_action_not_applicable(
913 unquote_identifier,
914 r#"select "my[col]"$0 from t;"#
915 ));
916 assert!(code_action_not_applicable(
918 unquote_identifier,
919 r#"select "my{}"$0 from t;"#
920 ));
921 assert!(code_action_not_applicable(
923 unquote_identifier,
924 r#"select "select"$0 from t;"#
925 ));
926 }
927
928 #[test]
929 fn unquote_identifier_on_name() {
930 assert_snapshot!(apply_code_action(
931 unquote_identifier,
932 r#"create table T("x"$0 int);"#),
933 @"create table T(x int);"
934 );
935 }
936}