Skip to main content

nautilus_schema/analysis/
completion.rs

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