1use super::{analyze, span_contains, AnalysisResult};
4use crate::ast::Declaration;
5use crate::token::{Token, TokenKind};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum CompletionKind {
10 Keyword,
12 Type,
14 FieldAttribute,
16 ModelAttribute,
18 ModelName,
20 EnumName,
22 FieldName,
24}
25
26#[derive(Debug, Clone, PartialEq)]
28pub struct CompletionItem {
29 pub label: String,
31 pub insert_text: Option<String>,
33 pub is_snippet: bool,
35 pub kind: CompletionKind,
37 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
87pub fn completion(source: &str, offset: usize) -> Vec<CompletionItem> {
98 let result = analyze(source);
99 completion_with_analysis(source, &result, offset)
100}
101
102pub 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 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 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 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
313fn 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;
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
364fn 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
399fn 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 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
422fn 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 let idx = tokens
436 .iter()
437 .position(|t| std::ptr::eq(t, *tok))
438 .unwrap_or(0);
439 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
486fn 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
523fn 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
664fn 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
949fn 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}