Skip to main content

nautilus_schema/analysis/
completion.rs

1//! Completion suggestions for `.nautilus` schema files.
2
3use super::{analyze, span_contains};
4use crate::ast::Declaration;
5use crate::token::{Token, TokenKind};
6
7/// The kind of a completion item.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum CompletionKind {
10    /// A language keyword (`model`, `enum`, …).
11    Keyword,
12    /// A scalar or user-defined field type.
13    Type,
14    /// A field-level attribute (`@id`, `@unique`, …).
15    FieldAttribute,
16    /// A model-level attribute (`@@id`, `@@map`, …).
17    ModelAttribute,
18    /// A reference to a model name.
19    ModelName,
20    /// A reference to an enum name.
21    EnumName,
22    /// A field name inside a model or datasource.
23    FieldName,
24}
25
26/// A single completion suggestion.
27#[derive(Debug, Clone, PartialEq)]
28pub struct CompletionItem {
29    /// The text displayed in the completion popup.
30    pub label: String,
31    /// The text to actually insert (defaults to `label` when `None`).
32    pub insert_text: Option<String>,
33    /// Whether `insert_text` uses LSP snippet syntax (`$1`, `${1:placeholder}`, etc.).
34    pub is_snippet: bool,
35    /// What kind of thing this item represents.
36    pub kind: CompletionKind,
37    /// Optional extra description shown in the completion popup.
38    pub detail: Option<String>,
39}
40
41impl CompletionItem {
42    pub(super) fn new(
43        label: impl Into<String>,
44        kind: CompletionKind,
45        detail: impl Into<Option<String>>,
46    ) -> Self {
47        Self {
48            label: label.into(),
49            insert_text: None,
50            is_snippet: false,
51            kind,
52            detail: detail.into(),
53        }
54    }
55
56    pub(super) fn with_insert(
57        label: impl Into<String>,
58        insert_text: impl Into<String>,
59        kind: CompletionKind,
60        detail: impl Into<Option<String>>,
61    ) -> Self {
62        Self {
63            label: label.into(),
64            insert_text: Some(insert_text.into()),
65            is_snippet: false,
66            kind,
67            detail: detail.into(),
68        }
69    }
70
71    pub(super) fn with_snippet(
72        label: impl Into<String>,
73        snippet: impl Into<String>,
74        kind: CompletionKind,
75        detail: impl Into<Option<String>>,
76    ) -> Self {
77        Self {
78            label: label.into(),
79            insert_text: Some(snippet.into()),
80            is_snippet: true,
81            kind,
82            detail: detail.into(),
83        }
84    }
85}
86
87/// Returns completions appropriate at `offset` (byte offset) in `source`.
88///
89/// Uses the parsed AST to determine context:
90/// - Outside all declarations → top-level keywords.
91/// - Inside a `datasource` or `generator` block → config key suggestions.
92/// - Inside a `model` block:
93///   - After `@` → field attribute names.
94///   - After `@@` → model attribute names.
95///   - Otherwise → scalar types, user-defined model/enum names, and common
96///     field attributes as a convenience.
97pub fn completion(source: &str, offset: usize) -> Vec<CompletionItem> {
98    let result = analyze(source);
99    let tokens = &result.tokens;
100
101    // Extract provider once for all provider-aware completions.
102    let provider: Option<String> = extract_provider_from_tokens(tokens);
103
104    if let Some(attr_name) = inside_attr_args_at(tokens, offset) {
105        let arg_index = attr_arg_index_at(tokens, offset).unwrap_or(0);
106        return attr_argument_completions(&attr_name, provider.as_deref(), arg_index);
107    }
108
109    let attr_ctx = attribute_context_at(tokens, offset);
110    if attr_ctx == AttributeContext::FieldAttr {
111        return field_attribute_completions();
112    }
113    if attr_ctx == AttributeContext::ModelAttr {
114        return model_attribute_completions();
115    }
116
117    if let Some(key) = config_value_context_at(tokens, offset) {
118        let block_kind = config_block_kind_at(tokens, offset);
119        let completions = config_value_completions(&key, block_kind);
120        if !completions.is_empty() {
121            return completions;
122        }
123    }
124
125    let ast = match &result.ast {
126        Some(a) => a,
127        None => {
128            // AST unavailable (e.g. fatal parse error).  Use the raw token
129            // stream to make a best-effort guess about the enclosing block.
130            return if is_inside_model_tokens(tokens, offset) {
131                scalar_type_completions(provider.as_deref())
132            } else {
133                top_level_completions()
134            };
135        }
136    };
137
138    // Gather user-defined names for cross-ref completions.
139    let user_models: Vec<String> = ast
140        .declarations
141        .iter()
142        .filter_map(|d| {
143            if let Declaration::Model(m) = d {
144                Some(m.name.value.clone())
145            } else {
146                None
147            }
148        })
149        .collect();
150
151    let user_enums: Vec<String> = ast
152        .declarations
153        .iter()
154        .filter_map(|d| {
155            if let Declaration::Enum(e) = d {
156                Some(e.name.value.clone())
157            } else {
158                None
159            }
160        })
161        .collect();
162
163    // Determine which top-level declaration contains the cursor.
164    let containing_decl = ast
165        .declarations
166        .iter()
167        .find(|d| span_contains(d.span(), offset));
168
169    match containing_decl {
170        None => {
171            // The offset isn't inside any parsed declaration. This can happen
172            // when error recovery dropped the enclosing block.  Fall back to
173            // the token stream to make a best-effort guess.
174            if is_inside_model_tokens(tokens, offset) {
175                let mut items = scalar_type_completions(provider.as_deref());
176                for name in &user_models {
177                    items.push(CompletionItem::new(
178                        name.clone(),
179                        CompletionKind::ModelName,
180                        Some("Model reference".to_string()),
181                    ));
182                }
183                for name in &user_enums {
184                    items.push(CompletionItem::new(
185                        name.clone(),
186                        CompletionKind::EnumName,
187                        Some("Enum reference".to_string()),
188                    ));
189                }
190                items
191            } else {
192                top_level_completions()
193            }
194        }
195
196        Some(Declaration::Datasource(_)) | Some(Declaration::Generator(_)) => {
197            datasource_generator_completions()
198        }
199
200        Some(Declaration::Enum(_)) => {
201            // Inside an enum body: only enum variants are meaningful here,
202            // nothing to complete (they are user-defined identifiers).
203            Vec::new()
204        }
205
206        Some(Declaration::Model(_)) | Some(Declaration::Type(_)) => {
207            let mut items = scalar_type_completions(provider.as_deref());
208            for name in &user_models {
209                items.push(CompletionItem::new(
210                    name.clone(),
211                    CompletionKind::ModelName,
212                    Some("Model reference".to_string()),
213                ));
214            }
215            for name in &user_enums {
216                items.push(CompletionItem::new(
217                    name.clone(),
218                    CompletionKind::EnumName,
219                    Some("Enum reference".to_string()),
220                ));
221            }
222            items
223        }
224    }
225}
226
227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
228enum ConfigBlockKind {
229    Datasource,
230    Generator,
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234enum AttributeContext {
235    FieldAttr,
236    ModelAttr,
237    None,
238}
239
240/// Returns the name of the attribute whose argument list contains `offset`,
241/// e.g. `"store"` when the cursor is inside `@store(|)`, `"relation"` for
242/// `@relation(|)`, etc.  Returns `None` when `offset` is not inside any
243/// attribute argument list.
244fn inside_attr_args_at(tokens: &[Token], offset: usize) -> Option<String> {
245    // Collect non-newline tokens that end at or before `offset`.
246    let relevant: Vec<&Token> = tokens
247        .iter()
248        .filter(|t| t.span.end <= offset && !matches!(t.kind, TokenKind::Newline))
249        .collect();
250
251    let mut depth: i32 = 0;
252    for tok in relevant.iter().rev() {
253        match tok.kind {
254            TokenKind::RParen => depth += 1,
255            TokenKind::LParen => {
256                if depth == 0 {
257                    // This unmatched `(` — check what precedes it.
258                    let lparen_start = tok.span.start;
259                    let before: Vec<&Token> = tokens
260                        .iter()
261                        .filter(|t| {
262                            t.span.end <= lparen_start && !matches!(t.kind, TokenKind::Newline)
263                        })
264                        .collect();
265                    if let Some(name_tok) = before.last() {
266                        if let TokenKind::Ident(attr_name) = &name_tok.kind {
267                            let attr_name = attr_name.clone();
268                            // Verify it is preceded by `@` or `@@`.
269                            let before_name: Vec<&Token> = tokens
270                                .iter()
271                                .filter(|t| {
272                                    t.span.end <= name_tok.span.start
273                                        && !matches!(t.kind, TokenKind::Newline)
274                                })
275                                .collect();
276                            if let Some(at_tok) = before_name.last() {
277                                if matches!(at_tok.kind, TokenKind::At | TokenKind::AtAt) {
278                                    return Some(attr_name);
279                                }
280                            }
281                        }
282                    }
283                    return None;
284                }
285                depth -= 1;
286            }
287            _ => {}
288        }
289    }
290    None
291}
292
293/// Detects whether `offset` sits in a config value position (`key = <cursor>`)
294/// within a datasource or generator block. Returns the key name if so.
295fn config_value_context_at(tokens: &[Token], offset: usize) -> Option<String> {
296    let mut eq_pos: Option<usize> = None;
297    let mut key_pos: Option<usize> = None;
298
299    for (i, tok) in tokens.iter().enumerate() {
300        if tok.span.end > offset {
301            break;
302        }
303        if tok.kind == TokenKind::Newline {
304            eq_pos = None;
305            key_pos = None;
306        } else if tok.kind == TokenKind::Equal {
307            eq_pos = Some(i);
308        } else if let TokenKind::Ident(_) = tok.kind {
309            if eq_pos.is_none() {
310                key_pos = Some(i);
311            }
312        }
313    }
314
315    let eq_idx = eq_pos?;
316    let key_idx = key_pos?;
317
318    if eq_idx != key_idx + 1 {
319        return None;
320    }
321
322    if let TokenKind::Ident(key) = &tokens[key_idx].kind {
323        return Some(key.clone());
324    }
325    None
326}
327
328/// Detect whether `offset` immediately follows a `@` or `@@` token.
329fn attribute_context_at(tokens: &[Token], offset: usize) -> AttributeContext {
330    // Find the last token whose span ends at or before `offset`.
331    let last = tokens
332        .iter()
333        .rfind(|t| t.span.end <= offset && !matches!(t.kind, TokenKind::Newline));
334
335    match last {
336        Some(t) if t.kind == TokenKind::AtAt => AttributeContext::ModelAttr,
337        Some(t) if t.kind == TokenKind::At => AttributeContext::FieldAttr,
338        // The cursor might be in the middle of an identifier that started
339        // after `@` — look one token further back.
340        Some(t) if matches!(t.kind, TokenKind::Ident(_)) => {
341            let before = tokens.iter().rfind(|tok| tok.span.end <= t.span.start);
342            match before {
343                Some(b) if b.kind == TokenKind::AtAt => AttributeContext::ModelAttr,
344                Some(b) if b.kind == TokenKind::At => AttributeContext::FieldAttr,
345                _ => AttributeContext::None,
346            }
347        }
348        _ => AttributeContext::None,
349    }
350}
351
352/// Returns `true` if `offset` appears to be inside a `model { … }` block,
353/// based purely on the token stream (no AST required).
354///
355/// Algorithm: scan backwards from `offset`, counting unmatched `}` / `{`.
356/// When we find an unmatched `{`, check if the two tokens before it are
357/// `<Ident>` and `model`.
358fn is_inside_model_tokens(tokens: &[Token], offset: usize) -> bool {
359    // Only consider tokens whose span ends at or before offset.
360    let relevant: Vec<&Token> = tokens.iter().filter(|t| t.span.end <= offset).collect();
361
362    let mut depth: i32 = 0;
363    for tok in relevant.iter().rev() {
364        match tok.kind {
365            TokenKind::RBrace => depth += 1,
366            TokenKind::LBrace => {
367                if depth == 0 {
368                    // This is the unmatched `{` enclosing `offset`.
369                    // Walk backwards past it to find the block header.
370                    let idx = tokens
371                        .iter()
372                        .position(|t| std::ptr::eq(t, *tok))
373                        .unwrap_or(0);
374                    // Find the last non-newline token before this `{`.
375                    let before: Vec<&Token> = tokens[..idx]
376                        .iter()
377                        .filter(|t| !matches!(t.kind, TokenKind::Newline))
378                        .collect();
379                    if let Some(name_tok) = before.last() {
380                        if matches!(name_tok.kind, TokenKind::Ident(_)) {
381                            let before_name: Vec<&Token> = tokens[..idx]
382                                .iter()
383                                .filter(|t| !matches!(t.kind, TokenKind::Newline))
384                                .rev()
385                                .skip(1)
386                                .take(1)
387                                .collect();
388                            if let Some(kw) = before_name.first() {
389                                return kw.kind == TokenKind::Model || kw.kind == TokenKind::Type;
390                            }
391                        }
392                    }
393                    return false;
394                }
395                depth -= 1;
396            }
397            _ => {}
398        }
399    }
400    false
401}
402
403/// Extract the datasource `provider` value from a token stream.
404///
405/// Looks for the pattern:  `datasource <ident> { … provider = "<value>" … }`
406/// Returns `Some("postgresql" | "mysql" | "sqlite")` when found, `None` otherwise.
407fn extract_provider_from_tokens(tokens: &[Token]) -> Option<String> {
408    let n = tokens.len();
409    for i in 0..n {
410        // Match `provider`
411        if let TokenKind::Ident(ref kw) = tokens[i].kind {
412            if kw != "provider" {
413                continue;
414            }
415        } else {
416            continue;
417        }
418        // Skip newlines to find `=`
419        let mut j = i + 1;
420        while j < n && matches!(tokens[j].kind, TokenKind::Newline) {
421            j += 1;
422        }
423        if j >= n || tokens[j].kind != TokenKind::Equal {
424            continue;
425        }
426        j += 1;
427        while j < n && matches!(tokens[j].kind, TokenKind::Newline) {
428            j += 1;
429        }
430        if j < n {
431            if let TokenKind::String(ref val) = tokens[j].kind {
432                let v = val.as_str();
433                if matches!(v, "postgresql" | "mysql" | "sqlite") {
434                    return Some(v.to_string());
435                }
436            }
437        }
438    }
439    None
440}
441
442/// Returns context-sensitive completions for the arguments of a specific attribute.
443///
444/// Called when the cursor is detected to be inside `@attr(|)` argument parens.
445/// Returns the 0-based argument index of `offset` inside the innermost
446/// unmatched `(...)`, scanning backwards through `tokens`.
447/// Returns `None` if not inside any parentheses.
448fn attr_arg_index_at(tokens: &[Token], offset: usize) -> Option<usize> {
449    let relevant: Vec<&Token> = tokens
450        .iter()
451        .filter(|t| t.span.end <= offset && !matches!(t.kind, TokenKind::Newline))
452        .collect();
453    let mut depth: i32 = 0;
454    let mut commas: usize = 0;
455    for tok in relevant.iter().rev() {
456        match tok.kind {
457            TokenKind::RParen => depth += 1,
458            TokenKind::LParen => {
459                if depth == 0 {
460                    return Some(commas);
461                }
462                depth -= 1;
463            }
464            TokenKind::Comma if depth == 0 => commas += 1,
465            _ => {}
466        }
467    }
468    None
469}
470
471fn attr_argument_completions(
472    attr_name: &str,
473    provider: Option<&str>,
474    arg_index: usize,
475) -> Vec<CompletionItem> {
476    match attr_name {
477        "store" => vec![CompletionItem::new(
478            "json",
479            CompletionKind::FieldAttribute,
480            Some("Serialize array as JSON in the database".to_string()),
481        )],
482        "relation" => vec![
483            CompletionItem::new(
484                "fields: []",
485                CompletionKind::FieldName,
486                Some("Local FK field(s) on this model".to_string()),
487            ),
488            CompletionItem::new(
489                "references: []",
490                CompletionKind::FieldName,
491                Some("Referenced field(s) on the target model".to_string()),
492            ),
493            CompletionItem::new(
494                "name: \"\"",
495                CompletionKind::FieldName,
496                Some(
497                    "Relation name (required when multiple relations to the same model)"
498                        .to_string(),
499                ),
500            ),
501            CompletionItem::new(
502                "onDelete: Cascade",
503                CompletionKind::FieldName,
504                Some("Referential action on parent record delete".to_string()),
505            ),
506            CompletionItem::new(
507                "onUpdate: Cascade",
508                CompletionKind::FieldName,
509                Some("Referential action on parent record update".to_string()),
510            ),
511        ],
512        "default" => vec![
513            CompletionItem::new(
514                "autoincrement()",
515                CompletionKind::Keyword,
516                Some("Auto-incrementing integer sequence".to_string()),
517            ),
518            CompletionItem::new(
519                "now()",
520                CompletionKind::Keyword,
521                Some("Current timestamp at insert time".to_string()),
522            ),
523            CompletionItem::new(
524                "uuid()",
525                CompletionKind::Keyword,
526                Some("Randomly generated UUID".to_string()),
527            ),
528            CompletionItem::new(
529                "cuid()",
530                CompletionKind::Keyword,
531                Some("Randomly generated CUID".to_string()),
532            ),
533            CompletionItem::new(
534                "dbgenerated(\"expr\")",
535                CompletionKind::Keyword,
536                Some("Database-level default expression".to_string()),
537            ),
538        ],
539        "computed" => match arg_index {
540            // First argument: the SQL expression — no predefined choices, but
541            // show a hint so the user knows what's expected.
542            0 => vec![CompletionItem::new(
543                "SQL expression",
544                CompletionKind::Keyword,
545                Some("e.g. price * quantity  or  first_name || ' ' || last_name".to_string()),
546            )],
547            // Second argument: storage kind.
548            _ => vec![
549                CompletionItem::new(
550                    "Stored",
551                    CompletionKind::Keyword,
552                    Some("Computed on write, persisted on disk (all databases)".to_string()),
553                ),
554                CompletionItem::new(
555                    "Virtual",
556                    CompletionKind::Keyword,
557                    Some("Computed on read, never stored (MySQL / SQLite only)".to_string()),
558                ),
559            ],
560        },
561        "index" => index_argument_completions(provider),
562        _ => vec![],
563    }
564}
565
566fn top_level_completions() -> Vec<CompletionItem> {
567    vec![
568        CompletionItem::new(
569            "model",
570            CompletionKind::Keyword,
571            Some("Define a data model".to_string()),
572        ),
573        CompletionItem::new(
574            "enum",
575            CompletionKind::Keyword,
576            Some("Define an enumeration".to_string()),
577        ),
578        CompletionItem::new(
579            "type",
580            CompletionKind::Keyword,
581            Some("Define a composite type".to_string()),
582        ),
583        CompletionItem::new(
584            "datasource",
585            CompletionKind::Keyword,
586            Some("Configure a data source".to_string()),
587        ),
588        CompletionItem::new(
589            "generator",
590            CompletionKind::Keyword,
591            Some("Configure code generation".to_string()),
592        ),
593    ]
594}
595
596/// Return argument completions for `@@index(…)`, filtered by DB provider when known.
597///
598/// All DB types:   BTree (default, always shown)
599/// PG + MySQL:     Hash
600/// PG only:        Gin, Gist, Brin
601/// MySQL only:     FullText
602fn index_argument_completions(provider: Option<&str>) -> Vec<CompletionItem> {
603    // Helper — (label, description) pairs for index type options.
604    struct TypeEntry {
605        label: &'static str,
606        desc: &'static str,
607        providers: &'static [&'static str],
608    }
609    let type_entries = [
610        TypeEntry {
611            label: "type: BTree",
612            desc: "B-Tree index — default on all databases",
613            providers: &["postgresql", "mysql", "sqlite"],
614        },
615        TypeEntry {
616            label: "type: Hash",
617            desc: "Hash index — PostgreSQL and MySQL 8+",
618            providers: &["postgresql", "mysql"],
619        },
620        TypeEntry {
621            label: "type: Gin",
622            desc: "GIN index — PostgreSQL only (arrays, JSONB, full-text)",
623            providers: &["postgresql"],
624        },
625        TypeEntry {
626            label: "type: Gist",
627            desc: "GiST index — PostgreSQL only (geometry, range types)",
628            providers: &["postgresql"],
629        },
630        TypeEntry {
631            label: "type: Brin",
632            desc: "BRIN index — PostgreSQL only (ordered large tables)",
633            providers: &["postgresql"],
634        },
635        TypeEntry {
636            label: "type: FullText",
637            desc: "FULLTEXT index — MySQL only",
638            providers: &["mysql"],
639        },
640    ];
641
642    let mut items: Vec<CompletionItem> = type_entries
643        .iter()
644        .filter(|e| {
645            // When provider is known, only show compatible types.
646            // When unknown, show all.
647            match provider {
648                Some(p) => e.providers.contains(&p),
649                None => true,
650            }
651        })
652        .map(|e| CompletionItem::new(e.label, CompletionKind::Keyword, Some(e.desc.to_string())))
653        .collect();
654
655    // Named-argument helpers.
656    items.push(CompletionItem::new(
657        "name: \"\"",
658        CompletionKind::FieldName,
659        Some("Logical developer name for this index".to_string()),
660    ));
661    items.push(CompletionItem::new(
662        "map: \"\"",
663        CompletionKind::FieldName,
664        Some("Physical DDL index name (overrides auto-generated idx_… name)".to_string()),
665    ));
666
667    items
668}
669
670fn scalar_type_completions(provider: Option<&str>) -> Vec<CompletionItem> {
671    let pg = matches!(provider, Some("postgresql") | None);
672    let pg_or_mysql = matches!(provider, Some("postgresql") | Some("mysql") | None);
673
674    let mut items = vec![
675        CompletionItem::new(
676            "String",
677            CompletionKind::Type,
678            Some("UTF-8 text → VARCHAR / TEXT".to_string()),
679        ),
680        CompletionItem::new(
681            "Boolean",
682            CompletionKind::Type,
683            Some("true / false → BOOLEAN".to_string()),
684        ),
685        CompletionItem::new(
686            "Int",
687            CompletionKind::Type,
688            Some("32-bit integer → INTEGER".to_string()),
689        ),
690        CompletionItem::new(
691            "BigInt",
692            CompletionKind::Type,
693            Some("64-bit integer → BIGINT".to_string()),
694        ),
695        CompletionItem::new(
696            "Float",
697            CompletionKind::Type,
698            Some("64-bit float → DOUBLE PRECISION".to_string()),
699        ),
700        CompletionItem::new(
701            "Decimal",
702            CompletionKind::Type,
703            Some("Exact decimal → NUMERIC".to_string()),
704        ),
705        CompletionItem::new(
706            "DateTime",
707            CompletionKind::Type,
708            Some("Timestamp with time zone → TIMESTAMPTZ".to_string()),
709        ),
710        CompletionItem::new(
711            "Bytes",
712            CompletionKind::Type,
713            Some("Binary data → BYTEA".to_string()),
714        ),
715        CompletionItem::new(
716            "Json",
717            CompletionKind::Type,
718            Some("JSON document → JSONB".to_string()),
719        ),
720        CompletionItem::new(
721            "Uuid",
722            CompletionKind::Type,
723            Some("UUID → UUID".to_string()),
724        ),
725    ];
726
727    if pg {
728        items.push(CompletionItem::new(
729            "Jsonb",
730            CompletionKind::Type,
731            Some("JSONB document → JSONB (PostgreSQL only)".to_string()),
732        ));
733        items.push(CompletionItem::new(
734            "Xml",
735            CompletionKind::Type,
736            Some("XML document → XML (PostgreSQL only)".to_string()),
737        ));
738    }
739
740    if pg_or_mysql {
741        items.push(CompletionItem::with_snippet(
742            "Char(n)",
743            "Char(${1:n})",
744            CompletionKind::Type,
745            Some("Fixed-length string → CHAR(n) (PostgreSQL and MySQL)".to_string()),
746        ));
747        items.push(CompletionItem::with_snippet(
748            "VarChar(n)",
749            "VarChar(${1:n})",
750            CompletionKind::Type,
751            Some("Variable-length string → VARCHAR(n) (PostgreSQL and MySQL)".to_string()),
752        ));
753    }
754
755    items
756}
757
758fn field_attribute_completions() -> Vec<CompletionItem> {
759    vec![
760        CompletionItem::new(
761            "id",
762            CompletionKind::FieldAttribute,
763            Some("Mark as primary key".to_string()),
764        ),
765        CompletionItem::new(
766            "unique",
767            CompletionKind::FieldAttribute,
768            Some("Add a unique constraint".to_string()),
769        ),
770        CompletionItem::new(
771            "default()",
772            CompletionKind::FieldAttribute,
773            Some("Set a default value".to_string()),
774        ),
775        CompletionItem::new(
776            "relation()",
777            CompletionKind::FieldAttribute,
778            Some("Define a relation".to_string()),
779        ),
780        CompletionItem::new(
781            "map(\"\")",
782            CompletionKind::FieldAttribute,
783            Some("Override the column name".to_string()),
784        ),
785        CompletionItem::new(
786            "store(json)",
787            CompletionKind::FieldAttribute,
788            Some("Store as JSON column".to_string()),
789        ),
790        CompletionItem::new(
791            "updatedAt",
792            CompletionKind::FieldAttribute,
793            Some("Auto-set to current timestamp on every write".to_string()),
794        ),
795        CompletionItem::with_snippet(
796            "computed(…, Stored)",
797            "computed(${1:expr}, ${2|Stored,Virtual|})",
798            CompletionKind::FieldAttribute,
799            Some("Database-generated column (Stored or Virtual)".to_string()),
800        ),
801        CompletionItem::with_snippet(
802            "check(…)",
803            "check(${1:expr})",
804            CompletionKind::FieldAttribute,
805            Some("Add a CHECK constraint on this field".to_string()),
806        ),
807    ]
808}
809
810fn model_attribute_completions() -> Vec<CompletionItem> {
811    vec![
812        CompletionItem::new(
813            "id([])",
814            CompletionKind::ModelAttribute,
815            Some("Composite primary key".to_string()),
816        ),
817        CompletionItem::new(
818            "unique([])",
819            CompletionKind::ModelAttribute,
820            Some("Composite unique constraint".to_string()),
821        ),
822        CompletionItem::new(
823            "index([])",
824            CompletionKind::ModelAttribute,
825            Some(
826                "Add a database index — optionally with type: BTree|Hash|Gin|Gist|Brin|FullText"
827                    .to_string(),
828            ),
829        ),
830        CompletionItem::new(
831            "map(\"\")",
832            CompletionKind::ModelAttribute,
833            Some("Override the table name".to_string()),
834        ),
835        CompletionItem::with_snippet(
836            "check(…)",
837            "check(${1:expr})",
838            CompletionKind::ModelAttribute,
839            Some("Add a table-level CHECK constraint".to_string()),
840        ),
841    ]
842}
843
844fn datasource_generator_completions() -> Vec<CompletionItem> {
845    vec![
846        CompletionItem::new(
847            "provider",
848            CompletionKind::FieldName,
849            Some("Database / generator provider".to_string()),
850        ),
851        CompletionItem::new(
852            "url",
853            CompletionKind::FieldName,
854            Some("Connection URL".to_string()),
855        ),
856        CompletionItem::new(
857            "output",
858            CompletionKind::FieldName,
859            Some("Output path for generated files".to_string()),
860        ),
861        CompletionItem::new(
862            "interface",
863            CompletionKind::FieldName,
864            Some("Client interface style: \"sync\" (default) or \"async\"".to_string()),
865        ),
866        CompletionItem::new(
867            "recursive_type_depth",
868            CompletionKind::FieldName,
869            Some(
870                "Depth of recursive include TypedDicts — Python client only (default: 5)"
871                    .to_string(),
872            ),
873        ),
874    ]
875}
876
877/// Detects whether `offset` is inside a `datasource` or `generator` block,
878/// by scanning the token stream backwards to find the enclosing block keyword.
879fn config_block_kind_at(tokens: &[Token], offset: usize) -> Option<ConfigBlockKind> {
880    let relevant: Vec<&Token> = tokens.iter().filter(|t| t.span.end <= offset).collect();
881
882    let mut depth: i32 = 0;
883    for tok in relevant.iter().rev() {
884        match tok.kind {
885            TokenKind::RBrace => depth += 1,
886            TokenKind::LBrace => {
887                if depth == 0 {
888                    let idx = tokens
889                        .iter()
890                        .position(|t| std::ptr::eq(t, *tok))
891                        .unwrap_or(0);
892                    // Block structure: keyword name { … }
893                    // The token before the name is the keyword.
894                    let before: Vec<&Token> = tokens[..idx]
895                        .iter()
896                        .filter(|t| !matches!(t.kind, TokenKind::Newline))
897                        .collect();
898                    if before.len() >= 2 {
899                        let kw_tok = &before[before.len() - 2];
900                        return match kw_tok.kind {
901                            TokenKind::Datasource => Some(ConfigBlockKind::Datasource),
902                            TokenKind::Generator => Some(ConfigBlockKind::Generator),
903                            _ => None,
904                        };
905                    }
906                    return None;
907                }
908                depth -= 1;
909            }
910            _ => {}
911        }
912    }
913    None
914}
915
916fn config_value_completions(key: &str, block_kind: Option<ConfigBlockKind>) -> Vec<CompletionItem> {
917    match key {
918        "provider" => match block_kind {
919            Some(ConfigBlockKind::Datasource) => vec![
920                CompletionItem::with_insert(
921                    "postgresql",
922                    "\"postgresql\"",
923                    CompletionKind::Keyword,
924                    Some("PostgreSQL database".to_string()),
925                ),
926                CompletionItem::with_insert(
927                    "mysql",
928                    "\"mysql\"",
929                    CompletionKind::Keyword,
930                    Some("MySQL database".to_string()),
931                ),
932                CompletionItem::with_insert(
933                    "sqlite",
934                    "\"sqlite\"",
935                    CompletionKind::Keyword,
936                    Some("SQLite database".to_string()),
937                ),
938            ],
939            Some(ConfigBlockKind::Generator) => vec![
940                CompletionItem::with_insert(
941                    "nautilus-client-rs",
942                    "\"nautilus-client-rs\"",
943                    CompletionKind::Keyword,
944                    Some("Rust client generator".to_string()),
945                ),
946                CompletionItem::with_insert(
947                    "nautilus-client-py",
948                    "\"nautilus-client-py\"",
949                    CompletionKind::Keyword,
950                    Some("Python client generator".to_string()),
951                ),
952                CompletionItem::with_insert(
953                    "nautilus-client-js",
954                    "\"nautilus-client-js\"",
955                    CompletionKind::Keyword,
956                    Some("JavaScript/TypeScript client generator".to_string()),
957                ),
958            ],
959            None => vec![
960                CompletionItem::with_insert(
961                    "postgresql",
962                    "\"postgresql\"",
963                    CompletionKind::Keyword,
964                    Some("PostgreSQL database".to_string()),
965                ),
966                CompletionItem::with_insert(
967                    "mysql",
968                    "\"mysql\"",
969                    CompletionKind::Keyword,
970                    Some("MySQL database".to_string()),
971                ),
972                CompletionItem::with_insert(
973                    "sqlite",
974                    "\"sqlite\"",
975                    CompletionKind::Keyword,
976                    Some("SQLite database".to_string()),
977                ),
978                CompletionItem::with_insert(
979                    "nautilus-client-rs",
980                    "\"nautilus-client-rs\"",
981                    CompletionKind::Keyword,
982                    Some("Rust client generator".to_string()),
983                ),
984                CompletionItem::with_insert(
985                    "nautilus-client-py",
986                    "\"nautilus-client-py\"",
987                    CompletionKind::Keyword,
988                    Some("Python client generator".to_string()),
989                ),
990                CompletionItem::with_insert(
991                    "nautilus-client-js",
992                    "\"nautilus-client-js\"",
993                    CompletionKind::Keyword,
994                    Some("JavaScript/TypeScript client generator".to_string()),
995                ),
996            ],
997        },
998        "interface" => vec![
999            CompletionItem::with_insert(
1000                "sync",
1001                "\"sync\"",
1002                CompletionKind::Keyword,
1003                Some("Synchronous client interface (default)".to_string()),
1004            ),
1005            CompletionItem::with_insert(
1006                "async",
1007                "\"async\"",
1008                CompletionKind::Keyword,
1009                Some("Asynchronous client interface".to_string()),
1010            ),
1011        ],
1012        _ => Vec::new(),
1013    }
1014}