1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
//! Completion management for the REPL (Issue #209: extracted from App).
//!
//! Handles LSP completions and builtin completions for tab completion.
use crate::lsp_client::LspClient;
use lsp_types::CompletionItem;
use std::fs;
use std::path::Path;
/// Manages tab completion state and LSP communication.
pub struct CompletionManager {
/// LSP client for completions (None if unavailable)
lsp_client: Option<LspClient>,
/// Current completion items
items: Vec<CompletionItem>,
/// Selected completion index
index: usize,
/// Whether completion popup is visible
visible: bool,
}
impl CompletionManager {
/// Create a new completion manager without LSP.
pub fn new() -> Self {
Self {
lsp_client: None,
items: Vec::new(),
index: 0,
visible: false,
}
}
/// Create a new completion manager with LSP client.
pub fn with_lsp(lsp_client: LspClient) -> Self {
Self {
lsp_client: Some(lsp_client),
items: Vec::new(),
index: 0,
visible: false,
}
}
/// Try to start LSP and return a completion manager.
pub fn try_with_lsp(session_path: &Path, content: &str) -> Self {
match LspClient::new(session_path) {
Ok(mut client) => {
// Open the document
if client.did_open(content).is_ok() {
Self::with_lsp(client)
} else {
Self::new()
}
}
Err(_) => Self::new(),
}
}
/// Check if completions are visible.
pub fn is_visible(&self) -> bool {
self.visible
}
/// Get current completion items.
pub fn items(&self) -> &[CompletionItem] {
&self.items
}
/// Get current selection index.
pub fn index(&self) -> usize {
self.index
}
/// Move up in completion list (wraps around).
pub fn up(&mut self) {
if !self.items.is_empty() {
if self.index > 0 {
self.index -= 1;
} else {
self.index = self.items.len() - 1;
}
}
}
/// Move down in completion list (wraps around).
pub fn down(&mut self) {
if !self.items.is_empty() {
self.index = (self.index + 1) % self.items.len();
}
}
/// Hide completion popup and clear items.
pub fn hide(&mut self) {
self.visible = false;
self.items.clear();
self.index = 0;
}
/// Request completions for the given input and cursor position.
///
/// Returns a status message if completions couldn't be provided.
pub fn request(&mut self, input: &str, cursor: usize, session_path: &Path) -> Option<String> {
// Find word start for replacement
let word_start = input[..cursor]
.rfind(|c: char| c.is_whitespace())
.map(|i| i + 1)
.unwrap_or(0);
// Get the prefix the user has typed
let prefix = &input[word_start..cursor];
// Don't show completions for empty prefix - too noisy
if prefix.is_empty() {
return Some("Tab: type a prefix first".to_string());
}
// Try LSP completions first
if self.try_lsp_completions(input, cursor, prefix, session_path) {
return None;
}
// Fall back to builtin completions
self.builtin_completions(prefix);
None
}
/// Try to get completions from LSP.
/// Returns true if LSP provided completions (even if empty).
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;
};
// Get file content
let Ok(file_content) = fs::read_to_string(session_path) else {
return false;
};
// Find where to insert (before stack.dump)
let Some(insert_pos) = file_content.find(" stack.dump") else {
return false;
};
// Create virtual document: file content + current line at end of main
let virtual_content = format!(
"{} {}\n{}",
&file_content[..insert_pos],
input,
&file_content[insert_pos..]
);
// Calculate line/column in virtual document
let lines_before: u32 = file_content[..insert_pos].matches('\n').count() as u32;
let line_num = lines_before; // 0-indexed
let col_num = cursor as u32 + 2; // +2 for the " " indent
// Sync virtual document and get completions
if lsp.did_change(&virtual_content).is_err() {
return false;
}
let items = match lsp.completions(line_num, col_num) {
Ok(items) => items,
Err(_) => {
// Restore original and fail
let _ = lsp.did_change(&file_content);
return false;
}
};
// Restore original document
let _ = lsp.did_change(&file_content);
// Filter by prefix (case-insensitive)
let prefix_lower = prefix.to_lowercase();
self.items = items
.into_iter()
.filter(|item| item.label.to_lowercase().starts_with(&prefix_lower))
.take(10)
.collect();
if !self.items.is_empty() {
self.index = 0;
self.visible = true;
}
true // LSP was available, even if no completions
}
/// Provide built-in completions when LSP is not available.
fn builtin_completions(&mut self, prefix: &str) {
// Get all builtin names from the compiler's canonical source
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();
if !self.items.is_empty() {
self.index = 0;
self.visible = true;
}
}
/// Accept the current completion and return the replacement text.
///
/// Returns (word_start, completion_text) if a completion is selected.
pub fn accept(&mut self, input: &str, cursor: usize) -> Option<(usize, String)> {
let item = self.items.get(self.index)?;
// Find start of current word
let word_start = input[..cursor]
.rfind(|c: char| c.is_whitespace())
.map(|i| i + 1)
.unwrap_or(0);
let completion = item.label.clone();
self.hide();
Some((word_start, completion))
}
}
impl Default for CompletionManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_completion_navigation() {
let mut mgr = CompletionManager::new();
// Add some test items
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;
// Test down navigation
mgr.down();
assert_eq!(mgr.index, 1);
mgr.down();
assert_eq!(mgr.index, 2);
mgr.down(); // Wrap around
assert_eq!(mgr.index, 0);
// Test up navigation
mgr.up(); // Wrap around
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());
// Should find "dup" at minimum
assert!(mgr.items.iter().any(|i| i.label == "dup"));
}
}