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" | "SMALLINT" | "TINYINT" | "BYTEINT" => {
613 "Integer numeric alias."
614 }
615 "FLOAT" | "FLOAT4" | "FLOAT8" | "DOUBLE" | "REAL" => {
616 "Approximate floating-point numeric type."
617 }
618 "DECFLOAT" => "Decimal floating-point numeric type.",
619 "VARCHAR" => "Variable-length character data.",
620 "STRING" | "TEXT" | "NVARCHAR" | "NVARCHAR2" => "Alias for VARCHAR.",
621 "CHAR" => "Character data.",
622 "CHARACTER" | "NCHAR" => "Alias for CHAR.",
623 "VARBINARY" => "Alias for BINARY.",
624 "FILE" => "Reference to an unstructured file on a stage.",
625 "UUID" => "Universally unique identifier.",
626 "BOOLEAN" => "TRUE/FALSE logical value.",
627 "VARIANT" => "Semi-structured value.",
628 "OBJECT" => "Semi-structured key/value object.",
629 "ARRAY" => "Semi-structured ordered collection.",
630 "MAP" => "Structured key/value collection.",
631 "VECTOR" => "Vector type for embeddings.",
632 "DATE" => "Calendar date without time of day.",
633 "TIME" => "Time of day without date.",
634 "TIMESTAMP" => "Timestamp family.",
635 "TIMESTAMP_NTZ" => "Timestamp without time zone.",
636 "TIMESTAMP_LTZ" => "Timestamp using the session time zone.",
637 "TIMESTAMP_TZ" => "Timestamp with explicit offset.",
638 "BINARY" => "Variable-length binary data.",
639 "GEOGRAPHY" => "Spherical geospatial data.",
640 "GEOMETRY" => "Planar geospatial data.",
641 _ => "Built-in SQL type.",
642 }
643}
644
645fn completion_item(
646 label: &str,
647 kind: CompletionItemKind,
648 detail: &str,
649 documentation: &str,
650 insert_text: Option<&str>,
651 sort_prefix: &str,
652) -> CompletionItem {
653 CompletionItem {
654 label: label.to_string(),
655 kind: Some(kind),
656 detail: Some(detail.to_string()),
657 documentation: Some(lsp_types::Documentation::MarkupContent(MarkupContent {
658 kind: MarkupKind::Markdown,
659 value: documentation.to_string(),
660 })),
661 insert_text: insert_text.map(str::to_string),
662 insert_text_format: insert_text.map(|_| InsertTextFormat::SNIPPET),
663 sort_text: Some(format!("{sort_prefix}-{label}")),
664 ..CompletionItem::default()
665 }
666}
667
668struct Snippet {
669 label: &'static str,
670 insert_text: &'static str,
671 documentation: &'static str,
672}
673
674const SQL_SNIPPETS: &[Snippet] = &[
675 Snippet {
676 label: "SELECT ... FROM ...",
677 insert_text: "SELECT ${1:columns}\nFROM ${2:table};",
678 documentation: "Scaffold a SELECT statement.",
679 },
680 Snippet {
681 label: "WITH ... AS (...)",
682 insert_text: "WITH ${1:cte} AS (\n SELECT ${2:*}\n FROM ${3:source}\n)\nSELECT ${4:*}\nFROM ${1:cte};",
683 documentation: "Scaffold a common table expression.",
684 },
685 Snippet {
686 label: "CREATE TABLE ...",
687 insert_text: "CREATE TABLE ${1:table_name} (\n ${2:column_name} ${3:NUMBER}\n);",
688 documentation: "Scaffold a CREATE TABLE statement.",
689 },
690 Snippet {
691 label: "INSERT INTO ...",
692 insert_text: "INSERT INTO ${1:table_name} (${2:columns})\nVALUES (${3:values});",
693 documentation: "Scaffold an INSERT statement.",
694 },
695 Snippet {
696 label: "CREATE PROCEDURE ...",
697 insert_text: "CREATE PROCEDURE ${1:procedure_name}(${2:args})\nRETURNS ${3:STRING}\nLANGUAGE SQL\nAS\n$$\nBEGIN\n ${4:RETURN 'ok';}\nEND;\n$$;",
698 documentation: "Scaffold a Snowflake SQL procedure.",
699 },
700];
701
702pub fn folding_ranges(text: &str) -> Vec<FoldingRange> {
705 let index = LineIndex::new(text);
706 let root = sql_dialect_fmt_parser::parse(text).syntax();
707 root.children()
708 .filter_map(|stmt| {
709 let mut tokens = stmt
712 .descendants_with_tokens()
713 .filter_map(|el| el.into_token())
714 .filter(|t| !t.kind().is_trivia());
715 let first = tokens.next()?;
716 let last = tokens.last().unwrap_or_else(|| first.clone());
717 let start = lsp_position(
718 &index,
719 first.text_range().start().into(),
720 PositionEncoding::Utf16,
721 )
722 .line;
723 let end = lsp_position(
724 &index,
725 last.text_range().end().into(),
726 PositionEncoding::Utf16,
727 )
728 .line;
729 (end > start).then_some(FoldingRange {
730 start_line: start,
731 end_line: end,
732 kind: Some(FoldingRangeKind::Region),
733 ..FoldingRange::default()
734 })
735 })
736 .collect()
737}
738
739pub fn apply_change(text: &str, range: Option<Range>, new_text: &str) -> String {
745 apply_change_with_encoding(text, range, new_text, PositionEncoding::Utf16)
746}
747
748pub fn apply_change_with_encoding(
750 text: &str,
751 range: Option<Range>,
752 new_text: &str,
753 encoding: PositionEncoding,
754) -> String {
755 let Some(range) = range else {
756 return new_text.to_string();
757 };
758 let index = LineIndex::new(text);
759 let a = lsp_offset(&index, range.start, encoding);
760 let b = lsp_offset(&index, range.end, encoding);
761 let (start, end) = (a.min(b), a.max(b));
762 let mut out = String::with_capacity(text.len() - (end - start) + new_text.len());
763 out.push_str(&text[..start]);
764 out.push_str(new_text);
765 out.push_str(&text[end..]);
766 out
767}
768
769pub fn semantic_tokens(text: &str) -> Vec<SemanticToken> {
779 semantic_tokens_with_encoding(text, PositionEncoding::Utf16)
780}
781
782pub fn semantic_tokens_with_encoding(text: &str, encoding: PositionEncoding) -> Vec<SemanticToken> {
784 let raw = match encoding {
785 PositionEncoding::Utf16 => semantic::semantic_tokens_lsp(text),
786 PositionEncoding::Utf8 => sql_dialect_fmt_highlight::semantic_tokens_lsp_utf8(text),
787 };
788 raw.into_iter()
789 .map(
790 |[delta_line, delta_start, length, token_type, token_modifiers_bitset]| SemanticToken {
791 delta_line,
792 delta_start,
793 length,
794 token_type,
795 token_modifiers_bitset,
796 },
797 )
798 .collect()
799}
800
801pub fn semantic_tokens_range_with_encoding(
803 text: &str,
804 range: Range,
805 encoding: PositionEncoding,
806) -> Vec<SemanticToken> {
807 let full = semantic_tokens_with_encoding(text, encoding);
808 let mut absolute = Vec::with_capacity(full.len());
809 let mut line = 0u32;
810 let mut start = 0u32;
811 for token in full {
812 line += token.delta_line;
813 start = if token.delta_line == 0 {
814 start + token.delta_start
815 } else {
816 token.delta_start
817 };
818 absolute.push(AbsoluteSemanticToken { line, start, token });
819 }
820
821 let mut previous: Option<(u32, u32)> = None;
822 absolute
823 .into_iter()
824 .filter(|token| semantic_token_intersects_range(token, range))
825 .map(|absolute| {
826 let delta_line = previous.map_or(absolute.line, |(line, _)| absolute.line - line);
827 let delta_start = if delta_line == 0 {
828 previous.map_or(absolute.start, |(_, start)| absolute.start - start)
829 } else {
830 absolute.start
831 };
832 previous = Some((absolute.line, absolute.start));
833 SemanticToken {
834 delta_line,
835 delta_start,
836 length: absolute.token.length,
837 token_type: absolute.token.token_type,
838 token_modifiers_bitset: absolute.token.token_modifiers_bitset,
839 }
840 })
841 .collect()
842}
843
844#[derive(Clone, Debug)]
845struct AbsoluteSemanticToken {
846 line: u32,
847 start: u32,
848 token: SemanticToken,
849}
850
851fn semantic_token_intersects_range(token: &AbsoluteSemanticToken, range: Range) -> bool {
852 let token_start = Position::new(token.line, token.start);
853 let token_end = Position::new(token.line, token.start + token.token.length);
854 position_lt(token_start, range.end) && position_lt(range.start, token_end)
855}
856
857fn position_lt(a: Position, b: Position) -> bool {
858 a.line < b.line || (a.line == b.line && a.character < b.character)
859}
860
861#[cfg(test)]
862mod tests {
863 use super::*;
864
865 #[test]
866 fn line_index_maps_offsets_to_utf16_positions() {
867 let text = "SELECT a\nFROM 芋;\n"; let index = LineIndex::new(text);
869 assert_eq!(
870 lsp_position(&index, 0, PositionEncoding::Utf16),
871 Position::new(0, 0)
872 );
873 assert_eq!(
874 lsp_position(&index, 7, PositionEncoding::Utf16),
875 Position::new(0, 7)
876 ); let from = text.find("FROM").unwrap();
878 assert_eq!(
879 lsp_position(&index, from, PositionEncoding::Utf16),
880 Position::new(1, 0)
881 );
882 let semicolon = text.find(';').unwrap();
883 assert_eq!(
884 lsp_position(&index, semicolon, PositionEncoding::Utf16),
885 Position::new(1, 6)
886 ); }
888
889 #[test]
890 fn line_index_maps_offsets_to_utf8_positions() {
891 let text = "SELECT a\nFROM 芋;\n";
892 let index = LineIndex::new(text);
893 let semicolon = text.find(';').unwrap();
894 assert_eq!(
895 lsp_position(&index, semicolon, PositionEncoding::Utf8),
896 Position::new(1, 8)
897 ); }
899
900 #[test]
901 fn formatting_replaces_the_whole_document() {
902 let edits = format_edits("select a,b from t", &FormatOptions::default());
903 assert_eq!(edits.len(), 1);
904 assert_eq!(edits[0].new_text, "SELECT a, b\nFROM t;\n");
905 assert_eq!(edits[0].range.start, Position::new(0, 0));
906 }
907
908 #[test]
909 fn already_formatted_input_yields_no_edits() {
910 let formatted = "SELECT a, b\nFROM t;\n";
911 assert!(format_edits(formatted, &FormatOptions::default()).is_empty());
912 }
913
914 #[test]
915 fn range_formatting_reformats_only_the_selected_statement() {
916 let text = "select 1;\nselect a,b from t;\n";
917 let range = Range::new(Position::new(1, 0), Position::new(1, 3));
919 let edits = format_range_edits(text, range, &FormatOptions::default());
920 assert_eq!(edits.len(), 1);
921 assert_eq!(edits[0].new_text, "SELECT a, b\nFROM t;");
922 assert_eq!(edits[0].range.start, Position::new(1, 0));
924 }
925
926 #[test]
927 fn range_formatting_already_formatted_selection_yields_no_edits() {
928 let text = "SELECT 1;\nSELECT a, b\nFROM t;\n";
929 let range = Range::new(Position::new(0, 0), Position::new(0, 8));
930 assert!(format_range_edits(text, range, &FormatOptions::default()).is_empty());
931 }
932
933 #[test]
934 fn on_type_formatting_after_semicolon_reformats_the_finished_statement() {
935 let text = "SELECT 1;\nselect a,b from t;\n";
937 let edits = on_type_formatting_edits(text, Position::new(1, 18), &FormatOptions::default());
938 assert_eq!(edits.len(), 1);
939 assert_eq!(edits[0].new_text, "SELECT a, b\nFROM t;");
940 assert_eq!(edits[0].range.start, Position::new(1, 0));
942 }
943
944 #[test]
945 fn on_type_formatting_after_newline_reformats_the_previous_statement() {
946 let text = "select a,b from t;\n";
948 let edits = on_type_formatting_edits(text, Position::new(1, 0), &FormatOptions::default());
949 assert_eq!(edits.len(), 1);
950 assert_eq!(edits[0].new_text, "SELECT a, b\nFROM t;");
951 }
952
953 #[test]
954 fn on_type_formatting_already_formatted_statement_yields_no_edits() {
955 let text = "SELECT a, b\nFROM t;\n";
956 assert!(
957 on_type_formatting_edits(text, Position::new(1, 7), &FormatOptions::default())
958 .is_empty()
959 );
960 }
961
962 #[test]
963 fn on_type_formatting_in_leading_whitespace_yields_no_edits() {
964 let text = "\n\nselect 1;\n";
965 assert!(
966 on_type_formatting_edits(text, Position::new(1, 0), &FormatOptions::default())
967 .is_empty()
968 );
969 }
970
971 #[test]
972 fn clean_sql_has_no_diagnostics() {
973 assert!(diagnostics("select 1").is_empty());
974 }
975
976 #[test]
977 fn broken_sql_reports_a_diagnostic() {
978 let diags = diagnostics("select from where");
979 assert!(!diags.is_empty());
980 assert_eq!(diags[0].severity, Some(DiagnosticSeverity::ERROR));
981 }
982
983 #[test]
984 fn lexer_errors_reach_diagnostics() {
985 let text = "SELECT 'oops";
989 let diags = diagnostics(text);
990 let lex_diag = diags
991 .iter()
992 .find(|d| d.message.contains("unterminated string"))
993 .expect("an unterminated-string diagnostic");
994 assert_eq!(lex_diag.severity, Some(DiagnosticSeverity::ERROR));
995 let quote = text.find('\'').unwrap() as u32;
996 assert_eq!(lex_diag.range.start, Position::new(0, quote));
997 assert!(lex_diag.range.end.character > lex_diag.range.start.character);
999 }
1000
1001 #[test]
1002 fn parser_diagnostic_range_covers_the_token() {
1003 let text = "MERGE tgt USING src ON a = b";
1006 let diags = diagnostics(text);
1007 let into = diags
1008 .iter()
1009 .find(|d| d.message == "expected INTO")
1010 .expect("an INTO diagnostic");
1011 let col = text.find("tgt").unwrap() as u32;
1012 assert_eq!(into.range.start, Position::new(0, col));
1013 assert_eq!(into.range.end, Position::new(0, col + 3));
1014 }
1015
1016 #[test]
1017 fn clean_sql_still_has_no_lexer_or_parser_diagnostics() {
1018 assert!(diagnostics("SELECT a FROM t").is_empty());
1019 }
1020
1021 #[test]
1022 fn unsupported_embedded_language_is_a_warning() {
1023 let text = "CREATE FUNCTION f() RETURNS STRING LANGUAGE RUBY AS $$x$$;";
1024 let diags = diagnostics(text);
1025 let language = diags
1026 .iter()
1027 .find(|d| d.message.contains("unsupported embedded language RUBY"))
1028 .expect("unsupported-language diagnostic");
1029 assert_eq!(language.severity, Some(DiagnosticSeverity::WARNING));
1030 let col = text.find("RUBY").unwrap() as u32;
1031 assert_eq!(language.range.start, Position::new(0, col));
1032 assert_eq!(language.range.end, Position::new(0, col + 4));
1033 }
1034
1035 #[test]
1036 fn embedded_language_warning_does_not_fire_for_plain_columns_or_dynamic_sql() {
1037 for text in [
1038 "SELECT language FROM t;",
1039 "EXECUTE IMMEDIATE $$ SELECT 1 $$;",
1040 ] {
1041 assert!(
1042 diagnostics(text)
1043 .iter()
1044 .all(|diag| !diag.message.contains("unsupported embedded language")),
1045 "{text}"
1046 );
1047 }
1048 }
1049
1050 #[test]
1051 fn select_wildcard_lint_is_a_warning() {
1052 let text = "SELECT * FROM t;";
1053 let diags = diagnostics(text);
1054 let wildcard = diags
1055 .iter()
1056 .find(|d| d.message.contains("avoid SELECT *"))
1057 .expect("SELECT * warning");
1058 assert_eq!(wildcard.severity, Some(DiagnosticSeverity::WARNING));
1059 let col = text.find('*').unwrap() as u32;
1060 assert_eq!(wildcard.range.start, Position::new(0, col));
1061 assert_eq!(wildcard.range.end, Position::new(0, col + 1));
1062 }
1063
1064 #[test]
1065 fn select_wildcard_lint_ignores_function_stars() {
1066 let text = "SELECT count(*) FROM t;";
1067 assert!(
1068 diagnostics(text)
1069 .iter()
1070 .all(|diag| !diag.message.contains("avoid SELECT *")),
1071 "{text}"
1072 );
1073 }
1074
1075 #[test]
1076 fn large_in_list_lint_is_a_warning() {
1077 let values = (0..101)
1078 .map(|n| n.to_string())
1079 .collect::<Vec<_>>()
1080 .join(", ");
1081 let text = format!("SELECT id FROM t WHERE id IN ({values});");
1082 let diags = diagnostics(&text);
1083 let in_list = diags
1084 .iter()
1085 .find(|d| d.message.contains("large IN list"))
1086 .expect("large IN-list warning");
1087 assert_eq!(in_list.severity, Some(DiagnosticSeverity::WARNING));
1088 let col = text.find("IN").unwrap() as u32;
1089 assert_eq!(in_list.range.start, Position::new(0, col));
1090 }
1091
1092 #[test]
1093 fn normal_in_list_and_subquery_have_no_lint() {
1094 for text in [
1095 "SELECT id FROM t WHERE id IN (1, 2, 3);",
1096 "SELECT id FROM t WHERE id IN (SELECT id FROM src);",
1097 ] {
1098 assert!(
1099 diagnostics(text)
1100 .iter()
1101 .all(|diag| !diag.message.contains("large IN list")),
1102 "{text}"
1103 );
1104 }
1105 }
1106
1107 #[test]
1108 fn lint_diagnostics_have_codes_and_respect_options() {
1109 let wildcard = diagnostics("SELECT * FROM t;")
1110 .into_iter()
1111 .find(|diag| diag.message.contains("avoid SELECT *"))
1112 .expect("SELECT * lint");
1113 assert_eq!(
1114 wildcard.code,
1115 Some(lsp_types::NumberOrString::String("SDF001".to_string()))
1116 );
1117
1118 let text = "SELECT id FROM t WHERE id IN (1, 2, 3);";
1119 let diags = diagnostics_with_lint_options(
1120 text,
1121 &FormatOptions::default(),
1122 LintOptions {
1123 large_in_list_threshold: 2,
1124 ..LintOptions::default()
1125 },
1126 PositionEncoding::Utf16,
1127 );
1128 assert!(diags.iter().any(|diag| {
1129 diag.code == Some(lsp_types::NumberOrString::String("SDF002".to_string()))
1130 }));
1131
1132 let disabled = diagnostics_with_lint_options(
1133 text,
1134 &FormatOptions::default(),
1135 LintOptions {
1136 large_in_list: false,
1137 large_in_list_threshold: 2,
1138 ..LintOptions::default()
1139 },
1140 PositionEncoding::Utf16,
1141 );
1142 assert!(
1143 disabled
1144 .iter()
1145 .all(|diag| diag.code
1146 != Some(lsp_types::NumberOrString::String("SDF002".to_string())))
1147 );
1148 }
1149
1150 #[test]
1151 fn unsupported_embedded_language_has_a_code() {
1152 let diags = diagnostics("CREATE FUNCTION f() RETURNS STRING LANGUAGE RUBY AS $$x$$;");
1153 let language = diags
1154 .iter()
1155 .find(|diag| diag.message.contains("unsupported embedded language RUBY"))
1156 .expect("unsupported language lint");
1157 assert_eq!(
1158 language.code,
1159 Some(lsp_types::NumberOrString::String("SDF003".to_string()))
1160 );
1161 }
1162
1163 #[test]
1164 fn offset_is_the_inverse_of_position() {
1165 let text = "SELECT a\nFROM 芋;\n";
1166 let index = LineIndex::new(text);
1167 for offset in [
1168 0usize,
1169 7,
1170 text.find("FROM").unwrap(),
1171 text.find(';').unwrap(),
1172 ] {
1173 assert_eq!(
1174 lsp_offset(
1175 &index,
1176 lsp_position(&index, offset, PositionEncoding::Utf16),
1177 PositionEncoding::Utf16
1178 ),
1179 offset
1180 );
1181 }
1182 }
1183
1184 #[test]
1185 fn hover_describes_a_type() {
1186 let src = "select x::varchar from t";
1188 let col = src.find("varchar").unwrap() as u32;
1189 let hover = hover(src, Position::new(0, col)).expect("hover");
1190 assert!(hover.range.is_some());
1191 match hover.contents {
1192 HoverContents::Markup(m) => assert!(m.value.to_lowercase().contains("varchar")),
1193 _ => panic!("expected markup"),
1194 }
1195 }
1196
1197 #[test]
1198 fn hover_describes_spec_features_with_a_docs_link() {
1199 let src = "select c from t qualify row_number() over (order by c) = 1";
1201 let col = src.find("qualify").unwrap() as u32;
1202 let hover = hover(src, Position::new(0, col)).expect("hover");
1203 match hover.contents {
1204 HoverContents::Markup(m) => {
1205 assert!(m.value.contains("**QUALIFY**"));
1206 assert!(m.value.contains("QUALIFY <expr>"));
1207 assert!(m.value.contains("[Snowflake docs]("));
1208 }
1209 _ => panic!("expected markup"),
1210 }
1211 }
1212
1213 #[test]
1214 fn hover_describes_function_signatures() {
1215 let src = "select dateadd(day, 1, d) from t";
1216 let col = src.find("dateadd").unwrap() as u32;
1217 let hover = hover(src, Position::new(0, col)).expect("hover");
1218 match hover.contents {
1219 HoverContents::Markup(m) => {
1220 assert!(m.value.contains("DATEADD( <date_or_time_part>"));
1221 assert!(m.value.contains("[Snowflake docs]("));
1222 }
1223 _ => panic!("expected markup"),
1224 }
1225 }
1226
1227 #[test]
1228 fn apply_change_splices_an_incremental_edit() {
1229 let text = "hello\nworld\n";
1231 let range = Range::new(Position::new(1, 0), Position::new(1, 5));
1232 assert_eq!(apply_change(text, Some(range), "snow"), "hello\nsnow\n");
1233 }
1234
1235 #[test]
1236 fn apply_change_splices_utf8_encoded_ranges() {
1237 let text = "SELECT '長芋'\nFROM t\n";
1238 let start = Position::new(0, "SELECT '長".len() as u32);
1239 let end = Position::new(0, "SELECT '長芋".len() as u32);
1240 assert_eq!(
1241 apply_change_with_encoding(
1242 text,
1243 Some(Range::new(start, end)),
1244 "山芋",
1245 PositionEncoding::Utf8
1246 ),
1247 "SELECT '長山芋'\nFROM t\n"
1248 );
1249 }
1250
1251 #[test]
1252 fn apply_change_with_no_range_replaces_whole_document() {
1253 assert_eq!(apply_change("old", None, "new text"), "new text");
1254 }
1255
1256 #[test]
1257 fn folding_ranges_cover_multiline_statements() {
1258 let ranges = folding_ranges("select a,\nb\nfrom t;\n\nselect 1;");
1259 assert_eq!(ranges.len(), 1); assert_eq!(ranges[0].start_line, 0);
1261 assert_eq!(ranges[0].end_line, 2);
1262 }
1263
1264 #[test]
1265 fn document_symbols_name_top_level_statements() {
1266 let symbols = document_symbols(
1267 "CREATE TABLE db.t (id INT);\n\nSELECT id\nFROM db.t;",
1268 &FormatOptions::default(),
1269 );
1270 assert_eq!(symbols.len(), 2);
1271 assert_eq!(symbols[0].name, "CREATE TABLE db.t");
1272 assert_eq!(symbols[0].kind, SymbolKind::STRUCT);
1273 assert_eq!(symbols[0].selection_range.start, Position::new(0, 13));
1274 assert_eq!(symbols[1].name, "SELECT");
1275 assert_eq!(symbols[1].kind, SymbolKind::FUNCTION);
1276 }
1277
1278 #[test]
1279 fn document_symbols_name_stage_file_operations() {
1280 let symbols = document_symbols(
1281 "PUT file:///tmp/x.csv @stage;\nLIST @stage/path;",
1282 &FormatOptions::default(),
1283 );
1284 assert_eq!(symbols.len(), 2);
1285 assert_eq!(symbols[0].name, "PUT");
1286 assert_eq!(symbols[0].kind, SymbolKind::METHOD);
1287 assert_eq!(symbols[1].name, "LIST");
1288 assert_eq!(symbols[1].kind, SymbolKind::METHOD);
1289 }
1290
1291 #[test]
1292 fn completion_items_include_keywords_types_and_snippets() {
1293 let items = completion_items();
1294 assert!(items.iter().any(|item| {
1295 item.label == "SELECT" && item.kind == Some(CompletionItemKind::KEYWORD)
1296 }));
1297 assert!(items
1298 .iter()
1299 .any(|item| item.label == "NUMBER"
1300 && item.kind == Some(CompletionItemKind::TYPE_PARAMETER)));
1301 assert!(items.iter().any(|item| {
1302 item.label == "CREATE TABLE ..."
1303 && item.kind == Some(CompletionItemKind::SNIPPET)
1304 && item.insert_text_format == Some(InsertTextFormat::SNIPPET)
1305 }));
1306 }
1307
1308 #[test]
1309 fn semantic_tokens_tag_keywords() {
1310 let tokens = semantic_tokens("select a from t");
1311 assert!(!tokens.is_empty());
1312 assert_eq!(tokens[0].delta_line, 0);
1314 assert_eq!(tokens[0].delta_start, 0);
1315 assert_eq!(tokens[0].length, 6);
1316 assert_eq!(tokens[0].token_type, 0);
1317 assert_eq!(
1319 tokens[0].token_modifiers_bitset,
1320 semantic::SemanticTokenModifiers::DEFAULT_LIBRARY.bits()
1321 );
1322 }
1323
1324 #[test]
1325 fn server_legend_equals_the_highlighter_legend() {
1326 let advertised: Vec<String> = token_types()
1329 .iter()
1330 .map(|t| t.as_str().to_string())
1331 .collect();
1332 let expected: Vec<String> = semantic::SemanticTokenType::LEGEND
1333 .iter()
1334 .map(|t| t.name().to_string())
1335 .collect();
1336 assert_eq!(advertised, expected);
1337 assert_eq!(advertised.last().map(String::as_str), Some("function"));
1339
1340 let mods: Vec<String> = token_modifiers()
1342 .iter()
1343 .map(|m| m.as_str().to_string())
1344 .collect();
1345 assert_eq!(mods, vec!["documentation", "defaultLibrary"]);
1346 }
1347
1348 #[test]
1349 fn semantic_tokens_are_monotonic_and_never_panic_on_multiline() {
1350 let tokens = semantic_tokens("select 1 /* a\nb */ from t");
1352 assert!(tokens.iter().all(|t| t.length > 0));
1354 }
1355
1356 #[test]
1357 fn semantic_tokens_range_filters_and_reencodes_tokens() {
1358 let text = "SELECT a\nFROM t\nWHERE a = 1";
1359 let tokens = semantic_tokens_range_with_encoding(
1360 text,
1361 Range::new(Position::new(1, 0), Position::new(2, 0)),
1362 PositionEncoding::Utf16,
1363 );
1364 assert!(!tokens.is_empty());
1365 assert_eq!(tokens[0].delta_line, 1);
1366
1367 let mut line = 0u32;
1368 let mut start = 0u32;
1369 for token in tokens {
1370 line += token.delta_line;
1371 start = if token.delta_line == 0 {
1372 start + token.delta_start
1373 } else {
1374 token.delta_start
1375 };
1376 assert_eq!(line, 1);
1377 assert!(start + token.length <= 6);
1378 }
1379 }
1380}