Skip to main content

sql_dialect_fmt_lsp/
lib.rs

1//! LSP feature logic for Snowflake SQL: formatting, diagnostics, hover, folding, document symbols,
2//! completion, and semantic tokens.
3//!
4//! This module is deliberately transport-free (it never touches stdio or an `lsp-server`
5//! connection), so every feature is a pure `&str -> data` function that is unit-testable. The
6//! binary crate is the thin adapter that wires these into a language server.
7//!
8//! Positions follow the LSP convention: zero-based lines and **UTF-16** column offsets.
9
10use 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/// LSP position encoding negotiated with the client.
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum PositionEncoding {
29    /// UTF-16 code units, the LSP default.
30    Utf16,
31    /// UTF-8 byte offsets.
32    Utf8,
33}
34
35/// The semantic-token type legend, mirrored from the single source of truth in
36/// `sql-dialect-fmt-highlight` ([`semantic::SemanticTokenType::LEGEND`]). A token's `token_type` field is
37/// an index into this slice, so the order here is the contract with the editor (declared in the
38/// server's capabilities) and *must* equal the highlighter's legend — otherwise an editor would
39/// decode our token types against the wrong names.
40pub fn token_types() -> Vec<SemanticTokenType> {
41    semantic::SemanticTokenType::LEGEND
42        .iter()
43        .map(|ty| SemanticTokenType::new(ty.name()))
44        .collect()
45}
46
47/// The semantic-token modifier legend, mirrored from
48/// [`semantic::SemanticTokenModifiers::LEGEND`]. A token's `token_modifiers_bitset` is decoded
49/// bit-by-bit against this slice, so it too must match the highlighter.
50pub 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
94/// The edits to apply for `textDocument/formatting`: a single whole-document replacement, or an
95/// empty list when the input is already formatted (so the editor records no change).
96pub fn format_edits(text: &str, options: &FormatOptions) -> Vec<TextEdit> {
97    format_edits_with_encoding(text, options, PositionEncoding::Utf16)
98}
99
100/// Encoding-aware variant of [`format_edits`].
101pub 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
117/// The edits to apply for `textDocument/rangeFormatting`: reformat only the statements the range
118/// touches, as one minimal replacement, or an empty list when nothing changes.
119pub 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
123/// Encoding-aware variant of [`format_range_edits`].
124pub 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
146/// Diagnostics for `textDocument/publishDiagnostics`: both lexer and parser errors. Neither stage
147/// ever fails, so this is the set of *recovered* errors (empty for clean input).
148///
149/// Lexer errors (unterminated literals/comments, stray characters) are surfaced too — they are
150/// reported by the tokenizer before the parser runs, so the parser-only error list would otherwise
151/// miss them. We read them through the highlighter, which already re-exposes the lexer's errors.
152/// Each diagnostic's range covers the whole offending token (via its byte `range()`), not a single
153/// character, so editors underline the real span.
154pub fn diagnostics(text: &str) -> Vec<Diagnostic> {
155    diagnostics_with_options(text, &FormatOptions::default(), PositionEncoding::Utf16)
156}
157
158/// Encoding-aware variant of [`diagnostics`].
159pub fn diagnostics_with_encoding(text: &str, encoding: PositionEncoding) -> Vec<Diagnostic> {
160    diagnostics_with_options(text, &FormatOptions::default(), encoding)
161}
162
163/// Options- and encoding-aware variant of [`diagnostics`].
164pub 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
172/// Options-, lint-, and encoding-aware variant of [`diagnostics`].
173pub 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
218/// Hover information for `textDocument/hover`: the keyword/type/symbol description at `position`,
219/// rendered as Markdown with an optional docs link, scoped to the hovered token's range.
220pub fn hover(text: &str, position: Position) -> Option<Hover> {
221    hover_with_encoding(text, position, PositionEncoding::Utf16)
222}
223
224/// Encoding-aware variant of [`hover`].
225pub 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
248/// Document symbols for `textDocument/documentSymbol`: one outline item per top-level statement.
249pub fn document_symbols(text: &str, options: &FormatOptions) -> Vec<DocumentSymbol> {
250    document_symbols_with_encoding(text, options, PositionEncoding::Utf16)
251}
252
253/// Encoding-aware variant of [`document_symbols`].
254pub 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
531/// Static SQL completion items for `textDocument/completion`.
532pub 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
653/// Folding ranges for `textDocument/foldingRange`: one region per multi-line top-level statement,
654/// so an editor can collapse each statement in a script. The CST's root children are the statements.
655pub 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            // Use the span of the statement's significant tokens, so a leading/trailing blank line
661            // (attached as trivia) doesn't inflate a single-line statement into a foldable region.
662            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
690/// Apply one `textDocument/didChange` content change to `text`, returning the new document.
691///
692/// Supports incremental sync: a `Some(range)` splices `new_text` over the byte span the range
693/// covers; a `None` range is a whole-document replacement (the editor sent the full text). The
694/// range is treated as ordered and clamped to the document, so a malformed event can't panic.
695pub 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
699/// Encoding-aware variant of [`apply_change`].
700pub 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
720/// The delta-encoded semantic tokens for `textDocument/semanticTokens/full`.
721///
722/// This is a thin adapter over `sql-dialect-fmt-highlight`'s [`semantic::semantic_tokens_lsp`]: the
723/// highlighter already splits multi-line tokens (block comments, dollar-quoted strings) into one
724/// token per line — the LSP encoding requires each token to stay on a single line — computes UTF-16
725/// columns/lengths, and delta-encodes into `(deltaLine, deltaStartChar, length, tokenType,
726/// tokenModifiers)` quintuples against the same legend the server advertises. We only reshape each
727/// quintuple into an `lsp_types::SemanticToken`, **preserving** the modifier bitset (rather than
728/// hardcoding 0) so `defaultLibrary` / `documentation` modifiers reach the editor.
729pub fn semantic_tokens(text: &str) -> Vec<SemanticToken> {
730    semantic_tokens_with_encoding(text, PositionEncoding::Utf16)
731}
732
733/// Encoding-aware variant of [`semantic_tokens`].
734pub 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
752/// The delta-encoded semantic tokens that intersect `range`.
753pub 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"; // 芋 is one UTF-16 unit but 3 bytes
819        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        ); // the `a`
828        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        ); // FROM<sp>芋 = 6 utf16 units
838    }
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        ); // FROM<sp>芋 = 8 utf8 bytes
849    }
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        // A range on line 1 — the second statement.
869        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        // The edit is scoped to the second statement, not the top of the document.
874        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        // An unterminated string is a *lexer* error (the parser never sees it as a token boundary
899        // problem). It must still surface as a diagnostic, and its range must cover the whole
900        // unterminated literal, not a single character.
901        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        // Spans to end of the line (the whole literal), so end column > start column.
911        assert!(lex_diag.range.end.character > lex_diag.range.start.character);
912    }
913
914    #[test]
915    fn parser_diagnostic_range_covers_the_token() {
916        // `MERGE tgt ...` (no INTO) reports "expected INTO" at the `tgt` token; the LSP range must
917        // span the whole 3-character identifier, not one character.
918        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        // Hover over the `varchar` cast target should return a Snowflake type description.
1100        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        // Replace "world" (line 1, cols 0..5) with "snow".
1113        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); // only the first (multi-line) statement folds
1143        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        // The first token is `select`, a keyword (legend index 0), at line 0 column 0.
1196        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        // The keyword carries the `defaultLibrary` modifier — it must not be hardcoded to 0.
1201        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        // The advertised type legend must be exactly the highlighter's LEGEND, in order — this is
1210        // the contract an editor decodes `token_type` against.
1211        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        // It includes `function` (currently the last appended type) for Cortex/AISQL recognition.
1221        assert_eq!(advertised.last().map(String::as_str), Some("function"));
1222
1223        // Likewise the modifier legend mirrors the highlighter's.
1224        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        // A multi-line block comment must split into per-line tokens without panicking.
1234        let tokens = semantic_tokens("select 1 /* a\nb */ from t");
1235        // Deltas must be non-negative by construction (u32) and the stream stays consistent.
1236        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}