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                    let lparen_start = tok.span.start;
330                    let before: Vec<&Token> = tokens
331                        .iter()
332                        .filter(|t| {
333                            t.span.end <= lparen_start && !matches!(t.kind, TokenKind::Newline)
334                        })
335                        .collect();
336                    if let Some(name_tok) = before.last() {
337                        if let TokenKind::Ident(attr_name) = &name_tok.kind {
338                            let attr_name = attr_name.clone();
339                            let before_name: Vec<&Token> = tokens
340                                .iter()
341                                .filter(|t| {
342                                    t.span.end <= name_tok.span.start
343                                        && !matches!(t.kind, TokenKind::Newline)
344                                })
345                                .collect();
346                            if let Some(at_tok) = before_name.last() {
347                                if matches!(at_tok.kind, TokenKind::At | TokenKind::AtAt) {
348                                    return Some(attr_name);
349                                }
350                            }
351                        }
352                    }
353                    return None;
354                }
355                depth -= 1;
356            }
357            _ => {}
358        }
359    }
360    None
361}
362
363/// Detects whether `offset` sits in a config value position (`key = <cursor>`)
364/// within a datasource or generator block. Returns the key name if so.
365fn config_value_context_at(tokens: &[Token], offset: usize) -> Option<String> {
366    let mut eq_pos: Option<usize> = None;
367    let mut key_pos: Option<usize> = None;
368
369    for (i, tok) in tokens.iter().enumerate() {
370        if tok.span.end > offset {
371            break;
372        }
373        if tok.kind == TokenKind::Newline {
374            eq_pos = None;
375            key_pos = None;
376        } else if tok.kind == TokenKind::Equal {
377            eq_pos = Some(i);
378        } else if let TokenKind::Ident(_) = tok.kind {
379            if eq_pos.is_none() {
380                key_pos = Some(i);
381            }
382        }
383    }
384
385    let eq_idx = eq_pos?;
386    let key_idx = key_pos?;
387
388    if eq_idx != key_idx + 1 {
389        return None;
390    }
391
392    if let TokenKind::Ident(key) = &tokens[key_idx].kind {
393        return Some(key.clone());
394    }
395    None
396}
397
398/// Detect whether `offset` immediately follows a `@` or `@@` token.
399fn attribute_context_at(tokens: &[Token], offset: usize) -> AttributeContext {
400    let last = tokens
401        .iter()
402        .rfind(|t| t.span.end <= offset && !matches!(t.kind, TokenKind::Newline));
403
404    match last {
405        Some(t) if t.kind == TokenKind::AtAt => AttributeContext::ModelAttr,
406        Some(t) if t.kind == TokenKind::At => AttributeContext::FieldAttr,
407        // The cursor might be in the middle of an identifier that started
408        // after `@` — look one token further back.
409        Some(t) if matches!(t.kind, TokenKind::Ident(_)) => {
410            let before = tokens.iter().rfind(|tok| tok.span.end <= t.span.start);
411            match before {
412                Some(b) if b.kind == TokenKind::AtAt => AttributeContext::ModelAttr,
413                Some(b) if b.kind == TokenKind::At => AttributeContext::FieldAttr,
414                _ => AttributeContext::None,
415            }
416        }
417        _ => AttributeContext::None,
418    }
419}
420
421/// Returns the declaration block that appears to enclose `offset`, based
422/// purely on the token stream (no AST required).
423fn declaration_context_at_tokens(tokens: &[Token], offset: usize) -> DeclarationContext {
424    let relevant: Vec<&Token> = tokens.iter().filter(|t| t.span.end <= offset).collect();
425
426    let mut depth: i32 = 0;
427    for tok in relevant.iter().rev() {
428        match tok.kind {
429            TokenKind::RBrace => depth += 1,
430            TokenKind::LBrace => {
431                if depth == 0 {
432                    let idx = tokens
433                        .iter()
434                        .position(|t| std::ptr::eq(t, *tok))
435                        .unwrap_or(0);
436                    let before: Vec<&Token> = tokens[..idx]
437                        .iter()
438                        .filter(|t| !matches!(t.kind, TokenKind::Newline))
439                        .collect();
440                    if let Some(name_tok) = before.last() {
441                        if matches!(name_tok.kind, TokenKind::Ident(_)) {
442                            let before_name: Vec<&Token> = tokens[..idx]
443                                .iter()
444                                .filter(|t| !matches!(t.kind, TokenKind::Newline))
445                                .rev()
446                                .skip(1)
447                                .take(1)
448                                .collect();
449                            if let Some(kw) = before_name.first() {
450                                return match kw.kind {
451                                    TokenKind::Model => DeclarationContext::Model,
452                                    TokenKind::Type => DeclarationContext::Type,
453                                    _ => DeclarationContext::Other,
454                                };
455                            }
456                        }
457                    }
458                    return DeclarationContext::Other;
459                }
460                depth -= 1;
461            }
462            _ => {}
463        }
464    }
465    DeclarationContext::Other
466}
467
468fn user_enums_from_tokens(tokens: &[Token]) -> Vec<String> {
469    let mut enums = Vec::new();
470
471    for window in tokens.windows(2) {
472        if window[0].kind == TokenKind::Enum {
473            if let TokenKind::Ident(name) = &window[1].kind {
474                enums.push(name.clone());
475            }
476        }
477    }
478
479    enums
480}
481
482/// Extract the datasource `provider` value from a token stream.
483///
484/// Looks for the pattern:  `datasource <ident> { … provider = "<value>" … }`
485/// Returns `Some("postgresql" | "mysql" | "sqlite")` when found, `None` otherwise.
486fn extract_provider_from_tokens(tokens: &[Token]) -> Option<String> {
487    let n = tokens.len();
488    for i in 0..n {
489        if let TokenKind::Ident(ref kw) = tokens[i].kind {
490            if kw != "provider" {
491                continue;
492            }
493        } else {
494            continue;
495        }
496        let mut j = i + 1;
497        while j < n && matches!(tokens[j].kind, TokenKind::Newline) {
498            j += 1;
499        }
500        if j >= n || tokens[j].kind != TokenKind::Equal {
501            continue;
502        }
503        j += 1;
504        while j < n && matches!(tokens[j].kind, TokenKind::Newline) {
505            j += 1;
506        }
507        if j < n {
508            if let TokenKind::String(ref val) = tokens[j].kind {
509                let v = val.as_str();
510                if matches!(v, "postgresql" | "mysql" | "sqlite") {
511                    return Some(v.to_string());
512                }
513            }
514        }
515    }
516    None
517}
518
519/// Returns context-sensitive completions for the arguments of a specific attribute.
520///
521/// Called when the cursor is detected to be inside `@attr(|)` argument parens.
522/// Returns the 0-based argument index of `offset` inside the innermost
523/// unmatched `(...)`, scanning backwards through `tokens`.
524/// Returns `None` if not inside any parentheses.
525fn attr_arg_index_at(tokens: &[Token], offset: usize) -> Option<usize> {
526    let relevant: Vec<&Token> = tokens
527        .iter()
528        .filter(|t| t.span.end <= offset && !matches!(t.kind, TokenKind::Newline))
529        .collect();
530    let mut depth: i32 = 0;
531    let mut commas: usize = 0;
532    for tok in relevant.iter().rev() {
533        match tok.kind {
534            TokenKind::RParen => depth += 1,
535            TokenKind::LParen => {
536                if depth == 0 {
537                    return Some(commas);
538                }
539                depth -= 1;
540            }
541            TokenKind::Comma if depth == 0 => commas += 1,
542            _ => {}
543        }
544    }
545    None
546}
547
548fn attr_argument_completions(
549    attr_name: &str,
550    provider: Option<&str>,
551    arg_index: usize,
552) -> Vec<CompletionItem> {
553    match attr_name {
554        "store" => vec![CompletionItem::new(
555            "json",
556            CompletionKind::FieldAttribute,
557            Some("Serialize array as JSON in the database".to_string()),
558        )],
559        "relation" => vec![
560            CompletionItem::new(
561                "fields: []",
562                CompletionKind::FieldName,
563                Some("Local FK field(s) on this model".to_string()),
564            ),
565            CompletionItem::new(
566                "references: []",
567                CompletionKind::FieldName,
568                Some("Referenced field(s) on the target model".to_string()),
569            ),
570            CompletionItem::new(
571                "name: \"\"",
572                CompletionKind::FieldName,
573                Some(
574                    "Relation name (required when multiple relations to the same model)"
575                        .to_string(),
576                ),
577            ),
578            CompletionItem::new(
579                "onDelete: Cascade",
580                CompletionKind::FieldName,
581                Some("Referential action on parent record delete".to_string()),
582            ),
583            CompletionItem::new(
584                "onUpdate: Cascade",
585                CompletionKind::FieldName,
586                Some("Referential action on parent record update".to_string()),
587            ),
588        ],
589        "default" => vec![
590            CompletionItem::new(
591                "autoincrement()",
592                CompletionKind::Keyword,
593                Some("Auto-incrementing integer sequence".to_string()),
594            ),
595            CompletionItem::new(
596                "now()",
597                CompletionKind::Keyword,
598                Some("Current timestamp at insert time".to_string()),
599            ),
600            CompletionItem::new(
601                "uuid()",
602                CompletionKind::Keyword,
603                Some("Randomly generated UUID".to_string()),
604            ),
605        ],
606        "computed" => match arg_index {
607            0 => vec![CompletionItem::new(
608                "SQL expression",
609                CompletionKind::Keyword,
610                Some("e.g. price * quantity  or  first_name || ' ' || last_name".to_string()),
611            )],
612            _ => vec![
613                CompletionItem::new(
614                    "Stored",
615                    CompletionKind::Keyword,
616                    Some("Computed on write, persisted on disk (all databases)".to_string()),
617                ),
618                CompletionItem::new(
619                    "Virtual",
620                    CompletionKind::Keyword,
621                    Some("Computed on read, never stored (MySQL / SQLite only)".to_string()),
622                ),
623            ],
624        },
625        "index" => index_argument_completions(provider),
626        _ => vec![],
627    }
628}
629
630fn top_level_completions() -> Vec<CompletionItem> {
631    vec![
632        CompletionItem::new(
633            "model",
634            CompletionKind::Keyword,
635            Some("Define a data model".to_string()),
636        ),
637        CompletionItem::new(
638            "enum",
639            CompletionKind::Keyword,
640            Some("Define an enumeration".to_string()),
641        ),
642        CompletionItem::new(
643            "type",
644            CompletionKind::Keyword,
645            Some("Define a composite type".to_string()),
646        ),
647        CompletionItem::new(
648            "datasource",
649            CompletionKind::Keyword,
650            Some("Configure a data source".to_string()),
651        ),
652        CompletionItem::new(
653            "generator",
654            CompletionKind::Keyword,
655            Some("Configure code generation".to_string()),
656        ),
657    ]
658}
659
660/// Return argument completions for `@@index(…)`, filtered by DB provider when known.
661///
662/// All DB types:   BTree (default, always shown)
663/// PG + MySQL:     Hash
664/// PG only:        Gin, Gist, Brin
665/// MySQL only:     FullText
666fn index_argument_completions(provider: Option<&str>) -> Vec<CompletionItem> {
667    struct TypeEntry {
668        label: &'static str,
669        desc: &'static str,
670        providers: &'static [&'static str],
671    }
672    let type_entries = [
673        TypeEntry {
674            label: "type: BTree",
675            desc: "B-Tree index — default on all databases",
676            providers: &["postgresql", "mysql", "sqlite"],
677        },
678        TypeEntry {
679            label: "type: Hash",
680            desc: "Hash index — PostgreSQL and MySQL 8+",
681            providers: &["postgresql", "mysql"],
682        },
683        TypeEntry {
684            label: "type: Gin",
685            desc: "GIN index — PostgreSQL only (arrays, JSONB, full-text)",
686            providers: &["postgresql"],
687        },
688        TypeEntry {
689            label: "type: Gist",
690            desc: "GiST index — PostgreSQL only (geometry, range types)",
691            providers: &["postgresql"],
692        },
693        TypeEntry {
694            label: "type: Brin",
695            desc: "BRIN index — PostgreSQL only (ordered large tables)",
696            providers: &["postgresql"],
697        },
698        TypeEntry {
699            label: "type: FullText",
700            desc: "FULLTEXT index — MySQL only",
701            providers: &["mysql"],
702        },
703    ];
704
705    let mut items: Vec<CompletionItem> = type_entries
706        .iter()
707        .filter(|e| match provider {
708            Some(p) => e.providers.contains(&p),
709            None => true,
710        })
711        .map(|e| CompletionItem::new(e.label, CompletionKind::Keyword, Some(e.desc.to_string())))
712        .collect();
713
714    items.push(CompletionItem::new(
715        "name: \"\"",
716        CompletionKind::FieldName,
717        Some("Logical developer name for this index".to_string()),
718    ));
719    items.push(CompletionItem::new(
720        "map: \"\"",
721        CompletionKind::FieldName,
722        Some("Physical DDL index name (overrides auto-generated idx_… name)".to_string()),
723    ));
724
725    items
726}
727
728fn scalar_type_completions(provider: Option<&str>) -> Vec<CompletionItem> {
729    let pg = matches!(provider, Some("postgresql") | None);
730    let pg_or_mysql = matches!(provider, Some("postgresql") | Some("mysql") | None);
731
732    let mut items = vec![
733        CompletionItem::new(
734            "String",
735            CompletionKind::Type,
736            Some("UTF-8 text -> VARCHAR / TEXT".to_string()),
737        ),
738        CompletionItem::new(
739            "Boolean",
740            CompletionKind::Type,
741            Some("true / false -> BOOLEAN".to_string()),
742        ),
743        CompletionItem::new(
744            "Int",
745            CompletionKind::Type,
746            Some("32-bit integer -> INTEGER".to_string()),
747        ),
748        CompletionItem::new(
749            "BigInt",
750            CompletionKind::Type,
751            Some("64-bit integer -> BIGINT".to_string()),
752        ),
753        CompletionItem::new(
754            "Float",
755            CompletionKind::Type,
756            Some("64-bit float -> DOUBLE PRECISION".to_string()),
757        ),
758        CompletionItem::new(
759            "Decimal",
760            CompletionKind::Type,
761            Some("Exact decimal -> NUMERIC".to_string()),
762        ),
763        CompletionItem::new(
764            "DateTime",
765            CompletionKind::Type,
766            Some("Timestamp with time zone -> TIMESTAMPTZ".to_string()),
767        ),
768        CompletionItem::new(
769            "Bytes",
770            CompletionKind::Type,
771            Some("Binary data -> BYTEA".to_string()),
772        ),
773        CompletionItem::new(
774            "Json",
775            CompletionKind::Type,
776            Some("JSON document -> JSONB".to_string()),
777        ),
778        CompletionItem::new(
779            "Uuid",
780            CompletionKind::Type,
781            Some("UUID -> UUID".to_string()),
782        ),
783    ];
784
785    if pg {
786        items.push(CompletionItem::new(
787            "Jsonb",
788            CompletionKind::Type,
789            Some("JSONB document -> JSONB (PostgreSQL only)".to_string()),
790        ));
791        items.push(CompletionItem::new(
792            "Xml",
793            CompletionKind::Type,
794            Some("XML document -> XML (PostgreSQL only)".to_string()),
795        ));
796    }
797
798    if pg_or_mysql {
799        items.push(CompletionItem::with_snippet(
800            "Char(n)",
801            "Char(${1:n})",
802            CompletionKind::Type,
803            Some("Fixed-length string -> CHAR(n) (PostgreSQL and MySQL)".to_string()),
804        ));
805        items.push(CompletionItem::with_snippet(
806            "VarChar(n)",
807            "VarChar(${1:n})",
808            CompletionKind::Type,
809            Some("Variable-length string -> VARCHAR(n) (PostgreSQL and MySQL)".to_string()),
810        ));
811    }
812
813    items
814}
815
816fn field_attribute_completions() -> Vec<CompletionItem> {
817    vec![
818        CompletionItem::new(
819            "id",
820            CompletionKind::FieldAttribute,
821            Some("Mark as primary key".to_string()),
822        ),
823        CompletionItem::new(
824            "unique",
825            CompletionKind::FieldAttribute,
826            Some("Add a unique constraint".to_string()),
827        ),
828        CompletionItem::new(
829            "default()",
830            CompletionKind::FieldAttribute,
831            Some("Set a default value".to_string()),
832        ),
833        CompletionItem::new(
834            "relation()",
835            CompletionKind::FieldAttribute,
836            Some("Define a relation".to_string()),
837        ),
838        CompletionItem::new(
839            "map(\"\")",
840            CompletionKind::FieldAttribute,
841            Some("Override the column name".to_string()),
842        ),
843        CompletionItem::new(
844            "store(json)",
845            CompletionKind::FieldAttribute,
846            Some("Store as JSON column".to_string()),
847        ),
848        CompletionItem::new(
849            "updatedAt",
850            CompletionKind::FieldAttribute,
851            Some("Auto-set to current timestamp on every write".to_string()),
852        ),
853        CompletionItem::with_snippet(
854            "computed(…, Stored)",
855            "computed(${1:expr}, ${2|Stored,Virtual|})",
856            CompletionKind::FieldAttribute,
857            Some("Database-generated column (Stored or Virtual)".to_string()),
858        ),
859        CompletionItem::with_snippet(
860            "check(…)",
861            "check(${1:expr})",
862            CompletionKind::FieldAttribute,
863            Some("Add a CHECK constraint on this field".to_string()),
864        ),
865    ]
866}
867
868fn model_attribute_completions() -> Vec<CompletionItem> {
869    vec![
870        CompletionItem::new(
871            "id([])",
872            CompletionKind::ModelAttribute,
873            Some("Composite primary key".to_string()),
874        ),
875        CompletionItem::new(
876            "unique([])",
877            CompletionKind::ModelAttribute,
878            Some("Composite unique constraint".to_string()),
879        ),
880        CompletionItem::new(
881            "index([])",
882            CompletionKind::ModelAttribute,
883            Some(
884                "Add a database index — optionally with type: BTree|Hash|Gin|Gist|Brin|FullText"
885                    .to_string(),
886            ),
887        ),
888        CompletionItem::new(
889            "map(\"\")",
890            CompletionKind::ModelAttribute,
891            Some("Override the table name".to_string()),
892        ),
893        CompletionItem::with_snippet(
894            "check(…)",
895            "check(${1:expr})",
896            CompletionKind::ModelAttribute,
897            Some("Add a table-level CHECK constraint".to_string()),
898        ),
899    ]
900}
901
902fn datasource_field_completions() -> Vec<CompletionItem> {
903    vec![
904        CompletionItem::new(
905            "provider",
906            CompletionKind::FieldName,
907            Some("Database provider".to_string()),
908        ),
909        CompletionItem::new(
910            "url",
911            CompletionKind::FieldName,
912            Some("Connection URL".to_string()),
913        ),
914        CompletionItem::new(
915            "direct_url",
916            CompletionKind::FieldName,
917            Some("Direct admin/introspection URL".to_string()),
918        ),
919    ]
920}
921
922fn generator_field_completions() -> Vec<CompletionItem> {
923    vec![
924        CompletionItem::new(
925            "provider",
926            CompletionKind::FieldName,
927            Some("Client generator provider".to_string()),
928        ),
929        CompletionItem::new(
930            "output",
931            CompletionKind::FieldName,
932            Some("Output path for generated files".to_string()),
933        ),
934        CompletionItem::new(
935            "interface",
936            CompletionKind::FieldName,
937            Some("Client interface style: \"sync\" (default) or \"async\"".to_string()),
938        ),
939        CompletionItem::new(
940            "recursive_type_depth",
941            CompletionKind::FieldName,
942            Some(
943                "Depth of recursive include TypedDicts — Python client only (default: 5)"
944                    .to_string(),
945            ),
946        ),
947    ]
948}
949
950/// Detects whether `offset` is inside a `datasource` or `generator` block,
951/// by scanning the token stream backwards to find the enclosing block keyword.
952fn config_block_kind_at(tokens: &[Token], offset: usize) -> Option<ConfigBlockKind> {
953    let relevant: Vec<&Token> = tokens.iter().filter(|t| t.span.end <= offset).collect();
954
955    let mut depth: i32 = 0;
956    for tok in relevant.iter().rev() {
957        match tok.kind {
958            TokenKind::RBrace => depth += 1,
959            TokenKind::LBrace => {
960                if depth == 0 {
961                    let idx = tokens
962                        .iter()
963                        .position(|t| std::ptr::eq(t, *tok))
964                        .unwrap_or(0);
965                    let before: Vec<&Token> = tokens[..idx]
966                        .iter()
967                        .filter(|t| !matches!(t.kind, TokenKind::Newline))
968                        .collect();
969                    if before.len() >= 2 {
970                        let kw_tok = &before[before.len() - 2];
971                        return match kw_tok.kind {
972                            TokenKind::Datasource => Some(ConfigBlockKind::Datasource),
973                            TokenKind::Generator => Some(ConfigBlockKind::Generator),
974                            _ => None,
975                        };
976                    }
977                    return None;
978                }
979                depth -= 1;
980            }
981            _ => {}
982        }
983    }
984    None
985}
986
987fn config_value_completions(key: &str, block_kind: Option<ConfigBlockKind>) -> Vec<CompletionItem> {
988    match key {
989        "provider" => match block_kind {
990            Some(ConfigBlockKind::Datasource) => vec![
991                CompletionItem::with_insert(
992                    "postgresql",
993                    "\"postgresql\"",
994                    CompletionKind::Keyword,
995                    Some("PostgreSQL database".to_string()),
996                ),
997                CompletionItem::with_insert(
998                    "mysql",
999                    "\"mysql\"",
1000                    CompletionKind::Keyword,
1001                    Some("MySQL database".to_string()),
1002                ),
1003                CompletionItem::with_insert(
1004                    "sqlite",
1005                    "\"sqlite\"",
1006                    CompletionKind::Keyword,
1007                    Some("SQLite database".to_string()),
1008                ),
1009            ],
1010            Some(ConfigBlockKind::Generator) => vec![
1011                CompletionItem::with_insert(
1012                    "nautilus-client-rs",
1013                    "\"nautilus-client-rs\"",
1014                    CompletionKind::Keyword,
1015                    Some("Rust client generator".to_string()),
1016                ),
1017                CompletionItem::with_insert(
1018                    "nautilus-client-py",
1019                    "\"nautilus-client-py\"",
1020                    CompletionKind::Keyword,
1021                    Some("Python client generator".to_string()),
1022                ),
1023                CompletionItem::with_insert(
1024                    "nautilus-client-js",
1025                    "\"nautilus-client-js\"",
1026                    CompletionKind::Keyword,
1027                    Some("JavaScript/TypeScript client generator".to_string()),
1028                ),
1029            ],
1030            None => vec![
1031                CompletionItem::with_insert(
1032                    "postgresql",
1033                    "\"postgresql\"",
1034                    CompletionKind::Keyword,
1035                    Some("PostgreSQL database".to_string()),
1036                ),
1037                CompletionItem::with_insert(
1038                    "mysql",
1039                    "\"mysql\"",
1040                    CompletionKind::Keyword,
1041                    Some("MySQL database".to_string()),
1042                ),
1043                CompletionItem::with_insert(
1044                    "sqlite",
1045                    "\"sqlite\"",
1046                    CompletionKind::Keyword,
1047                    Some("SQLite database".to_string()),
1048                ),
1049                CompletionItem::with_insert(
1050                    "nautilus-client-rs",
1051                    "\"nautilus-client-rs\"",
1052                    CompletionKind::Keyword,
1053                    Some("Rust client generator".to_string()),
1054                ),
1055                CompletionItem::with_insert(
1056                    "nautilus-client-py",
1057                    "\"nautilus-client-py\"",
1058                    CompletionKind::Keyword,
1059                    Some("Python client generator".to_string()),
1060                ),
1061                CompletionItem::with_insert(
1062                    "nautilus-client-js",
1063                    "\"nautilus-client-js\"",
1064                    CompletionKind::Keyword,
1065                    Some("JavaScript/TypeScript client generator".to_string()),
1066                ),
1067            ],
1068        },
1069        "interface" => vec![
1070            CompletionItem::with_insert(
1071                "sync",
1072                "\"sync\"",
1073                CompletionKind::Keyword,
1074                Some("Synchronous client interface (default)".to_string()),
1075            ),
1076            CompletionItem::with_insert(
1077                "async",
1078                "\"async\"",
1079                CompletionKind::Keyword,
1080                Some("Asynchronous client interface".to_string()),
1081            ),
1082        ],
1083        _ => Vec::new(),
1084    }
1085}