1use lsp_types::{
11 CompletionItem, CompletionItemKind, Diagnostic, DiagnosticSeverity, DocumentSymbol,
12 FoldingRange, FoldingRangeKind, Hover, HoverContents, InsertTextFormat, MarkupContent,
13 MarkupKind, Position, Range, SemanticToken, SemanticTokenModifier, SemanticTokenType,
14 SymbolKind, TextEdit,
15};
16use sql_dialect_fmt_formatter::{format, FormatOptions};
17use sql_dialect_fmt_highlight::semantic;
18use sql_dialect_fmt_parser::{SyntaxKind, SyntaxNode};
19use sql_dialect_fmt_syntax::{keyword_texts, BUILTIN_TYPE_WORDS};
20use sql_dialect_fmt_text::{LineIndex, Utf16Position, Utf8Position};
21
22mod lint;
23
24pub use lint::{diagnostic_lint_code, LintCode, LintOptions};
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum PositionEncoding {
29 Utf16,
31 Utf8,
33}
34
35pub fn token_types() -> Vec<SemanticTokenType> {
41 semantic::SemanticTokenType::LEGEND
42 .iter()
43 .map(|ty| SemanticTokenType::new(ty.name()))
44 .collect()
45}
46
47pub fn token_modifiers() -> Vec<SemanticTokenModifier> {
51 semantic::SemanticTokenModifiers::LEGEND
52 .iter()
53 .map(|&name| SemanticTokenModifier::new(name))
54 .collect()
55}
56
57fn lsp_position(index: &LineIndex<'_>, offset: usize, encoding: PositionEncoding) -> Position {
58 match encoding {
59 PositionEncoding::Utf16 => {
60 let position = index.utf16_position(offset);
61 Position::new(position.line, position.character)
62 }
63 PositionEncoding::Utf8 => {
64 let position = index.utf8_position(offset);
65 Position::new(position.line, position.character)
66 }
67 }
68}
69
70fn lsp_end_position(index: &LineIndex<'_>, encoding: PositionEncoding) -> Position {
71 match encoding {
72 PositionEncoding::Utf16 => {
73 let position = index.end_utf16_position();
74 Position::new(position.line, position.character)
75 }
76 PositionEncoding::Utf8 => {
77 let position = index.end_utf8_position();
78 Position::new(position.line, position.character)
79 }
80 }
81}
82
83fn lsp_offset(index: &LineIndex<'_>, position: Position, encoding: PositionEncoding) -> usize {
84 match encoding {
85 PositionEncoding::Utf16 => {
86 index.offset_for_utf16_position(Utf16Position::new(position.line, position.character))
87 }
88 PositionEncoding::Utf8 => {
89 index.offset_for_utf8_position(Utf8Position::new(position.line, position.character))
90 }
91 }
92}
93
94pub fn format_edits(text: &str, options: &FormatOptions) -> Vec<TextEdit> {
97 format_edits_with_encoding(text, options, PositionEncoding::Utf16)
98}
99
100pub fn format_edits_with_encoding(
102 text: &str,
103 options: &FormatOptions,
104 encoding: PositionEncoding,
105) -> Vec<TextEdit> {
106 let formatted = format(text, options);
107 if formatted == text {
108 return Vec::new();
109 }
110 let index = LineIndex::new(text);
111 vec![TextEdit {
112 range: Range::new(Position::new(0, 0), lsp_end_position(&index, encoding)),
113 new_text: formatted,
114 }]
115}
116
117pub fn format_range_edits(text: &str, range: Range, options: &FormatOptions) -> Vec<TextEdit> {
120 format_range_edits_with_encoding(text, range, options, PositionEncoding::Utf16)
121}
122
123pub fn format_range_edits_with_encoding(
125 text: &str,
126 range: Range,
127 options: &FormatOptions,
128 encoding: PositionEncoding,
129) -> Vec<TextEdit> {
130 let index = LineIndex::new(text);
131 let a = lsp_offset(&index, range.start, encoding);
132 let b = lsp_offset(&index, range.end, encoding);
133 let target = a.min(b)..a.max(b);
134 let Some(edit) = sql_dialect_fmt_formatter::format_range(text, target, options) else {
135 return Vec::new();
136 };
137 vec![TextEdit {
138 range: Range::new(
139 lsp_position(&index, edit.range.start, encoding),
140 lsp_position(&index, edit.range.end, encoding),
141 ),
142 new_text: edit.new_text,
143 }]
144}
145
146pub fn on_type_formatting_edits(
149 text: &str,
150 position: Position,
151 options: &FormatOptions,
152) -> Vec<TextEdit> {
153 on_type_formatting_edits_with_encoding(text, position, options, PositionEncoding::Utf16)
154}
155
156pub fn on_type_formatting_edits_with_encoding(
163 text: &str,
164 position: Position,
165 options: &FormatOptions,
166 encoding: PositionEncoding,
167) -> Vec<TextEdit> {
168 let index = LineIndex::new(text);
169 let cursor = lsp_offset(&index, position, encoding);
170 let Some(last) = text[..cursor].rfind(|c: char| !c.is_whitespace()) else {
171 return Vec::new();
172 };
173 let Some(edit) = sql_dialect_fmt_formatter::format_range(text, last..cursor, options) else {
176 return Vec::new();
177 };
178 vec![TextEdit {
179 range: Range::new(
180 lsp_position(&index, edit.range.start, encoding),
181 lsp_position(&index, edit.range.end, encoding),
182 ),
183 new_text: edit.new_text,
184 }]
185}
186
187pub fn diagnostics(text: &str) -> Vec<Diagnostic> {
196 diagnostics_with_options(text, &FormatOptions::default(), PositionEncoding::Utf16)
197}
198
199pub fn diagnostics_with_encoding(text: &str, encoding: PositionEncoding) -> Vec<Diagnostic> {
201 diagnostics_with_options(text, &FormatOptions::default(), encoding)
202}
203
204pub fn diagnostics_with_options(
206 text: &str,
207 options: &FormatOptions,
208 encoding: PositionEncoding,
209) -> Vec<Diagnostic> {
210 diagnostics_with_lint_options(text, options, LintOptions::default(), encoding)
211}
212
213pub fn diagnostics_with_lint_options(
215 text: &str,
216 options: &FormatOptions,
217 lint_options: LintOptions,
218 encoding: PositionEncoding,
219) -> Vec<Diagnostic> {
220 let index = LineIndex::new(text);
221 let to_range = |span: std::ops::Range<usize>| {
222 Range::new(
223 lsp_position(&index, span.start, encoding),
224 lsp_position(&index, span.end, encoding),
225 )
226 };
227 let make = |range: Range, message: String| Diagnostic {
228 range,
229 severity: Some(DiagnosticSeverity::ERROR),
230 source: Some("sql-dialect-fmt".to_string()),
231 message,
232 ..Default::default()
233 };
234
235 let lexed = sql_dialect_fmt_lexer::tokenize_for_dialect(text, options.dialect);
236 let lex_errors = lexed.errors.clone();
237 let parse = sql_dialect_fmt_parser::parse_lexed(text, options.dialect, lexed);
238 let mut diagnostics: Vec<_> = lex_errors
239 .iter()
240 .map(|err| make(to_range(err.range()), err.message.clone()))
241 .chain(
242 parse
243 .errors()
244 .iter()
245 .map(|err| make(to_range(err.range()), err.message.clone())),
246 )
247 .collect();
248 diagnostics.extend(lint::diagnostics_with_encoding(
249 text,
250 options.dialect,
251 &index,
252 lint_options,
253 encoding,
254 ));
255 diagnostics
256}
257
258pub fn hover(text: &str, position: Position) -> Option<Hover> {
261 hover_with_encoding(text, position, PositionEncoding::Utf16)
262}
263
264pub fn hover_with_encoding(
266 text: &str,
267 position: Position,
268 encoding: PositionEncoding,
269) -> Option<Hover> {
270 let index = LineIndex::new(text);
271 let info = sql_dialect_fmt_hover::hover_at(text, lsp_offset(&index, position, encoding))?;
272 let mut value = format!("**{}**\n\n{}", info.title, info.body);
273 if let Some(url) = info.docs_url {
274 value.push_str(&format!("\n\n[Snowflake docs]({url})"));
275 }
276 Some(Hover {
277 contents: HoverContents::Markup(MarkupContent {
278 kind: MarkupKind::Markdown,
279 value,
280 }),
281 range: Some(Range::new(
282 lsp_position(&index, info.range.start, encoding),
283 lsp_position(&index, info.range.end, encoding),
284 )),
285 })
286}
287
288pub fn document_symbols(text: &str, options: &FormatOptions) -> Vec<DocumentSymbol> {
290 document_symbols_with_encoding(text, options, PositionEncoding::Utf16)
291}
292
293pub fn document_symbols_with_encoding(
295 text: &str,
296 options: &FormatOptions,
297 encoding: PositionEncoding,
298) -> Vec<DocumentSymbol> {
299 let index = LineIndex::new(text);
300 let root = sql_dialect_fmt_parser::parse_with_dialect(text, options.dialect).syntax();
301 root.children()
302 .filter_map(|stmt| document_symbol_for_statement(&stmt, &index, encoding))
303 .collect()
304}
305
306#[derive(Clone, Debug)]
307struct SymbolToken {
308 kind: SyntaxKind,
309 text: String,
310 range: std::ops::Range<usize>,
311}
312
313fn document_symbol_for_statement(
314 stmt: &SyntaxNode,
315 index: &LineIndex<'_>,
316 encoding: PositionEncoding,
317) -> Option<DocumentSymbol> {
318 let tokens = significant_symbol_tokens(stmt);
319 let first = tokens.first()?;
320 let last = tokens.last().unwrap_or(first);
321 let range = byte_range_to_lsp(index, first.range.start..last.range.end, encoding);
322 let selection_range = statement_selection_range(&tokens)
323 .map(|range| byte_range_to_lsp(index, range, encoding))
324 .unwrap_or(range);
325 let name = statement_symbol_name(stmt.kind(), &tokens);
326 let kind = statement_symbol_kind(stmt.kind(), &tokens);
327 #[allow(deprecated)]
328 let symbol = DocumentSymbol {
329 name,
330 detail: None,
331 kind,
332 tags: None,
333 deprecated: None,
334 range,
335 selection_range,
336 children: None,
337 };
338 Some(symbol)
339}
340
341fn significant_symbol_tokens(node: &SyntaxNode) -> Vec<SymbolToken> {
342 node.descendants_with_tokens()
343 .filter_map(|element| element.into_token())
344 .filter(|token| !token.kind().is_trivia())
345 .map(|token| {
346 let range = token.text_range();
347 let start: usize = range.start().into();
348 let end: usize = range.end().into();
349 SymbolToken {
350 kind: token.kind(),
351 text: token.text().to_string(),
352 range: start..end,
353 }
354 })
355 .collect()
356}
357
358fn byte_range_to_lsp(
359 index: &LineIndex<'_>,
360 range: std::ops::Range<usize>,
361 encoding: PositionEncoding,
362) -> Range {
363 Range::new(
364 lsp_position(index, range.start, encoding),
365 lsp_position(index, range.end, encoding),
366 )
367}
368
369fn statement_selection_range(tokens: &[SymbolToken]) -> Option<std::ops::Range<usize>> {
370 create_name_range(tokens).or_else(|| tokens.first().map(|token| token.range.clone()))
371}
372
373fn statement_symbol_name(kind: SyntaxKind, tokens: &[SymbolToken]) -> String {
374 if let Some(label) = create_statement_label(tokens) {
375 return label;
376 }
377
378 match kind {
379 SyntaxKind::SELECT_STMT => "SELECT".to_string(),
380 SyntaxKind::WITH_QUERY => "WITH".to_string(),
381 SyntaxKind::INSERT_STMT => statement_words_until(tokens, 4, &["VALUES", "SELECT"]),
382 SyntaxKind::UPDATE_STMT => statement_words_until(tokens, 3, &["SET"]),
383 SyntaxKind::DELETE_STMT => statement_words_until(tokens, 4, &["WHERE", "USING"]),
384 SyntaxKind::MERGE_STMT => statement_words_until(tokens, 4, &["USING"]),
385 SyntaxKind::COPY_STMT => statement_words_until(tokens, 4, &["FROM", "FILES"]),
386 SyntaxKind::STAGE_FILE_STMT => statement_words_until(tokens, 1, &[]),
387 SyntaxKind::CALL_STMT => statement_words_until(tokens, 3, &["("]),
388 SyntaxKind::BLOCK_STMT => "BEGIN".to_string(),
389 SyntaxKind::SET_OP | SyntaxKind::FLOW_STMT => statement_words_until(tokens, 3, &[]),
390 _ => statement_words_until(tokens, 4, &[";", "AS", "WHERE"]),
391 }
392}
393
394fn statement_symbol_kind(kind: SyntaxKind, tokens: &[SymbolToken]) -> SymbolKind {
395 if let Some((object_kind, _)) = create_object_kind(tokens) {
396 return match object_kind {
397 "TABLE" | "DYNAMIC TABLE" => SymbolKind::STRUCT,
398 "VIEW" | "SEMANTIC VIEW" => SymbolKind::INTERFACE,
399 "FUNCTION" => SymbolKind::FUNCTION,
400 "PROCEDURE" => SymbolKind::METHOD,
401 "TASK" => SymbolKind::EVENT,
402 "WAREHOUSE" | "STAGE" | "FILE FORMAT" | "STREAM" | "SEQUENCE" => SymbolKind::OBJECT,
403 "SCHEMA" | "DATABASE" => SymbolKind::NAMESPACE,
404 _ => SymbolKind::OBJECT,
405 };
406 }
407
408 match kind {
409 SyntaxKind::SELECT_STMT | SyntaxKind::WITH_QUERY | SyntaxKind::SET_OP => {
410 SymbolKind::FUNCTION
411 }
412 SyntaxKind::INSERT_STMT
413 | SyntaxKind::UPDATE_STMT
414 | SyntaxKind::DELETE_STMT
415 | SyntaxKind::MERGE_STMT
416 | SyntaxKind::COPY_STMT
417 | SyntaxKind::STAGE_FILE_STMT
418 | SyntaxKind::CALL_STMT
419 | SyntaxKind::SET_STMT
420 | SyntaxKind::EXECUTE_STMT => SymbolKind::METHOD,
421 SyntaxKind::BLOCK_STMT | SyntaxKind::IF_STMT | SyntaxKind::LOOP_STMT => SymbolKind::MODULE,
422 _ => SymbolKind::OBJECT,
423 }
424}
425
426fn create_statement_label(tokens: &[SymbolToken]) -> Option<String> {
427 let (object_kind, name_start) = create_object_kind(tokens)?;
428 let name = dotted_name(tokens, skip_if_not_exists(tokens, name_start));
429 Some(match name {
430 Some(name) => format!("CREATE {object_kind} {name}"),
431 None => format!("CREATE {object_kind}"),
432 })
433}
434
435fn create_name_range(tokens: &[SymbolToken]) -> Option<std::ops::Range<usize>> {
436 let (_, name_start) = create_object_kind(tokens)?;
437 tokens
438 .iter()
439 .skip(skip_if_not_exists(tokens, name_start))
440 .find(|token| is_name_token(token))
441 .map(|token| token.range.clone())
442}
443
444fn create_object_kind(tokens: &[SymbolToken]) -> Option<(&'static str, usize)> {
445 let create_index = tokens.iter().position(|token| word(token, "CREATE"))?;
446 for index in create_index + 1..tokens.len() {
447 let token = &tokens[index];
448 if word(token, "DYNAMIC") && tokens.get(index + 1).is_some_and(|t| word(t, "TABLE")) {
449 return Some(("DYNAMIC TABLE", index + 2));
450 }
451 if word(token, "SEMANTIC") && tokens.get(index + 1).is_some_and(|t| word(t, "VIEW")) {
452 return Some(("SEMANTIC VIEW", index + 2));
453 }
454 if word(token, "FILE") && tokens.get(index + 1).is_some_and(|t| word(t, "FORMAT")) {
455 return Some(("FILE FORMAT", index + 2));
456 }
457 if word(token, "MASKING") && tokens.get(index + 1).is_some_and(|t| word(t, "POLICY")) {
458 return Some(("MASKING POLICY", index + 2));
459 }
460 if word(token, "ACCESS") && tokens.get(index + 1).is_some_and(|t| word(t, "POLICY")) {
461 return Some(("ACCESS POLICY", index + 2));
462 }
463
464 for candidate in [
465 "TABLE",
466 "VIEW",
467 "FUNCTION",
468 "PROCEDURE",
469 "TASK",
470 "WAREHOUSE",
471 "STAGE",
472 "SCHEMA",
473 "DATABASE",
474 "SEQUENCE",
475 "STREAM",
476 ] {
477 if word(token, candidate) {
478 return Some((candidate, index + 1));
479 }
480 }
481 }
482 None
483}
484
485fn skip_if_not_exists(tokens: &[SymbolToken], index: usize) -> usize {
486 if tokens.get(index).is_some_and(|token| word(token, "IF"))
487 && tokens
488 .get(index + 1)
489 .is_some_and(|token| word(token, "NOT"))
490 && tokens
491 .get(index + 2)
492 .is_some_and(|token| word(token, "EXISTS"))
493 {
494 index + 3
495 } else {
496 index
497 }
498}
499
500fn dotted_name(tokens: &[SymbolToken], start: usize) -> Option<String> {
501 let mut out = String::new();
502 let mut saw_name = false;
503 let mut allow_dot = false;
504
505 for token in tokens.iter().skip(start) {
506 if token.text == "." && allow_dot {
507 out.push('.');
508 allow_dot = false;
509 continue;
510 }
511 if is_name_token(token) {
512 out.push_str(&token.text);
513 saw_name = true;
514 allow_dot = true;
515 continue;
516 }
517 break;
518 }
519
520 while out.ends_with('.') {
521 out.pop();
522 }
523 saw_name.then_some(out)
524}
525
526fn statement_words_until(tokens: &[SymbolToken], max_words: usize, stops: &[&str]) -> String {
527 let mut words = Vec::new();
528 for token in tokens.iter().filter(|token| symbol_word(token)) {
529 if !words.is_empty() && stops.iter().any(|stop| word(token, stop)) {
530 break;
531 }
532 words.push(display_word(token));
533 if words.len() == max_words {
534 break;
535 }
536 }
537 if words.is_empty() {
538 "SQL".to_string()
539 } else {
540 words.join(" ")
541 }
542}
543
544fn display_word(token: &SymbolToken) -> String {
545 if token.kind.is_keyword() || token.kind == SyntaxKind::CONTEXTUAL_KEYWORD {
546 token.text.to_ascii_uppercase()
547 } else {
548 token.text.clone()
549 }
550}
551
552fn symbol_word(token: &SymbolToken) -> bool {
553 token.kind.is_keyword()
554 || matches!(
555 token.kind,
556 SyntaxKind::IDENT | SyntaxKind::QUOTED_IDENT | SyntaxKind::CONTEXTUAL_KEYWORD
557 )
558}
559
560fn is_name_token(token: &SymbolToken) -> bool {
561 matches!(
562 token.kind,
563 SyntaxKind::IDENT | SyntaxKind::QUOTED_IDENT | SyntaxKind::CONTEXTUAL_KEYWORD
564 )
565}
566
567fn word(token: &SymbolToken, expected: &str) -> bool {
568 symbol_word(token) && token.text.eq_ignore_ascii_case(expected)
569}
570
571pub fn completion_items() -> Vec<CompletionItem> {
573 let mut items = Vec::new();
574 items.extend(keyword_texts().map(|keyword| {
575 let label = keyword.to_ascii_uppercase();
576 completion_item(
577 &label,
578 CompletionItemKind::KEYWORD,
579 "SQL keyword",
580 "Keyword recognized by sql-dialect-fmt.",
581 None,
582 "1",
583 )
584 }));
585 items.extend(BUILTIN_TYPE_WORDS.iter().map(|label| {
586 completion_item(
587 label,
588 CompletionItemKind::TYPE_PARAMETER,
589 "SQL type",
590 type_documentation(label),
591 None,
592 "2",
593 )
594 }));
595 items.extend(SQL_SNIPPETS.iter().map(|snippet| {
596 completion_item(
597 snippet.label,
598 CompletionItemKind::SNIPPET,
599 "SQL snippet",
600 snippet.documentation,
601 Some(snippet.insert_text),
602 "3",
603 )
604 }));
605 items
606}
607
608fn type_documentation(label: &str) -> &'static str {
609 match label {
610 "NUMBER" => "Exact fixed-point numeric type.",
611 "DECIMAL" | "NUMERIC" => "Alias for NUMBER.",
612 "INT" | "INTEGER" | "BIGINT" => "Integer numeric alias.",
613 "FLOAT" | "DOUBLE" | "REAL" => "Approximate floating-point numeric type.",
614 "VARCHAR" => "Variable-length character data.",
615 "STRING" | "TEXT" => "Alias for VARCHAR.",
616 "CHAR" => "Character data.",
617 "BOOLEAN" => "TRUE/FALSE logical value.",
618 "VARIANT" => "Semi-structured value.",
619 "OBJECT" => "Semi-structured key/value object.",
620 "ARRAY" => "Semi-structured ordered collection.",
621 "MAP" => "Structured key/value collection.",
622 "VECTOR" => "Vector type for embeddings.",
623 "DATE" => "Calendar date without time of day.",
624 "TIME" => "Time of day without date.",
625 "TIMESTAMP" => "Timestamp family.",
626 "TIMESTAMP_NTZ" => "Timestamp without time zone.",
627 "TIMESTAMP_LTZ" => "Timestamp using the session time zone.",
628 "TIMESTAMP_TZ" => "Timestamp with explicit offset.",
629 "BINARY" => "Variable-length binary data.",
630 "GEOGRAPHY" => "Spherical geospatial data.",
631 "GEOMETRY" => "Planar geospatial data.",
632 _ => "Built-in SQL type.",
633 }
634}
635
636fn completion_item(
637 label: &str,
638 kind: CompletionItemKind,
639 detail: &str,
640 documentation: &str,
641 insert_text: Option<&str>,
642 sort_prefix: &str,
643) -> CompletionItem {
644 CompletionItem {
645 label: label.to_string(),
646 kind: Some(kind),
647 detail: Some(detail.to_string()),
648 documentation: Some(lsp_types::Documentation::MarkupContent(MarkupContent {
649 kind: MarkupKind::Markdown,
650 value: documentation.to_string(),
651 })),
652 insert_text: insert_text.map(str::to_string),
653 insert_text_format: insert_text.map(|_| InsertTextFormat::SNIPPET),
654 sort_text: Some(format!("{sort_prefix}-{label}")),
655 ..CompletionItem::default()
656 }
657}
658
659struct Snippet {
660 label: &'static str,
661 insert_text: &'static str,
662 documentation: &'static str,
663}
664
665const SQL_SNIPPETS: &[Snippet] = &[
666 Snippet {
667 label: "SELECT ... FROM ...",
668 insert_text: "SELECT ${1:columns}\nFROM ${2:table};",
669 documentation: "Scaffold a SELECT statement.",
670 },
671 Snippet {
672 label: "WITH ... AS (...)",
673 insert_text: "WITH ${1:cte} AS (\n SELECT ${2:*}\n FROM ${3:source}\n)\nSELECT ${4:*}\nFROM ${1:cte};",
674 documentation: "Scaffold a common table expression.",
675 },
676 Snippet {
677 label: "CREATE TABLE ...",
678 insert_text: "CREATE TABLE ${1:table_name} (\n ${2:column_name} ${3:NUMBER}\n);",
679 documentation: "Scaffold a CREATE TABLE statement.",
680 },
681 Snippet {
682 label: "INSERT INTO ...",
683 insert_text: "INSERT INTO ${1:table_name} (${2:columns})\nVALUES (${3:values});",
684 documentation: "Scaffold an INSERT statement.",
685 },
686 Snippet {
687 label: "CREATE PROCEDURE ...",
688 insert_text: "CREATE PROCEDURE ${1:procedure_name}(${2:args})\nRETURNS ${3:STRING}\nLANGUAGE SQL\nAS\n$$\nBEGIN\n ${4:RETURN 'ok';}\nEND;\n$$;",
689 documentation: "Scaffold a Snowflake SQL procedure.",
690 },
691];
692
693pub fn folding_ranges(text: &str) -> Vec<FoldingRange> {
696 let index = LineIndex::new(text);
697 let root = sql_dialect_fmt_parser::parse(text).syntax();
698 root.children()
699 .filter_map(|stmt| {
700 let mut tokens = stmt
703 .descendants_with_tokens()
704 .filter_map(|el| el.into_token())
705 .filter(|t| !t.kind().is_trivia());
706 let first = tokens.next()?;
707 let last = tokens.last().unwrap_or_else(|| first.clone());
708 let start = lsp_position(
709 &index,
710 first.text_range().start().into(),
711 PositionEncoding::Utf16,
712 )
713 .line;
714 let end = lsp_position(
715 &index,
716 last.text_range().end().into(),
717 PositionEncoding::Utf16,
718 )
719 .line;
720 (end > start).then_some(FoldingRange {
721 start_line: start,
722 end_line: end,
723 kind: Some(FoldingRangeKind::Region),
724 ..FoldingRange::default()
725 })
726 })
727 .collect()
728}
729
730pub fn apply_change(text: &str, range: Option<Range>, new_text: &str) -> String {
736 apply_change_with_encoding(text, range, new_text, PositionEncoding::Utf16)
737}
738
739pub fn apply_change_with_encoding(
741 text: &str,
742 range: Option<Range>,
743 new_text: &str,
744 encoding: PositionEncoding,
745) -> String {
746 let Some(range) = range else {
747 return new_text.to_string();
748 };
749 let index = LineIndex::new(text);
750 let a = lsp_offset(&index, range.start, encoding);
751 let b = lsp_offset(&index, range.end, encoding);
752 let (start, end) = (a.min(b), a.max(b));
753 let mut out = String::with_capacity(text.len() - (end - start) + new_text.len());
754 out.push_str(&text[..start]);
755 out.push_str(new_text);
756 out.push_str(&text[end..]);
757 out
758}
759
760pub fn semantic_tokens(text: &str) -> Vec<SemanticToken> {
770 semantic_tokens_with_encoding(text, PositionEncoding::Utf16)
771}
772
773pub fn semantic_tokens_with_encoding(text: &str, encoding: PositionEncoding) -> Vec<SemanticToken> {
775 let raw = match encoding {
776 PositionEncoding::Utf16 => semantic::semantic_tokens_lsp(text),
777 PositionEncoding::Utf8 => sql_dialect_fmt_highlight::semantic_tokens_lsp_utf8(text),
778 };
779 raw.into_iter()
780 .map(
781 |[delta_line, delta_start, length, token_type, token_modifiers_bitset]| SemanticToken {
782 delta_line,
783 delta_start,
784 length,
785 token_type,
786 token_modifiers_bitset,
787 },
788 )
789 .collect()
790}
791
792pub fn semantic_tokens_range_with_encoding(
794 text: &str,
795 range: Range,
796 encoding: PositionEncoding,
797) -> Vec<SemanticToken> {
798 let full = semantic_tokens_with_encoding(text, encoding);
799 let mut absolute = Vec::with_capacity(full.len());
800 let mut line = 0u32;
801 let mut start = 0u32;
802 for token in full {
803 line += token.delta_line;
804 start = if token.delta_line == 0 {
805 start + token.delta_start
806 } else {
807 token.delta_start
808 };
809 absolute.push(AbsoluteSemanticToken { line, start, token });
810 }
811
812 let mut previous: Option<(u32, u32)> = None;
813 absolute
814 .into_iter()
815 .filter(|token| semantic_token_intersects_range(token, range))
816 .map(|absolute| {
817 let delta_line = previous.map_or(absolute.line, |(line, _)| absolute.line - line);
818 let delta_start = if delta_line == 0 {
819 previous.map_or(absolute.start, |(_, start)| absolute.start - start)
820 } else {
821 absolute.start
822 };
823 previous = Some((absolute.line, absolute.start));
824 SemanticToken {
825 delta_line,
826 delta_start,
827 length: absolute.token.length,
828 token_type: absolute.token.token_type,
829 token_modifiers_bitset: absolute.token.token_modifiers_bitset,
830 }
831 })
832 .collect()
833}
834
835#[derive(Clone, Debug)]
836struct AbsoluteSemanticToken {
837 line: u32,
838 start: u32,
839 token: SemanticToken,
840}
841
842fn semantic_token_intersects_range(token: &AbsoluteSemanticToken, range: Range) -> bool {
843 let token_start = Position::new(token.line, token.start);
844 let token_end = Position::new(token.line, token.start + token.token.length);
845 position_lt(token_start, range.end) && position_lt(range.start, token_end)
846}
847
848fn position_lt(a: Position, b: Position) -> bool {
849 a.line < b.line || (a.line == b.line && a.character < b.character)
850}
851
852#[cfg(test)]
853mod tests {
854 use super::*;
855
856 #[test]
857 fn line_index_maps_offsets_to_utf16_positions() {
858 let text = "SELECT a\nFROM 芋;\n"; let index = LineIndex::new(text);
860 assert_eq!(
861 lsp_position(&index, 0, PositionEncoding::Utf16),
862 Position::new(0, 0)
863 );
864 assert_eq!(
865 lsp_position(&index, 7, PositionEncoding::Utf16),
866 Position::new(0, 7)
867 ); let from = text.find("FROM").unwrap();
869 assert_eq!(
870 lsp_position(&index, from, PositionEncoding::Utf16),
871 Position::new(1, 0)
872 );
873 let semicolon = text.find(';').unwrap();
874 assert_eq!(
875 lsp_position(&index, semicolon, PositionEncoding::Utf16),
876 Position::new(1, 6)
877 ); }
879
880 #[test]
881 fn line_index_maps_offsets_to_utf8_positions() {
882 let text = "SELECT a\nFROM 芋;\n";
883 let index = LineIndex::new(text);
884 let semicolon = text.find(';').unwrap();
885 assert_eq!(
886 lsp_position(&index, semicolon, PositionEncoding::Utf8),
887 Position::new(1, 8)
888 ); }
890
891 #[test]
892 fn formatting_replaces_the_whole_document() {
893 let edits = format_edits("select a,b from t", &FormatOptions::default());
894 assert_eq!(edits.len(), 1);
895 assert_eq!(edits[0].new_text, "SELECT a, b\nFROM t;\n");
896 assert_eq!(edits[0].range.start, Position::new(0, 0));
897 }
898
899 #[test]
900 fn already_formatted_input_yields_no_edits() {
901 let formatted = "SELECT a, b\nFROM t;\n";
902 assert!(format_edits(formatted, &FormatOptions::default()).is_empty());
903 }
904
905 #[test]
906 fn range_formatting_reformats_only_the_selected_statement() {
907 let text = "select 1;\nselect a,b from t;\n";
908 let range = Range::new(Position::new(1, 0), Position::new(1, 3));
910 let edits = format_range_edits(text, range, &FormatOptions::default());
911 assert_eq!(edits.len(), 1);
912 assert_eq!(edits[0].new_text, "SELECT a, b\nFROM t;");
913 assert_eq!(edits[0].range.start, Position::new(1, 0));
915 }
916
917 #[test]
918 fn range_formatting_already_formatted_selection_yields_no_edits() {
919 let text = "SELECT 1;\nSELECT a, b\nFROM t;\n";
920 let range = Range::new(Position::new(0, 0), Position::new(0, 8));
921 assert!(format_range_edits(text, range, &FormatOptions::default()).is_empty());
922 }
923
924 #[test]
925 fn on_type_formatting_after_semicolon_reformats_the_finished_statement() {
926 let text = "SELECT 1;\nselect a,b from t;\n";
928 let edits = on_type_formatting_edits(text, Position::new(1, 18), &FormatOptions::default());
929 assert_eq!(edits.len(), 1);
930 assert_eq!(edits[0].new_text, "SELECT a, b\nFROM t;");
931 assert_eq!(edits[0].range.start, Position::new(1, 0));
933 }
934
935 #[test]
936 fn on_type_formatting_after_newline_reformats_the_previous_statement() {
937 let text = "select a,b from t;\n";
939 let edits = on_type_formatting_edits(text, Position::new(1, 0), &FormatOptions::default());
940 assert_eq!(edits.len(), 1);
941 assert_eq!(edits[0].new_text, "SELECT a, b\nFROM t;");
942 }
943
944 #[test]
945 fn on_type_formatting_already_formatted_statement_yields_no_edits() {
946 let text = "SELECT a, b\nFROM t;\n";
947 assert!(
948 on_type_formatting_edits(text, Position::new(1, 7), &FormatOptions::default())
949 .is_empty()
950 );
951 }
952
953 #[test]
954 fn on_type_formatting_in_leading_whitespace_yields_no_edits() {
955 let text = "\n\nselect 1;\n";
956 assert!(
957 on_type_formatting_edits(text, Position::new(1, 0), &FormatOptions::default())
958 .is_empty()
959 );
960 }
961
962 #[test]
963 fn clean_sql_has_no_diagnostics() {
964 assert!(diagnostics("select 1").is_empty());
965 }
966
967 #[test]
968 fn broken_sql_reports_a_diagnostic() {
969 let diags = diagnostics("select from where");
970 assert!(!diags.is_empty());
971 assert_eq!(diags[0].severity, Some(DiagnosticSeverity::ERROR));
972 }
973
974 #[test]
975 fn lexer_errors_reach_diagnostics() {
976 let text = "SELECT 'oops";
980 let diags = diagnostics(text);
981 let lex_diag = diags
982 .iter()
983 .find(|d| d.message.contains("unterminated string"))
984 .expect("an unterminated-string diagnostic");
985 assert_eq!(lex_diag.severity, Some(DiagnosticSeverity::ERROR));
986 let quote = text.find('\'').unwrap() as u32;
987 assert_eq!(lex_diag.range.start, Position::new(0, quote));
988 assert!(lex_diag.range.end.character > lex_diag.range.start.character);
990 }
991
992 #[test]
993 fn parser_diagnostic_range_covers_the_token() {
994 let text = "MERGE tgt USING src ON a = b";
997 let diags = diagnostics(text);
998 let into = diags
999 .iter()
1000 .find(|d| d.message == "expected INTO")
1001 .expect("an INTO diagnostic");
1002 let col = text.find("tgt").unwrap() as u32;
1003 assert_eq!(into.range.start, Position::new(0, col));
1004 assert_eq!(into.range.end, Position::new(0, col + 3));
1005 }
1006
1007 #[test]
1008 fn clean_sql_still_has_no_lexer_or_parser_diagnostics() {
1009 assert!(diagnostics("SELECT a FROM t").is_empty());
1010 }
1011
1012 #[test]
1013 fn unsupported_embedded_language_is_a_warning() {
1014 let text = "CREATE FUNCTION f() RETURNS STRING LANGUAGE RUBY AS $$x$$;";
1015 let diags = diagnostics(text);
1016 let language = diags
1017 .iter()
1018 .find(|d| d.message.contains("unsupported embedded language RUBY"))
1019 .expect("unsupported-language diagnostic");
1020 assert_eq!(language.severity, Some(DiagnosticSeverity::WARNING));
1021 let col = text.find("RUBY").unwrap() as u32;
1022 assert_eq!(language.range.start, Position::new(0, col));
1023 assert_eq!(language.range.end, Position::new(0, col + 4));
1024 }
1025
1026 #[test]
1027 fn embedded_language_warning_does_not_fire_for_plain_columns_or_dynamic_sql() {
1028 for text in [
1029 "SELECT language FROM t;",
1030 "EXECUTE IMMEDIATE $$ SELECT 1 $$;",
1031 ] {
1032 assert!(
1033 diagnostics(text)
1034 .iter()
1035 .all(|diag| !diag.message.contains("unsupported embedded language")),
1036 "{text}"
1037 );
1038 }
1039 }
1040
1041 #[test]
1042 fn select_wildcard_lint_is_a_warning() {
1043 let text = "SELECT * FROM t;";
1044 let diags = diagnostics(text);
1045 let wildcard = diags
1046 .iter()
1047 .find(|d| d.message.contains("avoid SELECT *"))
1048 .expect("SELECT * warning");
1049 assert_eq!(wildcard.severity, Some(DiagnosticSeverity::WARNING));
1050 let col = text.find('*').unwrap() as u32;
1051 assert_eq!(wildcard.range.start, Position::new(0, col));
1052 assert_eq!(wildcard.range.end, Position::new(0, col + 1));
1053 }
1054
1055 #[test]
1056 fn select_wildcard_lint_ignores_function_stars() {
1057 let text = "SELECT count(*) FROM t;";
1058 assert!(
1059 diagnostics(text)
1060 .iter()
1061 .all(|diag| !diag.message.contains("avoid SELECT *")),
1062 "{text}"
1063 );
1064 }
1065
1066 #[test]
1067 fn large_in_list_lint_is_a_warning() {
1068 let values = (0..101)
1069 .map(|n| n.to_string())
1070 .collect::<Vec<_>>()
1071 .join(", ");
1072 let text = format!("SELECT id FROM t WHERE id IN ({values});");
1073 let diags = diagnostics(&text);
1074 let in_list = diags
1075 .iter()
1076 .find(|d| d.message.contains("large IN list"))
1077 .expect("large IN-list warning");
1078 assert_eq!(in_list.severity, Some(DiagnosticSeverity::WARNING));
1079 let col = text.find("IN").unwrap() as u32;
1080 assert_eq!(in_list.range.start, Position::new(0, col));
1081 }
1082
1083 #[test]
1084 fn normal_in_list_and_subquery_have_no_lint() {
1085 for text in [
1086 "SELECT id FROM t WHERE id IN (1, 2, 3);",
1087 "SELECT id FROM t WHERE id IN (SELECT id FROM src);",
1088 ] {
1089 assert!(
1090 diagnostics(text)
1091 .iter()
1092 .all(|diag| !diag.message.contains("large IN list")),
1093 "{text}"
1094 );
1095 }
1096 }
1097
1098 #[test]
1099 fn lint_diagnostics_have_codes_and_respect_options() {
1100 let wildcard = diagnostics("SELECT * FROM t;")
1101 .into_iter()
1102 .find(|diag| diag.message.contains("avoid SELECT *"))
1103 .expect("SELECT * lint");
1104 assert_eq!(
1105 wildcard.code,
1106 Some(lsp_types::NumberOrString::String("SDF001".to_string()))
1107 );
1108
1109 let text = "SELECT id FROM t WHERE id IN (1, 2, 3);";
1110 let diags = diagnostics_with_lint_options(
1111 text,
1112 &FormatOptions::default(),
1113 LintOptions {
1114 large_in_list_threshold: 2,
1115 ..LintOptions::default()
1116 },
1117 PositionEncoding::Utf16,
1118 );
1119 assert!(diags.iter().any(|diag| {
1120 diag.code == Some(lsp_types::NumberOrString::String("SDF002".to_string()))
1121 }));
1122
1123 let disabled = diagnostics_with_lint_options(
1124 text,
1125 &FormatOptions::default(),
1126 LintOptions {
1127 large_in_list: false,
1128 large_in_list_threshold: 2,
1129 ..LintOptions::default()
1130 },
1131 PositionEncoding::Utf16,
1132 );
1133 assert!(
1134 disabled
1135 .iter()
1136 .all(|diag| diag.code
1137 != Some(lsp_types::NumberOrString::String("SDF002".to_string())))
1138 );
1139 }
1140
1141 #[test]
1142 fn unsupported_embedded_language_has_a_code() {
1143 let diags = diagnostics("CREATE FUNCTION f() RETURNS STRING LANGUAGE RUBY AS $$x$$;");
1144 let language = diags
1145 .iter()
1146 .find(|diag| diag.message.contains("unsupported embedded language RUBY"))
1147 .expect("unsupported language lint");
1148 assert_eq!(
1149 language.code,
1150 Some(lsp_types::NumberOrString::String("SDF003".to_string()))
1151 );
1152 }
1153
1154 #[test]
1155 fn offset_is_the_inverse_of_position() {
1156 let text = "SELECT a\nFROM 芋;\n";
1157 let index = LineIndex::new(text);
1158 for offset in [
1159 0usize,
1160 7,
1161 text.find("FROM").unwrap(),
1162 text.find(';').unwrap(),
1163 ] {
1164 assert_eq!(
1165 lsp_offset(
1166 &index,
1167 lsp_position(&index, offset, PositionEncoding::Utf16),
1168 PositionEncoding::Utf16
1169 ),
1170 offset
1171 );
1172 }
1173 }
1174
1175 #[test]
1176 fn hover_describes_a_type() {
1177 let src = "select x::varchar from t";
1179 let col = src.find("varchar").unwrap() as u32;
1180 let hover = hover(src, Position::new(0, col)).expect("hover");
1181 assert!(hover.range.is_some());
1182 match hover.contents {
1183 HoverContents::Markup(m) => assert!(m.value.to_lowercase().contains("varchar")),
1184 _ => panic!("expected markup"),
1185 }
1186 }
1187
1188 #[test]
1189 fn hover_describes_spec_features_with_a_docs_link() {
1190 let src = "select c from t qualify row_number() over (order by c) = 1";
1192 let col = src.find("qualify").unwrap() as u32;
1193 let hover = hover(src, Position::new(0, col)).expect("hover");
1194 match hover.contents {
1195 HoverContents::Markup(m) => {
1196 assert!(m.value.contains("**QUALIFY**"));
1197 assert!(m.value.contains("QUALIFY <expr>"));
1198 assert!(m.value.contains("[Snowflake docs]("));
1199 }
1200 _ => panic!("expected markup"),
1201 }
1202 }
1203
1204 #[test]
1205 fn hover_describes_function_signatures() {
1206 let src = "select dateadd(day, 1, d) from t";
1207 let col = src.find("dateadd").unwrap() as u32;
1208 let hover = hover(src, Position::new(0, col)).expect("hover");
1209 match hover.contents {
1210 HoverContents::Markup(m) => {
1211 assert!(m.value.contains("DATEADD( <date_or_time_part>"));
1212 assert!(m.value.contains("[Snowflake docs]("));
1213 }
1214 _ => panic!("expected markup"),
1215 }
1216 }
1217
1218 #[test]
1219 fn apply_change_splices_an_incremental_edit() {
1220 let text = "hello\nworld\n";
1222 let range = Range::new(Position::new(1, 0), Position::new(1, 5));
1223 assert_eq!(apply_change(text, Some(range), "snow"), "hello\nsnow\n");
1224 }
1225
1226 #[test]
1227 fn apply_change_splices_utf8_encoded_ranges() {
1228 let text = "SELECT '長芋'\nFROM t\n";
1229 let start = Position::new(0, "SELECT '長".len() as u32);
1230 let end = Position::new(0, "SELECT '長芋".len() as u32);
1231 assert_eq!(
1232 apply_change_with_encoding(
1233 text,
1234 Some(Range::new(start, end)),
1235 "山芋",
1236 PositionEncoding::Utf8
1237 ),
1238 "SELECT '長山芋'\nFROM t\n"
1239 );
1240 }
1241
1242 #[test]
1243 fn apply_change_with_no_range_replaces_whole_document() {
1244 assert_eq!(apply_change("old", None, "new text"), "new text");
1245 }
1246
1247 #[test]
1248 fn folding_ranges_cover_multiline_statements() {
1249 let ranges = folding_ranges("select a,\nb\nfrom t;\n\nselect 1;");
1250 assert_eq!(ranges.len(), 1); assert_eq!(ranges[0].start_line, 0);
1252 assert_eq!(ranges[0].end_line, 2);
1253 }
1254
1255 #[test]
1256 fn document_symbols_name_top_level_statements() {
1257 let symbols = document_symbols(
1258 "CREATE TABLE db.t (id INT);\n\nSELECT id\nFROM db.t;",
1259 &FormatOptions::default(),
1260 );
1261 assert_eq!(symbols.len(), 2);
1262 assert_eq!(symbols[0].name, "CREATE TABLE db.t");
1263 assert_eq!(symbols[0].kind, SymbolKind::STRUCT);
1264 assert_eq!(symbols[0].selection_range.start, Position::new(0, 13));
1265 assert_eq!(symbols[1].name, "SELECT");
1266 assert_eq!(symbols[1].kind, SymbolKind::FUNCTION);
1267 }
1268
1269 #[test]
1270 fn document_symbols_name_stage_file_operations() {
1271 let symbols = document_symbols(
1272 "PUT file:///tmp/x.csv @stage;\nLIST @stage/path;",
1273 &FormatOptions::default(),
1274 );
1275 assert_eq!(symbols.len(), 2);
1276 assert_eq!(symbols[0].name, "PUT");
1277 assert_eq!(symbols[0].kind, SymbolKind::METHOD);
1278 assert_eq!(symbols[1].name, "LIST");
1279 assert_eq!(symbols[1].kind, SymbolKind::METHOD);
1280 }
1281
1282 #[test]
1283 fn completion_items_include_keywords_types_and_snippets() {
1284 let items = completion_items();
1285 assert!(items.iter().any(|item| {
1286 item.label == "SELECT" && item.kind == Some(CompletionItemKind::KEYWORD)
1287 }));
1288 assert!(items
1289 .iter()
1290 .any(|item| item.label == "NUMBER"
1291 && item.kind == Some(CompletionItemKind::TYPE_PARAMETER)));
1292 assert!(items.iter().any(|item| {
1293 item.label == "CREATE TABLE ..."
1294 && item.kind == Some(CompletionItemKind::SNIPPET)
1295 && item.insert_text_format == Some(InsertTextFormat::SNIPPET)
1296 }));
1297 }
1298
1299 #[test]
1300 fn semantic_tokens_tag_keywords() {
1301 let tokens = semantic_tokens("select a from t");
1302 assert!(!tokens.is_empty());
1303 assert_eq!(tokens[0].delta_line, 0);
1305 assert_eq!(tokens[0].delta_start, 0);
1306 assert_eq!(tokens[0].length, 6);
1307 assert_eq!(tokens[0].token_type, 0);
1308 assert_eq!(
1310 tokens[0].token_modifiers_bitset,
1311 semantic::SemanticTokenModifiers::DEFAULT_LIBRARY.bits()
1312 );
1313 }
1314
1315 #[test]
1316 fn server_legend_equals_the_highlighter_legend() {
1317 let advertised: Vec<String> = token_types()
1320 .iter()
1321 .map(|t| t.as_str().to_string())
1322 .collect();
1323 let expected: Vec<String> = semantic::SemanticTokenType::LEGEND
1324 .iter()
1325 .map(|t| t.name().to_string())
1326 .collect();
1327 assert_eq!(advertised, expected);
1328 assert_eq!(advertised.last().map(String::as_str), Some("function"));
1330
1331 let mods: Vec<String> = token_modifiers()
1333 .iter()
1334 .map(|m| m.as_str().to_string())
1335 .collect();
1336 assert_eq!(mods, vec!["documentation", "defaultLibrary"]);
1337 }
1338
1339 #[test]
1340 fn semantic_tokens_are_monotonic_and_never_panic_on_multiline() {
1341 let tokens = semantic_tokens("select 1 /* a\nb */ from t");
1343 assert!(tokens.iter().all(|t| t.length > 0));
1345 }
1346
1347 #[test]
1348 fn semantic_tokens_range_filters_and_reencodes_tokens() {
1349 let text = "SELECT a\nFROM t\nWHERE a = 1";
1350 let tokens = semantic_tokens_range_with_encoding(
1351 text,
1352 Range::new(Position::new(1, 0), Position::new(2, 0)),
1353 PositionEncoding::Utf16,
1354 );
1355 assert!(!tokens.is_empty());
1356 assert_eq!(tokens[0].delta_line, 1);
1357
1358 let mut line = 0u32;
1359 let mut start = 0u32;
1360 for token in tokens {
1361 line += token.delta_line;
1362 start = if token.delta_line == 0 {
1363 start + token.delta_start
1364 } else {
1365 token.delta_start
1366 };
1367 assert_eq!(line, 1);
1368 assert!(start + token.length <= 6);
1369 }
1370 }
1371}