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 diagnostics(text: &str) -> Vec<Diagnostic> {
126 diagnostics_with_options(text, &FormatOptions::default(), PositionEncoding::Utf16)
127}
128
129pub fn diagnostics_with_encoding(text: &str, encoding: PositionEncoding) -> Vec<Diagnostic> {
131 diagnostics_with_options(text, &FormatOptions::default(), encoding)
132}
133
134pub fn diagnostics_with_options(
136 text: &str,
137 options: &FormatOptions,
138 encoding: PositionEncoding,
139) -> Vec<Diagnostic> {
140 diagnostics_with_lint_options(text, options, LintOptions::default(), encoding)
141}
142
143pub fn diagnostics_with_lint_options(
145 text: &str,
146 options: &FormatOptions,
147 lint_options: LintOptions,
148 encoding: PositionEncoding,
149) -> Vec<Diagnostic> {
150 let index = LineIndex::new(text);
151 let to_range = |span: std::ops::Range<usize>| {
152 Range::new(
153 lsp_position(&index, span.start, encoding),
154 lsp_position(&index, span.end, encoding),
155 )
156 };
157 let make = |range: Range, message: String| Diagnostic {
158 range,
159 severity: Some(DiagnosticSeverity::ERROR),
160 source: Some("sql-dialect-fmt".to_string()),
161 message,
162 ..Default::default()
163 };
164
165 let lexed = sql_dialect_fmt_lexer::tokenize_for_dialect(text, options.dialect);
166 let lex_errors = lexed.errors.clone();
167 let parse = sql_dialect_fmt_parser::parse_lexed(text, options.dialect, lexed);
168 let highlighted = sql_dialect_fmt_highlight::highlight(text);
169 let mut diagnostics: Vec<_> = lex_errors
170 .iter()
171 .map(|err| make(to_range(err.range()), err.message.clone()))
172 .chain(
173 parse
174 .errors()
175 .iter()
176 .map(|err| make(to_range(err.range()), err.message.clone())),
177 )
178 .collect();
179 diagnostics.extend(lint::diagnostics_with_encoding(
180 text,
181 &highlighted.tokens,
182 &index,
183 lint_options,
184 encoding,
185 ));
186 diagnostics
187}
188
189pub fn hover(text: &str, position: Position) -> Option<Hover> {
192 hover_with_encoding(text, position, PositionEncoding::Utf16)
193}
194
195pub fn hover_with_encoding(
197 text: &str,
198 position: Position,
199 encoding: PositionEncoding,
200) -> Option<Hover> {
201 let index = LineIndex::new(text);
202 let info = sql_dialect_fmt_hover::hover_at(text, lsp_offset(&index, position, encoding))?;
203 let mut value = format!("**{}**\n\n{}", info.title, info.body);
204 if let Some(url) = info.docs_url {
205 value.push_str(&format!("\n\n[Snowflake docs]({url})"));
206 }
207 Some(Hover {
208 contents: HoverContents::Markup(MarkupContent {
209 kind: MarkupKind::Markdown,
210 value,
211 }),
212 range: Some(Range::new(
213 lsp_position(&index, info.range.start, encoding),
214 lsp_position(&index, info.range.end, encoding),
215 )),
216 })
217}
218
219pub fn document_symbols(text: &str, options: &FormatOptions) -> Vec<DocumentSymbol> {
221 document_symbols_with_encoding(text, options, PositionEncoding::Utf16)
222}
223
224pub fn document_symbols_with_encoding(
226 text: &str,
227 options: &FormatOptions,
228 encoding: PositionEncoding,
229) -> Vec<DocumentSymbol> {
230 let index = LineIndex::new(text);
231 let root = sql_dialect_fmt_parser::parse_with_dialect(text, options.dialect).syntax();
232 root.children()
233 .filter_map(|stmt| document_symbol_for_statement(&stmt, &index, encoding))
234 .collect()
235}
236
237#[derive(Clone, Debug)]
238struct SymbolToken {
239 kind: SyntaxKind,
240 text: String,
241 range: std::ops::Range<usize>,
242}
243
244fn document_symbol_for_statement(
245 stmt: &SyntaxNode,
246 index: &LineIndex<'_>,
247 encoding: PositionEncoding,
248) -> Option<DocumentSymbol> {
249 let tokens = significant_symbol_tokens(stmt);
250 let first = tokens.first()?;
251 let last = tokens.last().unwrap_or(first);
252 let range = byte_range_to_lsp(index, first.range.start..last.range.end, encoding);
253 let selection_range = statement_selection_range(&tokens)
254 .map(|range| byte_range_to_lsp(index, range, encoding))
255 .unwrap_or(range);
256 let name = statement_symbol_name(stmt.kind(), &tokens);
257 let kind = statement_symbol_kind(stmt.kind(), &tokens);
258 #[allow(deprecated)]
259 let symbol = DocumentSymbol {
260 name,
261 detail: None,
262 kind,
263 tags: None,
264 deprecated: None,
265 range,
266 selection_range,
267 children: None,
268 };
269 Some(symbol)
270}
271
272fn significant_symbol_tokens(node: &SyntaxNode) -> Vec<SymbolToken> {
273 node.descendants_with_tokens()
274 .filter_map(|element| element.into_token())
275 .filter(|token| !token.kind().is_trivia())
276 .map(|token| {
277 let range = token.text_range();
278 let start: usize = range.start().into();
279 let end: usize = range.end().into();
280 SymbolToken {
281 kind: token.kind(),
282 text: token.text().to_string(),
283 range: start..end,
284 }
285 })
286 .collect()
287}
288
289fn byte_range_to_lsp(
290 index: &LineIndex<'_>,
291 range: std::ops::Range<usize>,
292 encoding: PositionEncoding,
293) -> Range {
294 Range::new(
295 lsp_position(index, range.start, encoding),
296 lsp_position(index, range.end, encoding),
297 )
298}
299
300fn statement_selection_range(tokens: &[SymbolToken]) -> Option<std::ops::Range<usize>> {
301 create_name_range(tokens).or_else(|| tokens.first().map(|token| token.range.clone()))
302}
303
304fn statement_symbol_name(kind: SyntaxKind, tokens: &[SymbolToken]) -> String {
305 if let Some(label) = create_statement_label(tokens) {
306 return label;
307 }
308
309 match kind {
310 SyntaxKind::SELECT_STMT => "SELECT".to_string(),
311 SyntaxKind::WITH_QUERY => "WITH".to_string(),
312 SyntaxKind::INSERT_STMT => statement_words_until(tokens, 4, &["VALUES", "SELECT"]),
313 SyntaxKind::UPDATE_STMT => statement_words_until(tokens, 3, &["SET"]),
314 SyntaxKind::DELETE_STMT => statement_words_until(tokens, 4, &["WHERE", "USING"]),
315 SyntaxKind::MERGE_STMT => statement_words_until(tokens, 4, &["USING"]),
316 SyntaxKind::COPY_STMT => statement_words_until(tokens, 4, &["FROM", "FILES"]),
317 SyntaxKind::STAGE_FILE_STMT => statement_words_until(tokens, 1, &[]),
318 SyntaxKind::CALL_STMT => statement_words_until(tokens, 3, &["("]),
319 SyntaxKind::BLOCK_STMT => "BEGIN".to_string(),
320 SyntaxKind::SET_OP | SyntaxKind::FLOW_STMT => statement_words_until(tokens, 3, &[]),
321 _ => statement_words_until(tokens, 4, &[";", "AS", "WHERE"]),
322 }
323}
324
325fn statement_symbol_kind(kind: SyntaxKind, tokens: &[SymbolToken]) -> SymbolKind {
326 if let Some((object_kind, _)) = create_object_kind(tokens) {
327 return match object_kind {
328 "TABLE" | "DYNAMIC TABLE" => SymbolKind::STRUCT,
329 "VIEW" | "SEMANTIC VIEW" => SymbolKind::INTERFACE,
330 "FUNCTION" => SymbolKind::FUNCTION,
331 "PROCEDURE" => SymbolKind::METHOD,
332 "TASK" => SymbolKind::EVENT,
333 "WAREHOUSE" | "STAGE" | "FILE FORMAT" | "STREAM" | "SEQUENCE" => SymbolKind::OBJECT,
334 "SCHEMA" | "DATABASE" => SymbolKind::NAMESPACE,
335 _ => SymbolKind::OBJECT,
336 };
337 }
338
339 match kind {
340 SyntaxKind::SELECT_STMT | SyntaxKind::WITH_QUERY | SyntaxKind::SET_OP => {
341 SymbolKind::FUNCTION
342 }
343 SyntaxKind::INSERT_STMT
344 | SyntaxKind::UPDATE_STMT
345 | SyntaxKind::DELETE_STMT
346 | SyntaxKind::MERGE_STMT
347 | SyntaxKind::COPY_STMT
348 | SyntaxKind::STAGE_FILE_STMT
349 | SyntaxKind::CALL_STMT
350 | SyntaxKind::SET_STMT
351 | SyntaxKind::EXECUTE_STMT => SymbolKind::METHOD,
352 SyntaxKind::BLOCK_STMT | SyntaxKind::IF_STMT | SyntaxKind::LOOP_STMT => SymbolKind::MODULE,
353 _ => SymbolKind::OBJECT,
354 }
355}
356
357fn create_statement_label(tokens: &[SymbolToken]) -> Option<String> {
358 let (object_kind, name_start) = create_object_kind(tokens)?;
359 let name = dotted_name(tokens, skip_if_not_exists(tokens, name_start));
360 Some(match name {
361 Some(name) => format!("CREATE {object_kind} {name}"),
362 None => format!("CREATE {object_kind}"),
363 })
364}
365
366fn create_name_range(tokens: &[SymbolToken]) -> Option<std::ops::Range<usize>> {
367 let (_, name_start) = create_object_kind(tokens)?;
368 tokens
369 .iter()
370 .skip(skip_if_not_exists(tokens, name_start))
371 .find(|token| is_name_token(token))
372 .map(|token| token.range.clone())
373}
374
375fn create_object_kind(tokens: &[SymbolToken]) -> Option<(&'static str, usize)> {
376 let create_index = tokens.iter().position(|token| word(token, "CREATE"))?;
377 for index in create_index + 1..tokens.len() {
378 let token = &tokens[index];
379 if word(token, "DYNAMIC") && tokens.get(index + 1).is_some_and(|t| word(t, "TABLE")) {
380 return Some(("DYNAMIC TABLE", index + 2));
381 }
382 if word(token, "SEMANTIC") && tokens.get(index + 1).is_some_and(|t| word(t, "VIEW")) {
383 return Some(("SEMANTIC VIEW", index + 2));
384 }
385 if word(token, "FILE") && tokens.get(index + 1).is_some_and(|t| word(t, "FORMAT")) {
386 return Some(("FILE FORMAT", index + 2));
387 }
388 if word(token, "MASKING") && tokens.get(index + 1).is_some_and(|t| word(t, "POLICY")) {
389 return Some(("MASKING POLICY", index + 2));
390 }
391 if word(token, "ACCESS") && tokens.get(index + 1).is_some_and(|t| word(t, "POLICY")) {
392 return Some(("ACCESS POLICY", index + 2));
393 }
394
395 for candidate in [
396 "TABLE",
397 "VIEW",
398 "FUNCTION",
399 "PROCEDURE",
400 "TASK",
401 "WAREHOUSE",
402 "STAGE",
403 "SCHEMA",
404 "DATABASE",
405 "SEQUENCE",
406 "STREAM",
407 ] {
408 if word(token, candidate) {
409 return Some((candidate, index + 1));
410 }
411 }
412 }
413 None
414}
415
416fn skip_if_not_exists(tokens: &[SymbolToken], index: usize) -> usize {
417 if tokens.get(index).is_some_and(|token| word(token, "IF"))
418 && tokens
419 .get(index + 1)
420 .is_some_and(|token| word(token, "NOT"))
421 && tokens
422 .get(index + 2)
423 .is_some_and(|token| word(token, "EXISTS"))
424 {
425 index + 3
426 } else {
427 index
428 }
429}
430
431fn dotted_name(tokens: &[SymbolToken], start: usize) -> Option<String> {
432 let mut out = String::new();
433 let mut saw_name = false;
434 let mut allow_dot = false;
435
436 for token in tokens.iter().skip(start) {
437 if token.text == "." && allow_dot {
438 out.push('.');
439 allow_dot = false;
440 continue;
441 }
442 if is_name_token(token) {
443 out.push_str(&token.text);
444 saw_name = true;
445 allow_dot = true;
446 continue;
447 }
448 break;
449 }
450
451 while out.ends_with('.') {
452 out.pop();
453 }
454 saw_name.then_some(out)
455}
456
457fn statement_words_until(tokens: &[SymbolToken], max_words: usize, stops: &[&str]) -> String {
458 let mut words = Vec::new();
459 for token in tokens.iter().filter(|token| symbol_word(token)) {
460 if !words.is_empty() && stops.iter().any(|stop| word(token, stop)) {
461 break;
462 }
463 words.push(display_word(token));
464 if words.len() == max_words {
465 break;
466 }
467 }
468 if words.is_empty() {
469 "SQL".to_string()
470 } else {
471 words.join(" ")
472 }
473}
474
475fn display_word(token: &SymbolToken) -> String {
476 if token.kind.is_keyword() || token.kind == SyntaxKind::CONTEXTUAL_KEYWORD {
477 token.text.to_ascii_uppercase()
478 } else {
479 token.text.clone()
480 }
481}
482
483fn symbol_word(token: &SymbolToken) -> bool {
484 token.kind.is_keyword()
485 || matches!(
486 token.kind,
487 SyntaxKind::IDENT | SyntaxKind::QUOTED_IDENT | SyntaxKind::CONTEXTUAL_KEYWORD
488 )
489}
490
491fn is_name_token(token: &SymbolToken) -> bool {
492 matches!(
493 token.kind,
494 SyntaxKind::IDENT | SyntaxKind::QUOTED_IDENT | SyntaxKind::CONTEXTUAL_KEYWORD
495 )
496}
497
498fn word(token: &SymbolToken, expected: &str) -> bool {
499 symbol_word(token) && token.text.eq_ignore_ascii_case(expected)
500}
501
502pub fn completion_items() -> Vec<CompletionItem> {
504 let mut items = Vec::new();
505 items.extend(keyword_texts().map(|keyword| {
506 let label = keyword.to_ascii_uppercase();
507 completion_item(
508 &label,
509 CompletionItemKind::KEYWORD,
510 "SQL keyword",
511 "Keyword recognized by sql-dialect-fmt.",
512 None,
513 "1",
514 )
515 }));
516 items.extend(BUILTIN_TYPE_WORDS.iter().map(|label| {
517 completion_item(
518 label,
519 CompletionItemKind::TYPE_PARAMETER,
520 "SQL type",
521 type_documentation(label),
522 None,
523 "2",
524 )
525 }));
526 items.extend(SQL_SNIPPETS.iter().map(|snippet| {
527 completion_item(
528 snippet.label,
529 CompletionItemKind::SNIPPET,
530 "SQL snippet",
531 snippet.documentation,
532 Some(snippet.insert_text),
533 "3",
534 )
535 }));
536 items
537}
538
539fn type_documentation(label: &str) -> &'static str {
540 match label {
541 "NUMBER" => "Exact fixed-point numeric type.",
542 "DECIMAL" | "NUMERIC" => "Alias for NUMBER.",
543 "INT" | "INTEGER" | "BIGINT" => "Integer numeric alias.",
544 "FLOAT" | "DOUBLE" | "REAL" => "Approximate floating-point numeric type.",
545 "VARCHAR" => "Variable-length character data.",
546 "STRING" | "TEXT" => "Alias for VARCHAR.",
547 "CHAR" => "Character data.",
548 "BOOLEAN" => "TRUE/FALSE logical value.",
549 "VARIANT" => "Semi-structured value.",
550 "OBJECT" => "Semi-structured key/value object.",
551 "ARRAY" => "Semi-structured ordered collection.",
552 "MAP" => "Structured key/value collection.",
553 "VECTOR" => "Vector type for embeddings.",
554 "DATE" => "Calendar date without time of day.",
555 "TIME" => "Time of day without date.",
556 "TIMESTAMP" => "Timestamp family.",
557 "TIMESTAMP_NTZ" => "Timestamp without time zone.",
558 "TIMESTAMP_LTZ" => "Timestamp using the session time zone.",
559 "TIMESTAMP_TZ" => "Timestamp with explicit offset.",
560 "BINARY" => "Variable-length binary data.",
561 "GEOGRAPHY" => "Spherical geospatial data.",
562 "GEOMETRY" => "Planar geospatial data.",
563 _ => "Built-in SQL type.",
564 }
565}
566
567fn completion_item(
568 label: &str,
569 kind: CompletionItemKind,
570 detail: &str,
571 documentation: &str,
572 insert_text: Option<&str>,
573 sort_prefix: &str,
574) -> CompletionItem {
575 CompletionItem {
576 label: label.to_string(),
577 kind: Some(kind),
578 detail: Some(detail.to_string()),
579 documentation: Some(lsp_types::Documentation::MarkupContent(MarkupContent {
580 kind: MarkupKind::Markdown,
581 value: documentation.to_string(),
582 })),
583 insert_text: insert_text.map(str::to_string),
584 insert_text_format: insert_text.map(|_| InsertTextFormat::SNIPPET),
585 sort_text: Some(format!("{sort_prefix}-{label}")),
586 ..CompletionItem::default()
587 }
588}
589
590struct Snippet {
591 label: &'static str,
592 insert_text: &'static str,
593 documentation: &'static str,
594}
595
596const SQL_SNIPPETS: &[Snippet] = &[
597 Snippet {
598 label: "SELECT ... FROM ...",
599 insert_text: "SELECT ${1:columns}\nFROM ${2:table};",
600 documentation: "Scaffold a SELECT statement.",
601 },
602 Snippet {
603 label: "WITH ... AS (...)",
604 insert_text: "WITH ${1:cte} AS (\n SELECT ${2:*}\n FROM ${3:source}\n)\nSELECT ${4:*}\nFROM ${1:cte};",
605 documentation: "Scaffold a common table expression.",
606 },
607 Snippet {
608 label: "CREATE TABLE ...",
609 insert_text: "CREATE TABLE ${1:table_name} (\n ${2:column_name} ${3:NUMBER}\n);",
610 documentation: "Scaffold a CREATE TABLE statement.",
611 },
612 Snippet {
613 label: "INSERT INTO ...",
614 insert_text: "INSERT INTO ${1:table_name} (${2:columns})\nVALUES (${3:values});",
615 documentation: "Scaffold an INSERT statement.",
616 },
617 Snippet {
618 label: "CREATE PROCEDURE ...",
619 insert_text: "CREATE PROCEDURE ${1:procedure_name}(${2:args})\nRETURNS ${3:STRING}\nLANGUAGE SQL\nAS\n$$\nBEGIN\n ${4:RETURN 'ok';}\nEND;\n$$;",
620 documentation: "Scaffold a Snowflake SQL procedure.",
621 },
622];
623
624pub fn folding_ranges(text: &str) -> Vec<FoldingRange> {
627 let index = LineIndex::new(text);
628 let root = sql_dialect_fmt_parser::parse(text).syntax();
629 root.children()
630 .filter_map(|stmt| {
631 let mut tokens = stmt
634 .descendants_with_tokens()
635 .filter_map(|el| el.into_token())
636 .filter(|t| !t.kind().is_trivia());
637 let first = tokens.next()?;
638 let last = tokens.last().unwrap_or_else(|| first.clone());
639 let start = lsp_position(
640 &index,
641 first.text_range().start().into(),
642 PositionEncoding::Utf16,
643 )
644 .line;
645 let end = lsp_position(
646 &index,
647 last.text_range().end().into(),
648 PositionEncoding::Utf16,
649 )
650 .line;
651 (end > start).then_some(FoldingRange {
652 start_line: start,
653 end_line: end,
654 kind: Some(FoldingRangeKind::Region),
655 ..FoldingRange::default()
656 })
657 })
658 .collect()
659}
660
661pub fn apply_change(text: &str, range: Option<Range>, new_text: &str) -> String {
667 apply_change_with_encoding(text, range, new_text, PositionEncoding::Utf16)
668}
669
670pub fn apply_change_with_encoding(
672 text: &str,
673 range: Option<Range>,
674 new_text: &str,
675 encoding: PositionEncoding,
676) -> String {
677 let Some(range) = range else {
678 return new_text.to_string();
679 };
680 let index = LineIndex::new(text);
681 let a = lsp_offset(&index, range.start, encoding);
682 let b = lsp_offset(&index, range.end, encoding);
683 let (start, end) = (a.min(b), a.max(b));
684 let mut out = String::with_capacity(text.len() - (end - start) + new_text.len());
685 out.push_str(&text[..start]);
686 out.push_str(new_text);
687 out.push_str(&text[end..]);
688 out
689}
690
691pub fn semantic_tokens(text: &str) -> Vec<SemanticToken> {
701 semantic_tokens_with_encoding(text, PositionEncoding::Utf16)
702}
703
704pub fn semantic_tokens_with_encoding(text: &str, encoding: PositionEncoding) -> Vec<SemanticToken> {
706 let raw = match encoding {
707 PositionEncoding::Utf16 => semantic::semantic_tokens_lsp(text),
708 PositionEncoding::Utf8 => sql_dialect_fmt_highlight::semantic_tokens_lsp_utf8(text),
709 };
710 raw.into_iter()
711 .map(
712 |[delta_line, delta_start, length, token_type, token_modifiers_bitset]| SemanticToken {
713 delta_line,
714 delta_start,
715 length,
716 token_type,
717 token_modifiers_bitset,
718 },
719 )
720 .collect()
721}
722
723pub fn semantic_tokens_range_with_encoding(
725 text: &str,
726 range: Range,
727 encoding: PositionEncoding,
728) -> Vec<SemanticToken> {
729 let full = semantic_tokens_with_encoding(text, encoding);
730 let mut absolute = Vec::with_capacity(full.len());
731 let mut line = 0u32;
732 let mut start = 0u32;
733 for token in full {
734 line += token.delta_line;
735 start = if token.delta_line == 0 {
736 start + token.delta_start
737 } else {
738 token.delta_start
739 };
740 absolute.push(AbsoluteSemanticToken { line, start, token });
741 }
742
743 let mut previous: Option<(u32, u32)> = None;
744 absolute
745 .into_iter()
746 .filter(|token| semantic_token_intersects_range(token, range))
747 .map(|absolute| {
748 let delta_line = previous.map_or(absolute.line, |(line, _)| absolute.line - line);
749 let delta_start = if delta_line == 0 {
750 previous.map_or(absolute.start, |(_, start)| absolute.start - start)
751 } else {
752 absolute.start
753 };
754 previous = Some((absolute.line, absolute.start));
755 SemanticToken {
756 delta_line,
757 delta_start,
758 length: absolute.token.length,
759 token_type: absolute.token.token_type,
760 token_modifiers_bitset: absolute.token.token_modifiers_bitset,
761 }
762 })
763 .collect()
764}
765
766#[derive(Clone, Debug)]
767struct AbsoluteSemanticToken {
768 line: u32,
769 start: u32,
770 token: SemanticToken,
771}
772
773fn semantic_token_intersects_range(token: &AbsoluteSemanticToken, range: Range) -> bool {
774 let token_start = Position::new(token.line, token.start);
775 let token_end = Position::new(token.line, token.start + token.token.length);
776 position_lt(token_start, range.end) && position_lt(range.start, token_end)
777}
778
779fn position_lt(a: Position, b: Position) -> bool {
780 a.line < b.line || (a.line == b.line && a.character < b.character)
781}
782
783#[cfg(test)]
784mod tests {
785 use super::*;
786
787 #[test]
788 fn line_index_maps_offsets_to_utf16_positions() {
789 let text = "SELECT a\nFROM 芋;\n"; let index = LineIndex::new(text);
791 assert_eq!(
792 lsp_position(&index, 0, PositionEncoding::Utf16),
793 Position::new(0, 0)
794 );
795 assert_eq!(
796 lsp_position(&index, 7, PositionEncoding::Utf16),
797 Position::new(0, 7)
798 ); let from = text.find("FROM").unwrap();
800 assert_eq!(
801 lsp_position(&index, from, PositionEncoding::Utf16),
802 Position::new(1, 0)
803 );
804 let semicolon = text.find(';').unwrap();
805 assert_eq!(
806 lsp_position(&index, semicolon, PositionEncoding::Utf16),
807 Position::new(1, 6)
808 ); }
810
811 #[test]
812 fn line_index_maps_offsets_to_utf8_positions() {
813 let text = "SELECT a\nFROM 芋;\n";
814 let index = LineIndex::new(text);
815 let semicolon = text.find(';').unwrap();
816 assert_eq!(
817 lsp_position(&index, semicolon, PositionEncoding::Utf8),
818 Position::new(1, 8)
819 ); }
821
822 #[test]
823 fn formatting_replaces_the_whole_document() {
824 let edits = format_edits("select a,b from t", &FormatOptions::default());
825 assert_eq!(edits.len(), 1);
826 assert_eq!(edits[0].new_text, "SELECT a, b\nFROM t;\n");
827 assert_eq!(edits[0].range.start, Position::new(0, 0));
828 }
829
830 #[test]
831 fn already_formatted_input_yields_no_edits() {
832 let formatted = "SELECT a, b\nFROM t;\n";
833 assert!(format_edits(formatted, &FormatOptions::default()).is_empty());
834 }
835
836 #[test]
837 fn clean_sql_has_no_diagnostics() {
838 assert!(diagnostics("select 1").is_empty());
839 }
840
841 #[test]
842 fn broken_sql_reports_a_diagnostic() {
843 let diags = diagnostics("select from where");
844 assert!(!diags.is_empty());
845 assert_eq!(diags[0].severity, Some(DiagnosticSeverity::ERROR));
846 }
847
848 #[test]
849 fn lexer_errors_reach_diagnostics() {
850 let text = "SELECT 'oops";
854 let diags = diagnostics(text);
855 let lex_diag = diags
856 .iter()
857 .find(|d| d.message.contains("unterminated string"))
858 .expect("an unterminated-string diagnostic");
859 assert_eq!(lex_diag.severity, Some(DiagnosticSeverity::ERROR));
860 let quote = text.find('\'').unwrap() as u32;
861 assert_eq!(lex_diag.range.start, Position::new(0, quote));
862 assert!(lex_diag.range.end.character > lex_diag.range.start.character);
864 }
865
866 #[test]
867 fn parser_diagnostic_range_covers_the_token() {
868 let text = "MERGE tgt USING src ON a = b";
871 let diags = diagnostics(text);
872 let into = diags
873 .iter()
874 .find(|d| d.message == "expected INTO")
875 .expect("an INTO diagnostic");
876 let col = text.find("tgt").unwrap() as u32;
877 assert_eq!(into.range.start, Position::new(0, col));
878 assert_eq!(into.range.end, Position::new(0, col + 3));
879 }
880
881 #[test]
882 fn clean_sql_still_has_no_lexer_or_parser_diagnostics() {
883 assert!(diagnostics("SELECT a FROM t").is_empty());
884 }
885
886 #[test]
887 fn unsupported_embedded_language_is_a_warning() {
888 let text = "CREATE FUNCTION f() RETURNS STRING LANGUAGE RUBY AS $$x$$;";
889 let diags = diagnostics(text);
890 let language = diags
891 .iter()
892 .find(|d| d.message.contains("unsupported embedded language RUBY"))
893 .expect("unsupported-language diagnostic");
894 assert_eq!(language.severity, Some(DiagnosticSeverity::WARNING));
895 let col = text.find("RUBY").unwrap() as u32;
896 assert_eq!(language.range.start, Position::new(0, col));
897 assert_eq!(language.range.end, Position::new(0, col + 4));
898 }
899
900 #[test]
901 fn embedded_language_warning_does_not_fire_for_plain_columns_or_dynamic_sql() {
902 for text in [
903 "SELECT language FROM t;",
904 "EXECUTE IMMEDIATE $$ SELECT 1 $$;",
905 ] {
906 assert!(
907 diagnostics(text)
908 .iter()
909 .all(|diag| !diag.message.contains("unsupported embedded language")),
910 "{text}"
911 );
912 }
913 }
914
915 #[test]
916 fn select_wildcard_lint_is_a_warning() {
917 let text = "SELECT * FROM t;";
918 let diags = diagnostics(text);
919 let wildcard = diags
920 .iter()
921 .find(|d| d.message.contains("avoid SELECT *"))
922 .expect("SELECT * warning");
923 assert_eq!(wildcard.severity, Some(DiagnosticSeverity::WARNING));
924 let col = text.find('*').unwrap() as u32;
925 assert_eq!(wildcard.range.start, Position::new(0, col));
926 assert_eq!(wildcard.range.end, Position::new(0, col + 1));
927 }
928
929 #[test]
930 fn select_wildcard_lint_ignores_function_stars() {
931 let text = "SELECT count(*) FROM t;";
932 assert!(
933 diagnostics(text)
934 .iter()
935 .all(|diag| !diag.message.contains("avoid SELECT *")),
936 "{text}"
937 );
938 }
939
940 #[test]
941 fn large_in_list_lint_is_a_warning() {
942 let values = (0..101)
943 .map(|n| n.to_string())
944 .collect::<Vec<_>>()
945 .join(", ");
946 let text = format!("SELECT id FROM t WHERE id IN ({values});");
947 let diags = diagnostics(&text);
948 let in_list = diags
949 .iter()
950 .find(|d| d.message.contains("large IN list"))
951 .expect("large IN-list warning");
952 assert_eq!(in_list.severity, Some(DiagnosticSeverity::WARNING));
953 let col = text.find("IN").unwrap() as u32;
954 assert_eq!(in_list.range.start, Position::new(0, col));
955 }
956
957 #[test]
958 fn normal_in_list_and_subquery_have_no_lint() {
959 for text in [
960 "SELECT id FROM t WHERE id IN (1, 2, 3);",
961 "SELECT id FROM t WHERE id IN (SELECT id FROM src);",
962 ] {
963 assert!(
964 diagnostics(text)
965 .iter()
966 .all(|diag| !diag.message.contains("large IN list")),
967 "{text}"
968 );
969 }
970 }
971
972 #[test]
973 fn lint_diagnostics_have_codes_and_respect_options() {
974 let wildcard = diagnostics("SELECT * FROM t;")
975 .into_iter()
976 .find(|diag| diag.message.contains("avoid SELECT *"))
977 .expect("SELECT * lint");
978 assert_eq!(
979 wildcard.code,
980 Some(lsp_types::NumberOrString::String("SDF001".to_string()))
981 );
982
983 let text = "SELECT id FROM t WHERE id IN (1, 2, 3);";
984 let diags = diagnostics_with_lint_options(
985 text,
986 &FormatOptions::default(),
987 LintOptions {
988 large_in_list_threshold: 2,
989 ..LintOptions::default()
990 },
991 PositionEncoding::Utf16,
992 );
993 assert!(diags.iter().any(|diag| {
994 diag.code == Some(lsp_types::NumberOrString::String("SDF002".to_string()))
995 }));
996
997 let disabled = diagnostics_with_lint_options(
998 text,
999 &FormatOptions::default(),
1000 LintOptions {
1001 large_in_list: false,
1002 large_in_list_threshold: 2,
1003 ..LintOptions::default()
1004 },
1005 PositionEncoding::Utf16,
1006 );
1007 assert!(
1008 disabled
1009 .iter()
1010 .all(|diag| diag.code
1011 != Some(lsp_types::NumberOrString::String("SDF002".to_string())))
1012 );
1013 }
1014
1015 #[test]
1016 fn unsupported_embedded_language_has_a_code() {
1017 let diags = diagnostics("CREATE FUNCTION f() RETURNS STRING LANGUAGE RUBY AS $$x$$;");
1018 let language = diags
1019 .iter()
1020 .find(|diag| diag.message.contains("unsupported embedded language RUBY"))
1021 .expect("unsupported language lint");
1022 assert_eq!(
1023 language.code,
1024 Some(lsp_types::NumberOrString::String("SDF003".to_string()))
1025 );
1026 }
1027
1028 #[test]
1029 fn offset_is_the_inverse_of_position() {
1030 let text = "SELECT a\nFROM 芋;\n";
1031 let index = LineIndex::new(text);
1032 for offset in [
1033 0usize,
1034 7,
1035 text.find("FROM").unwrap(),
1036 text.find(';').unwrap(),
1037 ] {
1038 assert_eq!(
1039 lsp_offset(
1040 &index,
1041 lsp_position(&index, offset, PositionEncoding::Utf16),
1042 PositionEncoding::Utf16
1043 ),
1044 offset
1045 );
1046 }
1047 }
1048
1049 #[test]
1050 fn hover_describes_a_type() {
1051 let src = "select x::varchar from t";
1053 let col = src.find("varchar").unwrap() as u32;
1054 let hover = hover(src, Position::new(0, col)).expect("hover");
1055 assert!(hover.range.is_some());
1056 match hover.contents {
1057 HoverContents::Markup(m) => assert!(m.value.to_lowercase().contains("varchar")),
1058 _ => panic!("expected markup"),
1059 }
1060 }
1061
1062 #[test]
1063 fn apply_change_splices_an_incremental_edit() {
1064 let text = "hello\nworld\n";
1066 let range = Range::new(Position::new(1, 0), Position::new(1, 5));
1067 assert_eq!(apply_change(text, Some(range), "snow"), "hello\nsnow\n");
1068 }
1069
1070 #[test]
1071 fn apply_change_splices_utf8_encoded_ranges() {
1072 let text = "SELECT '長芋'\nFROM t\n";
1073 let start = Position::new(0, "SELECT '長".len() as u32);
1074 let end = Position::new(0, "SELECT '長芋".len() as u32);
1075 assert_eq!(
1076 apply_change_with_encoding(
1077 text,
1078 Some(Range::new(start, end)),
1079 "山芋",
1080 PositionEncoding::Utf8
1081 ),
1082 "SELECT '長山芋'\nFROM t\n"
1083 );
1084 }
1085
1086 #[test]
1087 fn apply_change_with_no_range_replaces_whole_document() {
1088 assert_eq!(apply_change("old", None, "new text"), "new text");
1089 }
1090
1091 #[test]
1092 fn folding_ranges_cover_multiline_statements() {
1093 let ranges = folding_ranges("select a,\nb\nfrom t;\n\nselect 1;");
1094 assert_eq!(ranges.len(), 1); assert_eq!(ranges[0].start_line, 0);
1096 assert_eq!(ranges[0].end_line, 2);
1097 }
1098
1099 #[test]
1100 fn document_symbols_name_top_level_statements() {
1101 let symbols = document_symbols(
1102 "CREATE TABLE db.t (id INT);\n\nSELECT id\nFROM db.t;",
1103 &FormatOptions::default(),
1104 );
1105 assert_eq!(symbols.len(), 2);
1106 assert_eq!(symbols[0].name, "CREATE TABLE db.t");
1107 assert_eq!(symbols[0].kind, SymbolKind::STRUCT);
1108 assert_eq!(symbols[0].selection_range.start, Position::new(0, 13));
1109 assert_eq!(symbols[1].name, "SELECT");
1110 assert_eq!(symbols[1].kind, SymbolKind::FUNCTION);
1111 }
1112
1113 #[test]
1114 fn document_symbols_name_stage_file_operations() {
1115 let symbols = document_symbols(
1116 "PUT file:///tmp/x.csv @stage;\nLIST @stage/path;",
1117 &FormatOptions::default(),
1118 );
1119 assert_eq!(symbols.len(), 2);
1120 assert_eq!(symbols[0].name, "PUT");
1121 assert_eq!(symbols[0].kind, SymbolKind::METHOD);
1122 assert_eq!(symbols[1].name, "LIST");
1123 assert_eq!(symbols[1].kind, SymbolKind::METHOD);
1124 }
1125
1126 #[test]
1127 fn completion_items_include_keywords_types_and_snippets() {
1128 let items = completion_items();
1129 assert!(items.iter().any(|item| {
1130 item.label == "SELECT" && item.kind == Some(CompletionItemKind::KEYWORD)
1131 }));
1132 assert!(items
1133 .iter()
1134 .any(|item| item.label == "NUMBER"
1135 && item.kind == Some(CompletionItemKind::TYPE_PARAMETER)));
1136 assert!(items.iter().any(|item| {
1137 item.label == "CREATE TABLE ..."
1138 && item.kind == Some(CompletionItemKind::SNIPPET)
1139 && item.insert_text_format == Some(InsertTextFormat::SNIPPET)
1140 }));
1141 }
1142
1143 #[test]
1144 fn semantic_tokens_tag_keywords() {
1145 let tokens = semantic_tokens("select a from t");
1146 assert!(!tokens.is_empty());
1147 assert_eq!(tokens[0].delta_line, 0);
1149 assert_eq!(tokens[0].delta_start, 0);
1150 assert_eq!(tokens[0].length, 6);
1151 assert_eq!(tokens[0].token_type, 0);
1152 assert_eq!(
1154 tokens[0].token_modifiers_bitset,
1155 semantic::SemanticTokenModifiers::DEFAULT_LIBRARY.bits()
1156 );
1157 }
1158
1159 #[test]
1160 fn server_legend_equals_the_highlighter_legend() {
1161 let advertised: Vec<String> = token_types()
1164 .iter()
1165 .map(|t| t.as_str().to_string())
1166 .collect();
1167 let expected: Vec<String> = semantic::SemanticTokenType::LEGEND
1168 .iter()
1169 .map(|t| t.name().to_string())
1170 .collect();
1171 assert_eq!(advertised, expected);
1172 assert_eq!(advertised.last().map(String::as_str), Some("function"));
1174
1175 let mods: Vec<String> = token_modifiers()
1177 .iter()
1178 .map(|m| m.as_str().to_string())
1179 .collect();
1180 assert_eq!(mods, vec!["documentation", "defaultLibrary"]);
1181 }
1182
1183 #[test]
1184 fn semantic_tokens_are_monotonic_and_never_panic_on_multiline() {
1185 let tokens = semantic_tokens("select 1 /* a\nb */ from t");
1187 assert!(tokens.iter().all(|t| t.length > 0));
1189 }
1190
1191 #[test]
1192 fn semantic_tokens_range_filters_and_reencodes_tokens() {
1193 let text = "SELECT a\nFROM t\nWHERE a = 1";
1194 let tokens = semantic_tokens_range_with_encoding(
1195 text,
1196 Range::new(Position::new(1, 0), Position::new(2, 0)),
1197 PositionEncoding::Utf16,
1198 );
1199 assert!(!tokens.is_empty());
1200 assert_eq!(tokens[0].delta_line, 1);
1201
1202 let mut line = 0u32;
1203 let mut start = 0u32;
1204 for token in tokens {
1205 line += token.delta_line;
1206 start = if token.delta_line == 0 {
1207 start + token.delta_start
1208 } else {
1209 token.delta_start
1210 };
1211 assert_eq!(line, 1);
1212 assert!(start + token.length <= 6);
1213 }
1214 }
1215}