use crate::includes::{IncludedWord, LocalWord};
use seqc::builtins::builtin_signatures;
use tower_lsp::lsp_types::{
CompletionItem, CompletionItemKind, Documentation, MarkupContent, MarkupKind,
};
const STDLIB_MODULES: &[(&str, &str)] = &[
("json", "JSON parsing and serialization"),
("yaml", "YAML parsing and serialization"),
("http", "HTTP request/response utilities"),
("math", "Mathematical functions"),
("stack-utils", "Stack manipulation utilities"),
];
pub struct CompletionContext<'a> {
pub line_prefix: &'a str,
pub included_words: &'a [IncludedWord],
pub local_words: &'a [LocalWord],
}
#[derive(Debug, PartialEq)]
enum ContextType {
InString,
InComment,
IncludeModule,
IncludeStdModule,
InStackEffect,
WordDefName,
Code,
}
pub fn get_completions(context: Option<CompletionContext<'_>>) -> Vec<CompletionItem> {
let Some(ctx) = context else {
return get_builtin_completions();
};
let context_type = detect_context(ctx.line_prefix);
match context_type {
ContextType::InString | ContextType::InComment | ContextType::WordDefName => {
Vec::new()
}
ContextType::IncludeModule => get_include_module_completions(ctx.line_prefix),
ContextType::IncludeStdModule => get_include_std_completions(ctx.line_prefix),
ContextType::InStackEffect => get_type_completions(),
ContextType::Code => get_code_completions(ctx.included_words, ctx.local_words),
}
}
fn detect_context(line_prefix: &str) -> ContextType {
let trimmed = line_prefix.trim_start();
if let Some(_partial) = trimmed.strip_prefix("include std:") {
return ContextType::IncludeStdModule;
}
if trimmed.starts_with("include ") {
return ContextType::IncludeModule;
}
if is_in_string(line_prefix) {
return ContextType::InString;
}
if line_prefix.contains('#') {
if let Some(hash_pos) = line_prefix.rfind('#') {
let before_hash = &line_prefix[..hash_pos];
if !is_in_string(before_hash) {
return ContextType::InComment;
}
}
}
if let Some(after_colon) = trimmed.strip_prefix(':') {
let after_colon = after_colon.trim_start();
if !after_colon.contains(' ') && !after_colon.contains('(') {
return ContextType::WordDefName;
}
}
let unmatched_parens = count_unmatched_parens(line_prefix);
if unmatched_parens > 0 {
return ContextType::InStackEffect;
}
ContextType::Code
}
fn count_unmatched_parens(text: &str) -> i32 {
let mut in_string = false;
let mut count = 0;
for c in text.chars() {
match c {
'"' => in_string = !in_string,
'(' if !in_string => count += 1,
')' if !in_string => count -= 1,
_ => {}
}
}
count
}
fn is_in_string(text: &str) -> bool {
let mut in_string = false;
for c in text.chars() {
if c == '"' {
in_string = !in_string;
}
}
in_string
}
fn get_include_module_completions(line_prefix: &str) -> Vec<CompletionItem> {
let trimmed = line_prefix.trim_start();
let partial = trimmed.strip_prefix("include ").unwrap_or("");
let mut items = Vec::new();
if "std:".starts_with(partial) || partial.is_empty() {
items.push(CompletionItem {
label: "std:".to_string(),
kind: Some(CompletionItemKind::MODULE),
detail: Some("Standard library".to_string()),
documentation: Some(Documentation::String(
"Include a module from the standard library".to_string(),
)),
..Default::default()
});
}
for (name, desc) in STDLIB_MODULES {
let full_name = format!("std:{}", name);
if full_name.starts_with(partial) {
items.push(CompletionItem {
label: full_name.clone(),
kind: Some(CompletionItemKind::MODULE),
detail: Some(desc.to_string()),
documentation: Some(Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: format!("```seq\ninclude {}\n```\n\n{}", full_name, desc),
})),
..Default::default()
});
}
}
items
}
fn get_include_std_completions(line_prefix: &str) -> Vec<CompletionItem> {
let trimmed = line_prefix.trim_start();
let partial = trimmed.strip_prefix("include std:").unwrap_or("");
STDLIB_MODULES
.iter()
.filter(|(name, _)| name.starts_with(partial))
.map(|(name, desc)| CompletionItem {
label: name.to_string(),
kind: Some(CompletionItemKind::MODULE),
detail: Some(desc.to_string()),
documentation: Some(Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: format!("```seq\ninclude std:{}\n```\n\n{}", name, desc),
})),
..Default::default()
})
.collect()
}
fn get_type_completions() -> Vec<CompletionItem> {
let types = [
("Int", "64-bit signed integer"),
("Float", "64-bit floating point"),
("Bool", "Boolean (true/false)"),
("String", "UTF-8 string"),
("--", "Stack effect separator"),
];
types
.iter()
.map(|(name, desc)| CompletionItem {
label: name.to_string(),
kind: Some(CompletionItemKind::TYPE_PARAMETER),
detail: Some(desc.to_string()),
..Default::default()
})
.collect()
}
fn get_code_completions(
included_words: &[IncludedWord],
local_words: &[LocalWord],
) -> Vec<CompletionItem> {
let mut items = Vec::new();
for word in local_words {
let detail = word
.effect
.as_ref()
.map(format_effect)
.unwrap_or_else(|| "( ? )".to_string());
items.push(CompletionItem {
label: word.name.clone(),
kind: Some(CompletionItemKind::FUNCTION),
detail: Some(detail.clone()),
documentation: Some(Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: format!(
"```seq\n: {} {}\n```\n\n*Defined in this file*",
word.name, detail
),
})),
sort_text: Some(format!("0_{}", word.name)), ..Default::default()
});
}
for word in included_words {
let detail = word
.effect
.as_ref()
.map(format_effect)
.unwrap_or_else(|| "( ? )".to_string());
items.push(CompletionItem {
label: word.name.clone(),
kind: Some(CompletionItemKind::FUNCTION),
detail: Some(detail.clone()),
documentation: Some(Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: format!(
"```seq\n: {} {}\n```\n\n*From {}*",
word.name, detail, word.source
),
})),
sort_text: Some(format!("1_{}", word.name)), ..Default::default()
});
}
items.extend(get_builtin_completions());
items
}
fn get_builtin_completions() -> Vec<CompletionItem> {
let mut items = Vec::new();
for (name, effect) in builtin_signatures() {
let signature = format_effect(&effect);
items.push(CompletionItem {
label: name.clone(),
kind: Some(CompletionItemKind::FUNCTION),
detail: Some(signature.clone()),
documentation: Some(Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: format!("```seq\n{} {}\n```\n\n*Built-in*", name, signature),
})),
sort_text: Some(format!("2_{}", name)), ..Default::default()
});
}
for keyword in &["if", "else", "then", "include", "true", "false"] {
items.push(CompletionItem {
label: keyword.to_string(),
kind: Some(CompletionItemKind::KEYWORD),
sort_text: Some(format!("3_{}", keyword)), ..Default::default()
});
}
let control_flow = [
(
"while",
"( condition-quot body-quot -- )",
"Loop while condition is true",
),
(
"until",
"( body-quot condition-quot -- )",
"Loop until condition is true",
),
("times", "( quot n -- )", "Execute quotation n times"),
("forever", "( quot -- )", "Execute quotation forever"),
("call", "( quot -- ... )", "Execute a quotation"),
(
"spawn",
"( quot -- strand-id )",
"Spawn quotation as new strand",
),
];
for (name, sig, desc) in control_flow {
if items.iter().any(|i| i.label == name) {
continue;
}
items.push(CompletionItem {
label: name.to_string(),
kind: Some(CompletionItemKind::FUNCTION),
detail: Some(sig.to_string()),
documentation: Some(Documentation::String(desc.to_string())),
sort_text: Some(format!("2_{}", name)),
..Default::default()
});
}
items
}
pub fn format_effect(effect: &seqc::Effect) -> String {
format!(
"( {} -- {} )",
format_stack(&effect.inputs),
format_stack(&effect.outputs)
)
}
fn format_stack(stack: &seqc::StackType) -> String {
use seqc::StackType;
match stack {
StackType::Empty => String::new(),
StackType::RowVar(name) => format!("..{}", name),
StackType::Cons { rest, top } => {
let rest_str = format_stack(rest);
let top_str = format_type(top);
if rest_str.is_empty() {
top_str
} else {
format!("{} {}", rest_str, top_str)
}
}
}
}
fn format_type(ty: &seqc::Type) -> String {
use seqc::Type;
match ty {
Type::Int => "Int".to_string(),
Type::Float => "Float".to_string(),
Type::Bool => "Bool".to_string(),
Type::String => "String".to_string(),
Type::Var(name) => name.clone(),
Type::Quotation(effect) => format!("[ {} ]", format_effect(effect)),
Type::Closure { effect, .. } => format!("{{ {} }}", format_effect(effect)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_context_code() {
assert_eq!(detect_context(" dup"), ContextType::Code);
assert_eq!(detect_context("1 2 +"), ContextType::Code);
}
#[test]
fn test_detect_context_include() {
assert_eq!(detect_context("include "), ContextType::IncludeModule);
assert_eq!(
detect_context("include std:"),
ContextType::IncludeStdModule
);
assert_eq!(
detect_context("include std:js"),
ContextType::IncludeStdModule
);
}
#[test]
fn test_detect_context_string() {
assert_eq!(detect_context("\"hello"), ContextType::InString);
assert_eq!(detect_context("\"hello\" "), ContextType::Code);
assert_eq!(detect_context("\"hello\" \"world"), ContextType::InString);
}
#[test]
fn test_detect_context_comment() {
assert_eq!(detect_context("# comment"), ContextType::InComment);
assert_eq!(detect_context("dup # comment"), ContextType::InComment);
assert_eq!(detect_context("\"#hashtag\""), ContextType::Code);
}
#[test]
fn test_detect_context_word_def() {
assert_eq!(detect_context(": my-word"), ContextType::WordDefName);
assert_eq!(detect_context(": my-word ("), ContextType::InStackEffect);
assert_eq!(
detect_context(": my-word ( Int"),
ContextType::InStackEffect
);
}
#[test]
fn test_detect_context_stack_effect() {
assert_eq!(detect_context("( Int"), ContextType::InStackEffect);
assert_eq!(detect_context("( Int -- "), ContextType::InStackEffect);
assert_eq!(detect_context("( Int -- Int )"), ContextType::Code);
assert_eq!(detect_context("\"(\" dup"), ContextType::Code);
assert_eq!(detect_context("\")\" dup"), ContextType::Code);
}
#[test]
fn test_is_in_string() {
assert!(!is_in_string("hello"));
assert!(is_in_string("\"hello"));
assert!(!is_in_string("\"hello\""));
assert!(is_in_string("\"hello\" \"world"));
assert!(!is_in_string("\"hello\" \"world\""));
}
}