use super::{analyze, span_contains, AnalysisResult};
use crate::ast::Declaration;
use crate::token::{Token, TokenKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompletionKind {
Keyword,
Type,
FieldAttribute,
ModelAttribute,
ModelName,
EnumName,
FieldName,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CompletionItem {
pub label: String,
pub insert_text: Option<String>,
pub is_snippet: bool,
pub kind: CompletionKind,
pub detail: Option<String>,
}
impl CompletionItem {
pub(super) fn new(
label: impl Into<String>,
kind: CompletionKind,
detail: impl Into<Option<String>>,
) -> Self {
Self {
label: label.into(),
insert_text: None,
is_snippet: false,
kind,
detail: detail.into(),
}
}
pub(super) fn with_insert(
label: impl Into<String>,
insert_text: impl Into<String>,
kind: CompletionKind,
detail: impl Into<Option<String>>,
) -> Self {
Self {
label: label.into(),
insert_text: Some(insert_text.into()),
is_snippet: false,
kind,
detail: detail.into(),
}
}
pub(super) fn with_snippet(
label: impl Into<String>,
snippet: impl Into<String>,
kind: CompletionKind,
detail: impl Into<Option<String>>,
) -> Self {
Self {
label: label.into(),
insert_text: Some(snippet.into()),
is_snippet: true,
kind,
detail: detail.into(),
}
}
}
pub fn completion(source: &str, offset: usize) -> Vec<CompletionItem> {
let result = analyze(source);
completion_with_analysis(source, &result, offset)
}
pub fn completion_with_analysis(
_source: &str,
result: &AnalysisResult,
offset: usize,
) -> Vec<CompletionItem> {
let tokens = &result.tokens;
let provider: Option<String> = extract_provider_from_tokens(tokens);
if let Some(attr_name) = inside_attr_args_at(tokens, offset) {
let arg_index = attr_arg_index_at(tokens, offset).unwrap_or(0);
return attr_argument_completions(&attr_name, provider.as_deref(), arg_index);
}
let attr_ctx = attribute_context_at(tokens, offset);
if attr_ctx == AttributeContext::FieldAttr {
return field_attribute_completions();
}
if attr_ctx == AttributeContext::ModelAttr {
return model_attribute_completions();
}
if let Some(key) = config_value_context_at(tokens, offset) {
let block_kind = config_block_kind_at(tokens, offset);
let completions = config_value_completions(&key, block_kind);
if !completions.is_empty() {
return completions;
}
}
let ast = match &result.ast {
Some(a) => a,
None => {
return match declaration_context_at_tokens(tokens, offset) {
DeclarationContext::Model => scalar_type_completions(provider.as_deref()),
DeclarationContext::Type => {
let mut items = scalar_type_completions(provider.as_deref());
for name in user_enums_from_tokens(tokens) {
items.push(CompletionItem::new(
name,
CompletionKind::EnumName,
Some("Enum reference".to_string()),
));
}
items
}
DeclarationContext::Other => top_level_completions(),
};
}
};
let user_models: Vec<String> = ast
.declarations
.iter()
.filter_map(|d| {
if let Declaration::Model(m) = d {
Some(m.name.value.clone())
} else {
None
}
})
.collect();
let user_enums: Vec<String> = ast
.declarations
.iter()
.filter_map(|d| {
if let Declaration::Enum(e) = d {
Some(e.name.value.clone())
} else {
None
}
})
.collect();
let user_composite_types: Vec<String> = ast
.declarations
.iter()
.filter_map(|d| {
if let Declaration::Type(t) = d {
Some(t.name.value.clone())
} else {
None
}
})
.collect();
let containing_decl = ast
.declarations
.iter()
.find(|d| span_contains(d.span(), offset));
match containing_decl {
None => {
match declaration_context_at_tokens(tokens, offset) {
DeclarationContext::Model => {
let mut items = scalar_type_completions(provider.as_deref());
for name in &user_models {
items.push(CompletionItem::new(
name.clone(),
CompletionKind::ModelName,
Some("Model reference".to_string()),
));
}
for name in &user_enums {
items.push(CompletionItem::new(
name.clone(),
CompletionKind::EnumName,
Some("Enum reference".to_string()),
));
}
for name in &user_composite_types {
items.push(CompletionItem::new(
name.clone(),
CompletionKind::Type,
Some("Composite type reference".to_string()),
));
}
items
}
DeclarationContext::Type => {
let mut items = scalar_type_completions(provider.as_deref());
for name in &user_enums {
items.push(CompletionItem::new(
name.clone(),
CompletionKind::EnumName,
Some("Enum reference".to_string()),
));
}
items
}
DeclarationContext::Other => top_level_completions(),
}
}
Some(Declaration::Datasource(_)) => datasource_field_completions(),
Some(Declaration::Generator(_)) => generator_field_completions(),
Some(Declaration::Enum(_)) => {
Vec::new()
}
Some(Declaration::Model(_)) => {
let mut items = scalar_type_completions(provider.as_deref());
for name in &user_models {
items.push(CompletionItem::new(
name.clone(),
CompletionKind::ModelName,
Some("Model reference".to_string()),
));
}
for name in &user_enums {
items.push(CompletionItem::new(
name.clone(),
CompletionKind::EnumName,
Some("Enum reference".to_string()),
));
}
for name in &user_composite_types {
items.push(CompletionItem::new(
name.clone(),
CompletionKind::Type,
Some("Composite type reference".to_string()),
));
}
items
}
Some(Declaration::Type(_)) => {
let mut items = scalar_type_completions(provider.as_deref());
for name in &user_enums {
items.push(CompletionItem::new(
name.clone(),
CompletionKind::EnumName,
Some("Enum reference".to_string()),
));
}
items
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ConfigBlockKind {
Datasource,
Generator,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AttributeContext {
FieldAttr,
ModelAttr,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DeclarationContext {
Model,
Type,
Other,
}
fn inside_attr_args_at(tokens: &[Token], offset: usize) -> Option<String> {
let relevant: Vec<&Token> = tokens
.iter()
.filter(|t| t.span.end <= offset && !matches!(t.kind, TokenKind::Newline))
.collect();
let mut depth: i32 = 0;
for tok in relevant.iter().rev() {
match tok.kind {
TokenKind::RParen => depth += 1,
TokenKind::LParen => {
if depth == 0 {
let lparen_start = tok.span.start;
let before: Vec<&Token> = tokens
.iter()
.filter(|t| {
t.span.end <= lparen_start && !matches!(t.kind, TokenKind::Newline)
})
.collect();
if let Some(name_tok) = before.last() {
if let TokenKind::Ident(attr_name) = &name_tok.kind {
let attr_name = attr_name.clone();
let before_name: Vec<&Token> = tokens
.iter()
.filter(|t| {
t.span.end <= name_tok.span.start
&& !matches!(t.kind, TokenKind::Newline)
})
.collect();
if let Some(at_tok) = before_name.last() {
if matches!(at_tok.kind, TokenKind::At | TokenKind::AtAt) {
return Some(attr_name);
}
}
}
}
return None;
}
depth -= 1;
}
_ => {}
}
}
None
}
fn config_value_context_at(tokens: &[Token], offset: usize) -> Option<String> {
let mut eq_pos: Option<usize> = None;
let mut key_pos: Option<usize> = None;
for (i, tok) in tokens.iter().enumerate() {
if tok.span.end > offset {
break;
}
if tok.kind == TokenKind::Newline {
eq_pos = None;
key_pos = None;
} else if tok.kind == TokenKind::Equal {
eq_pos = Some(i);
} else if let TokenKind::Ident(_) = tok.kind {
if eq_pos.is_none() {
key_pos = Some(i);
}
}
}
let eq_idx = eq_pos?;
let key_idx = key_pos?;
if eq_idx != key_idx + 1 {
return None;
}
if let TokenKind::Ident(key) = &tokens[key_idx].kind {
return Some(key.clone());
}
None
}
fn attribute_context_at(tokens: &[Token], offset: usize) -> AttributeContext {
let last = tokens
.iter()
.rfind(|t| t.span.end <= offset && !matches!(t.kind, TokenKind::Newline));
match last {
Some(t) if t.kind == TokenKind::AtAt => AttributeContext::ModelAttr,
Some(t) if t.kind == TokenKind::At => AttributeContext::FieldAttr,
Some(t) if matches!(t.kind, TokenKind::Ident(_)) => {
let before = tokens.iter().rfind(|tok| tok.span.end <= t.span.start);
match before {
Some(b) if b.kind == TokenKind::AtAt => AttributeContext::ModelAttr,
Some(b) if b.kind == TokenKind::At => AttributeContext::FieldAttr,
_ => AttributeContext::None,
}
}
_ => AttributeContext::None,
}
}
fn declaration_context_at_tokens(tokens: &[Token], offset: usize) -> DeclarationContext {
let relevant: Vec<&Token> = tokens.iter().filter(|t| t.span.end <= offset).collect();
let mut depth: i32 = 0;
for tok in relevant.iter().rev() {
match tok.kind {
TokenKind::RBrace => depth += 1,
TokenKind::LBrace => {
if depth == 0 {
let idx = tokens
.iter()
.position(|t| std::ptr::eq(t, *tok))
.unwrap_or(0);
let before: Vec<&Token> = tokens[..idx]
.iter()
.filter(|t| !matches!(t.kind, TokenKind::Newline))
.collect();
if let Some(name_tok) = before.last() {
if matches!(name_tok.kind, TokenKind::Ident(_)) {
let before_name: Vec<&Token> = tokens[..idx]
.iter()
.filter(|t| !matches!(t.kind, TokenKind::Newline))
.rev()
.skip(1)
.take(1)
.collect();
if let Some(kw) = before_name.first() {
return match kw.kind {
TokenKind::Model => DeclarationContext::Model,
TokenKind::Type => DeclarationContext::Type,
_ => DeclarationContext::Other,
};
}
}
}
return DeclarationContext::Other;
}
depth -= 1;
}
_ => {}
}
}
DeclarationContext::Other
}
fn user_enums_from_tokens(tokens: &[Token]) -> Vec<String> {
let mut enums = Vec::new();
for window in tokens.windows(2) {
if window[0].kind == TokenKind::Enum {
if let TokenKind::Ident(name) = &window[1].kind {
enums.push(name.clone());
}
}
}
enums
}
fn extract_provider_from_tokens(tokens: &[Token]) -> Option<String> {
let n = tokens.len();
for i in 0..n {
if let TokenKind::Ident(ref kw) = tokens[i].kind {
if kw != "provider" {
continue;
}
} else {
continue;
}
let mut j = i + 1;
while j < n && matches!(tokens[j].kind, TokenKind::Newline) {
j += 1;
}
if j >= n || tokens[j].kind != TokenKind::Equal {
continue;
}
j += 1;
while j < n && matches!(tokens[j].kind, TokenKind::Newline) {
j += 1;
}
if j < n {
if let TokenKind::String(ref val) = tokens[j].kind {
let v = val.as_str();
if matches!(v, "postgresql" | "mysql" | "sqlite") {
return Some(v.to_string());
}
}
}
}
None
}
fn attr_arg_index_at(tokens: &[Token], offset: usize) -> Option<usize> {
let relevant: Vec<&Token> = tokens
.iter()
.filter(|t| t.span.end <= offset && !matches!(t.kind, TokenKind::Newline))
.collect();
let mut depth: i32 = 0;
let mut commas: usize = 0;
for tok in relevant.iter().rev() {
match tok.kind {
TokenKind::RParen => depth += 1,
TokenKind::LParen => {
if depth == 0 {
return Some(commas);
}
depth -= 1;
}
TokenKind::Comma if depth == 0 => commas += 1,
_ => {}
}
}
None
}
fn attr_argument_completions(
attr_name: &str,
provider: Option<&str>,
arg_index: usize,
) -> Vec<CompletionItem> {
match attr_name {
"store" => vec![CompletionItem::new(
"json",
CompletionKind::FieldAttribute,
Some("Serialize array as JSON in the database".to_string()),
)],
"relation" => vec![
CompletionItem::new(
"fields: []",
CompletionKind::FieldName,
Some("Local FK field(s) on this model".to_string()),
),
CompletionItem::new(
"references: []",
CompletionKind::FieldName,
Some("Referenced field(s) on the target model".to_string()),
),
CompletionItem::new(
"name: \"\"",
CompletionKind::FieldName,
Some(
"Relation name (required when multiple relations to the same model)"
.to_string(),
),
),
CompletionItem::new(
"onDelete: Cascade",
CompletionKind::FieldName,
Some("Referential action on parent record delete".to_string()),
),
CompletionItem::new(
"onUpdate: Cascade",
CompletionKind::FieldName,
Some("Referential action on parent record update".to_string()),
),
],
"default" => vec![
CompletionItem::new(
"autoincrement()",
CompletionKind::Keyword,
Some("Auto-incrementing integer sequence".to_string()),
),
CompletionItem::new(
"now()",
CompletionKind::Keyword,
Some("Current timestamp at insert time".to_string()),
),
CompletionItem::new(
"uuid()",
CompletionKind::Keyword,
Some("Randomly generated UUID".to_string()),
),
],
"computed" => match arg_index {
0 => vec![CompletionItem::new(
"SQL expression",
CompletionKind::Keyword,
Some("e.g. price * quantity or first_name || ' ' || last_name".to_string()),
)],
_ => vec![
CompletionItem::new(
"Stored",
CompletionKind::Keyword,
Some("Computed on write, persisted on disk (all databases)".to_string()),
),
CompletionItem::new(
"Virtual",
CompletionKind::Keyword,
Some("Computed on read, never stored (MySQL / SQLite only)".to_string()),
),
],
},
"index" => index_argument_completions(provider),
_ => vec![],
}
}
fn top_level_completions() -> Vec<CompletionItem> {
vec![
CompletionItem::new(
"model",
CompletionKind::Keyword,
Some("Define a data model".to_string()),
),
CompletionItem::new(
"enum",
CompletionKind::Keyword,
Some("Define an enumeration".to_string()),
),
CompletionItem::new(
"type",
CompletionKind::Keyword,
Some("Define a composite type".to_string()),
),
CompletionItem::new(
"datasource",
CompletionKind::Keyword,
Some("Configure a data source".to_string()),
),
CompletionItem::new(
"generator",
CompletionKind::Keyword,
Some("Configure code generation".to_string()),
),
]
}
fn index_argument_completions(provider: Option<&str>) -> Vec<CompletionItem> {
struct TypeEntry {
label: &'static str,
desc: &'static str,
providers: &'static [&'static str],
}
let type_entries = [
TypeEntry {
label: "type: BTree",
desc: "B-Tree index — default on all databases",
providers: &["postgresql", "mysql", "sqlite"],
},
TypeEntry {
label: "type: Hash",
desc: "Hash index — PostgreSQL and MySQL 8+",
providers: &["postgresql", "mysql"],
},
TypeEntry {
label: "type: Gin",
desc: "GIN index — PostgreSQL only (arrays, JSONB, full-text)",
providers: &["postgresql"],
},
TypeEntry {
label: "type: Gist",
desc: "GiST index — PostgreSQL only (geometry, range types)",
providers: &["postgresql"],
},
TypeEntry {
label: "type: Brin",
desc: "BRIN index — PostgreSQL only (ordered large tables)",
providers: &["postgresql"],
},
TypeEntry {
label: "type: FullText",
desc: "FULLTEXT index — MySQL only",
providers: &["mysql"],
},
];
let mut items: Vec<CompletionItem> = type_entries
.iter()
.filter(|e| match provider {
Some(p) => e.providers.contains(&p),
None => true,
})
.map(|e| CompletionItem::new(e.label, CompletionKind::Keyword, Some(e.desc.to_string())))
.collect();
items.push(CompletionItem::new(
"name: \"\"",
CompletionKind::FieldName,
Some("Logical developer name for this index".to_string()),
));
items.push(CompletionItem::new(
"map: \"\"",
CompletionKind::FieldName,
Some("Physical DDL index name (overrides auto-generated idx_… name)".to_string()),
));
items
}
fn scalar_type_completions(provider: Option<&str>) -> Vec<CompletionItem> {
let pg = matches!(provider, Some("postgresql") | None);
let pg_or_mysql = matches!(provider, Some("postgresql") | Some("mysql") | None);
let mut items = vec![
CompletionItem::new(
"String",
CompletionKind::Type,
Some("UTF-8 text -> VARCHAR / TEXT".to_string()),
),
CompletionItem::new(
"Boolean",
CompletionKind::Type,
Some("true / false -> BOOLEAN".to_string()),
),
CompletionItem::new(
"Int",
CompletionKind::Type,
Some("32-bit integer -> INTEGER".to_string()),
),
CompletionItem::new(
"BigInt",
CompletionKind::Type,
Some("64-bit integer -> BIGINT".to_string()),
),
CompletionItem::new(
"Float",
CompletionKind::Type,
Some("64-bit float -> DOUBLE PRECISION".to_string()),
),
CompletionItem::new(
"Decimal",
CompletionKind::Type,
Some("Exact decimal -> NUMERIC".to_string()),
),
CompletionItem::new(
"DateTime",
CompletionKind::Type,
Some("Timestamp with time zone -> TIMESTAMPTZ".to_string()),
),
CompletionItem::new(
"Bytes",
CompletionKind::Type,
Some("Binary data -> BYTEA".to_string()),
),
CompletionItem::new(
"Json",
CompletionKind::Type,
Some("JSON document -> JSONB".to_string()),
),
CompletionItem::new(
"Uuid",
CompletionKind::Type,
Some("UUID -> UUID".to_string()),
),
];
if pg {
items.push(CompletionItem::new(
"Jsonb",
CompletionKind::Type,
Some("JSONB document -> JSONB (PostgreSQL only)".to_string()),
));
items.push(CompletionItem::new(
"Xml",
CompletionKind::Type,
Some("XML document -> XML (PostgreSQL only)".to_string()),
));
}
if pg_or_mysql {
items.push(CompletionItem::with_snippet(
"Char(n)",
"Char(${1:n})",
CompletionKind::Type,
Some("Fixed-length string -> CHAR(n) (PostgreSQL and MySQL)".to_string()),
));
items.push(CompletionItem::with_snippet(
"VarChar(n)",
"VarChar(${1:n})",
CompletionKind::Type,
Some("Variable-length string -> VARCHAR(n) (PostgreSQL and MySQL)".to_string()),
));
}
items
}
fn field_attribute_completions() -> Vec<CompletionItem> {
vec![
CompletionItem::new(
"id",
CompletionKind::FieldAttribute,
Some("Mark as primary key".to_string()),
),
CompletionItem::new(
"unique",
CompletionKind::FieldAttribute,
Some("Add a unique constraint".to_string()),
),
CompletionItem::new(
"default()",
CompletionKind::FieldAttribute,
Some("Set a default value".to_string()),
),
CompletionItem::new(
"relation()",
CompletionKind::FieldAttribute,
Some("Define a relation".to_string()),
),
CompletionItem::new(
"map(\"\")",
CompletionKind::FieldAttribute,
Some("Override the column name".to_string()),
),
CompletionItem::new(
"store(json)",
CompletionKind::FieldAttribute,
Some("Store as JSON column".to_string()),
),
CompletionItem::new(
"updatedAt",
CompletionKind::FieldAttribute,
Some("Auto-set to current timestamp on every write".to_string()),
),
CompletionItem::with_snippet(
"computed(…, Stored)",
"computed(${1:expr}, ${2|Stored,Virtual|})",
CompletionKind::FieldAttribute,
Some("Database-generated column (Stored or Virtual)".to_string()),
),
CompletionItem::with_snippet(
"check(…)",
"check(${1:expr})",
CompletionKind::FieldAttribute,
Some("Add a CHECK constraint on this field".to_string()),
),
]
}
fn model_attribute_completions() -> Vec<CompletionItem> {
vec![
CompletionItem::new(
"id([])",
CompletionKind::ModelAttribute,
Some("Composite primary key".to_string()),
),
CompletionItem::new(
"unique([])",
CompletionKind::ModelAttribute,
Some("Composite unique constraint".to_string()),
),
CompletionItem::new(
"index([])",
CompletionKind::ModelAttribute,
Some(
"Add a database index — optionally with type: BTree|Hash|Gin|Gist|Brin|FullText"
.to_string(),
),
),
CompletionItem::new(
"map(\"\")",
CompletionKind::ModelAttribute,
Some("Override the table name".to_string()),
),
CompletionItem::with_snippet(
"check(…)",
"check(${1:expr})",
CompletionKind::ModelAttribute,
Some("Add a table-level CHECK constraint".to_string()),
),
]
}
fn datasource_field_completions() -> Vec<CompletionItem> {
vec![
CompletionItem::new(
"provider",
CompletionKind::FieldName,
Some("Database provider".to_string()),
),
CompletionItem::new(
"url",
CompletionKind::FieldName,
Some("Connection URL".to_string()),
),
]
}
fn generator_field_completions() -> Vec<CompletionItem> {
vec![
CompletionItem::new(
"provider",
CompletionKind::FieldName,
Some("Client generator provider".to_string()),
),
CompletionItem::new(
"output",
CompletionKind::FieldName,
Some("Output path for generated files".to_string()),
),
CompletionItem::new(
"interface",
CompletionKind::FieldName,
Some("Client interface style: \"sync\" (default) or \"async\"".to_string()),
),
CompletionItem::new(
"recursive_type_depth",
CompletionKind::FieldName,
Some(
"Depth of recursive include TypedDicts — Python client only (default: 5)"
.to_string(),
),
),
]
}
fn config_block_kind_at(tokens: &[Token], offset: usize) -> Option<ConfigBlockKind> {
let relevant: Vec<&Token> = tokens.iter().filter(|t| t.span.end <= offset).collect();
let mut depth: i32 = 0;
for tok in relevant.iter().rev() {
match tok.kind {
TokenKind::RBrace => depth += 1,
TokenKind::LBrace => {
if depth == 0 {
let idx = tokens
.iter()
.position(|t| std::ptr::eq(t, *tok))
.unwrap_or(0);
let before: Vec<&Token> = tokens[..idx]
.iter()
.filter(|t| !matches!(t.kind, TokenKind::Newline))
.collect();
if before.len() >= 2 {
let kw_tok = &before[before.len() - 2];
return match kw_tok.kind {
TokenKind::Datasource => Some(ConfigBlockKind::Datasource),
TokenKind::Generator => Some(ConfigBlockKind::Generator),
_ => None,
};
}
return None;
}
depth -= 1;
}
_ => {}
}
}
None
}
fn config_value_completions(key: &str, block_kind: Option<ConfigBlockKind>) -> Vec<CompletionItem> {
match key {
"provider" => match block_kind {
Some(ConfigBlockKind::Datasource) => vec![
CompletionItem::with_insert(
"postgresql",
"\"postgresql\"",
CompletionKind::Keyword,
Some("PostgreSQL database".to_string()),
),
CompletionItem::with_insert(
"mysql",
"\"mysql\"",
CompletionKind::Keyword,
Some("MySQL database".to_string()),
),
CompletionItem::with_insert(
"sqlite",
"\"sqlite\"",
CompletionKind::Keyword,
Some("SQLite database".to_string()),
),
],
Some(ConfigBlockKind::Generator) => vec![
CompletionItem::with_insert(
"nautilus-client-rs",
"\"nautilus-client-rs\"",
CompletionKind::Keyword,
Some("Rust client generator".to_string()),
),
CompletionItem::with_insert(
"nautilus-client-py",
"\"nautilus-client-py\"",
CompletionKind::Keyword,
Some("Python client generator".to_string()),
),
CompletionItem::with_insert(
"nautilus-client-js",
"\"nautilus-client-js\"",
CompletionKind::Keyword,
Some("JavaScript/TypeScript client generator".to_string()),
),
],
None => vec![
CompletionItem::with_insert(
"postgresql",
"\"postgresql\"",
CompletionKind::Keyword,
Some("PostgreSQL database".to_string()),
),
CompletionItem::with_insert(
"mysql",
"\"mysql\"",
CompletionKind::Keyword,
Some("MySQL database".to_string()),
),
CompletionItem::with_insert(
"sqlite",
"\"sqlite\"",
CompletionKind::Keyword,
Some("SQLite database".to_string()),
),
CompletionItem::with_insert(
"nautilus-client-rs",
"\"nautilus-client-rs\"",
CompletionKind::Keyword,
Some("Rust client generator".to_string()),
),
CompletionItem::with_insert(
"nautilus-client-py",
"\"nautilus-client-py\"",
CompletionKind::Keyword,
Some("Python client generator".to_string()),
),
CompletionItem::with_insert(
"nautilus-client-js",
"\"nautilus-client-js\"",
CompletionKind::Keyword,
Some("JavaScript/TypeScript client generator".to_string()),
),
],
},
"interface" => vec![
CompletionItem::with_insert(
"sync",
"\"sync\"",
CompletionKind::Keyword,
Some("Synchronous client interface (default)".to_string()),
),
CompletionItem::with_insert(
"async",
"\"async\"",
CompletionKind::Keyword,
Some("Asynchronous client interface".to_string()),
),
],
_ => Vec::new(),
}
}