use crate::lsp_client::LspClient;
use lsp_types::CompletionItem;
use std::fs;
use std::path::Path;
pub struct CompletionManager {
lsp_client: Option<LspClient>,
items: Vec<CompletionItem>,
index: usize,
visible: bool,
}
impl CompletionManager {
pub fn new() -> Self {
Self {
lsp_client: None,
items: Vec::new(),
index: 0,
visible: false,
}
}
pub fn with_lsp(lsp_client: LspClient) -> Self {
Self {
lsp_client: Some(lsp_client),
items: Vec::new(),
index: 0,
visible: false,
}
}
pub fn try_with_lsp(session_path: &Path, content: &str) -> Self {
match LspClient::new(session_path) {
Ok(mut client) => {
if client.did_open(content).is_ok() {
Self::with_lsp(client)
} else {
Self::new()
}
}
Err(_) => Self::new(),
}
}
pub fn is_visible(&self) -> bool {
self.visible
}
pub fn items(&self) -> &[CompletionItem] {
&self.items
}
pub fn index(&self) -> usize {
self.index
}
pub fn up(&mut self) {
if !self.items.is_empty() {
if self.index > 0 {
self.index -= 1;
} else {
self.index = self.items.len() - 1;
}
}
}
pub fn down(&mut self) {
if !self.items.is_empty() {
self.index = (self.index + 1) % self.items.len();
}
}
pub fn hide(&mut self) {
self.visible = false;
self.items.clear();
self.index = 0;
}
fn show_items(&mut self) {
if !self.items.is_empty() {
self.index = 0;
self.visible = true;
}
}
pub fn request(&mut self, input: &str, cursor: usize, session_path: &Path) -> Option<String> {
let start = word_start(input, cursor);
let prefix = &input[start..cursor];
if prefix.is_empty() {
return Some("Tab: type a prefix first".to_string());
}
if self.try_lsp_completions(input, cursor, prefix, session_path) {
return None;
}
self.builtin_completions(prefix);
None
}
fn try_lsp_completions(
&mut self,
input: &str,
cursor: usize,
prefix: &str,
session_path: &Path,
) -> bool {
let Some(ref mut lsp) = self.lsp_client else {
return false;
};
let Ok(file_content) = fs::read_to_string(session_path) else {
return false;
};
let Some(insert_pos) = file_content.find(" stack.dump") else {
return false;
};
let virtual_content = format!(
"{} {}\n{}",
&file_content[..insert_pos],
input,
&file_content[insert_pos..]
);
let lines_before: u32 = file_content[..insert_pos].matches('\n').count() as u32;
let line_num = lines_before; let col_num = cursor as u32 + 2;
if lsp.did_change(&virtual_content).is_err() {
return false;
}
let items = match lsp.completions(line_num, col_num) {
Ok(items) => items,
Err(_) => {
let _ = lsp.did_change(&file_content);
return false;
}
};
let _ = lsp.did_change(&file_content);
let prefix_lower = prefix.to_lowercase();
self.items = items
.into_iter()
.filter(|item| item.label.to_lowercase().starts_with(&prefix_lower))
.take(10)
.collect();
self.show_items();
true }
fn builtin_completions(&mut self, prefix: &str) {
let signatures = seqc::builtins::builtin_signatures();
let builtins: Vec<&str> = signatures.keys().map(|s| s.as_str()).collect();
self.items = builtins
.iter()
.filter(|b| b.starts_with(prefix) && **b != prefix)
.take(10)
.map(|s| CompletionItem {
label: s.to_string(),
..Default::default()
})
.collect();
self.show_items();
}
pub fn accept(&mut self, input: &str, cursor: usize) -> Option<(usize, String)> {
let item = self.items.get(self.index)?;
let start = word_start(input, cursor);
let completion = item.label.clone();
self.hide();
Some((start, completion))
}
}
impl Default for CompletionManager {
fn default() -> Self {
Self::new()
}
}
fn word_start(input: &str, cursor: usize) -> usize {
input[..cursor]
.rfind(|c: char| c.is_whitespace())
.map(|i| i + 1)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_completion_navigation() {
let mut mgr = CompletionManager::new();
mgr.items = vec![
CompletionItem {
label: "dup".to_string(),
..Default::default()
},
CompletionItem {
label: "drop".to_string(),
..Default::default()
},
CompletionItem {
label: "swap".to_string(),
..Default::default()
},
];
mgr.visible = true;
mgr.index = 0;
mgr.down();
assert_eq!(mgr.index, 1);
mgr.down();
assert_eq!(mgr.index, 2);
mgr.down(); assert_eq!(mgr.index, 0);
mgr.up(); assert_eq!(mgr.index, 2);
mgr.up();
assert_eq!(mgr.index, 1);
}
#[test]
fn test_completion_hide() {
let mut mgr = CompletionManager::new();
mgr.items = vec![CompletionItem {
label: "test".to_string(),
..Default::default()
}];
mgr.visible = true;
mgr.index = 0;
mgr.hide();
assert!(!mgr.is_visible());
assert!(mgr.items.is_empty());
assert_eq!(mgr.index, 0);
}
#[test]
fn test_builtin_completions() {
let mut mgr = CompletionManager::new();
mgr.builtin_completions("du");
assert!(mgr.is_visible());
assert!(!mgr.items.is_empty());
assert!(mgr.items.iter().any(|i| i.label == "dup"));
}
}