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