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;
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
363fn 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
398fn 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 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
421fn 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
482fn 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
519fn 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
660fn 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
950fn 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}