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
//! The Ctrl-P command palette and the `f`-key quick-jump, plus the
//! Tab-completion state machine behind the `:` prompt.
use super::*;
impl App {
/// `:rds` — fetch the env's RDS dbinstance option settings and
/// Advance / rewind the command-mode completion cycle by
/// `delta` (+1 = Tab, -1 = Shift-Tab). Captures the operator's
/// typed prefix on the first Tab; subsequent Tabs cycle
/// through matches without losing the original prefix (so
/// they can pop out by typing).
///
/// What gets completed depends on where the cursor is:
/// - **No whitespace yet** → the command name (the whole input
/// is the name fragment).
/// - **Whitespace, and the first token is an env-arg command**
/// (`:diff` / `:config-diff` / `:rds-detach`, see
/// [`command_takes_env_arg`]) → the *trailing* token as an
/// environment name, drawn from the loaded fleet. `:diff
/// ENV-A ENV-B` completes whichever env name is last.
/// - **Whitespace, any other command** → the command-name
/// fragment is re-completed and args after the first space
/// pass through untouched. Means `:set-option aws` still
/// completes `set-option` if the operator Tabs at the start.
pub(crate) fn command_completion_step(&mut self, delta: i32) {
// First Tab of a cycle: snapshot what the operator had typed
// so a subsequent reverse-Tab (or text input) can restore.
// `first_step` also anchors the landing spot below so the very
// first Tab lands on the *first* candidate (forward) / last
// (backward), rather than immediately stepping past it.
let first_step = self.completion.origin.is_none();
if first_step {
self.completion.origin = Some(self.command_input.text().to_string());
self.completion.index = 0;
}
let origin = self.completion.origin.clone().unwrap_or_default();
let ws = origin.find(char::is_whitespace);
// Env-arg mode: a whitespace-bearing input whose first token
// is one of the env-name-taking commands. Then we complete
// the trailing token against the loaded env names instead of
// the command list.
let env_mode = ws
.map(|i| command_takes_env_arg(&origin[..i]))
.unwrap_or(false);
// `head` is preserved verbatim before the candidate; `tail`
// is appended after it. Command-name completion keeps the
// arg tail (`rest`); env completion folds the whole prefix
// (command + earlier args + the separating space) into
// `head` and has no tail.
let (head, candidates, tail): (String, Vec<String>, String) = match ws {
None => (String::new(), completion_candidates(&origin), String::new()),
Some(first_ws) if env_mode => {
// `first_ws` is a whitespace index, so `rfind` cannot miss;
// falling back to it keeps this total rather than resting on
// that argument.
let last_ws = origin.rfind(char::is_whitespace).unwrap_or(first_ws);
// `rfind` gives the *first byte* of the last whitespace
// char; step over the whole char so the split lands on a
// char boundary (a multi-byte space like U+00A0 NBSP
// otherwise slices mid-char and panics).
let frag_start =
last_ws + origin[last_ws..].chars().next().map_or(1, char::len_utf8);
let head = origin[..frag_start].to_string();
let frag = origin[frag_start..].to_string();
(head, self.env_name_candidates(&frag), String::new())
}
Some(i) => (
String::new(),
completion_candidates(&origin[..i]),
origin[i..].to_string(),
),
};
if candidates.is_empty() {
// Restore the operator's typed prefix and surface a
// hint so the silent-no-op doesn't feel broken.
self.command_input = origin.clone().into();
self.status_message = Some(if env_mode {
"no environment matches (Tab cycles env names)".to_string()
} else {
let prefix = ws.map(|i| &origin[..i]).unwrap_or(&origin[..]);
format!("no command matches '{prefix}' (Tab cycles command names)")
});
return;
}
let n = candidates.len() as i32;
let next = if first_step {
// Land on the first (forward) / last (backward) match.
if delta >= 0 {
0
} else {
(n - 1) as usize
}
} else {
let cur = self.completion.index as i32;
(cur + delta).rem_euclid(n) as usize
};
self.completion.index = next;
self.command_input = format!("{head}{}{tail}", candidates[next]).into();
self.status_message = Some(format!(
"completion {}/{} — Tab cycles, Esc cancels",
next + 1,
n
));
}
/// Environment names from the loaded fleet that start with
/// `prefix`, sorted + deduped — the candidate list for
/// command-bar argument completion (see
/// [`Self::command_completion_step`]).
pub(crate) fn env_name_candidates(&self, prefix: &str) -> Vec<String> {
let mut names: Vec<String> = self
.environments
.iter()
.map(|e| e.name.clone())
.filter(|n| n.starts_with(prefix))
.collect();
names.sort();
names.dedup();
names
}
pub(crate) fn open_palette(&mut self) {
self.palette_input.clear();
self.palette_items = build_palette_items(self);
self.palette_refilter();
self.mode = Mode::Palette;
}
pub(crate) fn palette_refilter(&mut self) {
let needle = self.palette_input.text().to_lowercase();
let mut scored: Vec<(usize, isize)> = self
.palette_items
.iter()
.enumerate()
.filter_map(|(i, it)| {
let s = palette_score(&needle, &it.label, &it.detail)?;
Some((i, s))
})
.collect();
scored.sort_by(|a, b| a.1.cmp(&b.1).then(a.0.cmp(&b.0)));
self.palette_filtered = scored.into_iter().map(|(i, _)| i).collect();
self.palette_state
.select(if self.palette_filtered.is_empty() {
None
} else {
Some(0)
});
}
pub(crate) fn palette_move(&mut self, delta: i32) {
let n = self.palette_filtered.len();
if n == 0 {
self.palette_state.select(None);
return;
}
let cur = self.palette_state.selected().unwrap_or(0) as i32;
let next = (cur + delta).rem_euclid(n as i32) as usize;
self.palette_state.select(Some(next));
}
pub(crate) fn palette_execute(&mut self) {
let Some(pos) = self.palette_state.selected() else {
return;
};
let Some(&idx) = self.palette_filtered.get(pos) else {
return;
};
let Some(item) = self.palette_items.get(idx).cloned() else {
return;
};
self.mode = Mode::Normal;
self.palette_input.clear();
match item.action {
PaletteAction::RunCommand(cmd) => self.execute_command(&cmd),
PaletteAction::PrefillCommand(prefix) => {
self.command_input = prefix.into();
self.mode = Mode::Command;
}
PaletteAction::JumpEnv(name) => {
if let Some(pos) = self.view.display().iter().position(|r| match r {
DisplayRow::Env(i) => self.environments[*i].name == name,
DisplayRow::Separator => false,
}) {
self.table_state.select(Some(pos));
self.status_message = Some(format!("jumped to {name}"));
}
}
PaletteAction::LoadView(name) => {
self.execute_command(&format!("view {name}"));
}
}
}
pub(crate) fn quickjump_apply(&mut self) {
if self.quickjump_input.is_empty() {
return;
}
let needle = self.quickjump_input.text().to_lowercase();
for (pos, row) in self.view.display().iter().enumerate() {
if let DisplayRow::Env(i) = row {
let e = &self.environments[*i];
let alias = self
.aliases
.get(&e.name)
.map(|a| a.to_lowercase())
.unwrap_or_default();
if e.name.to_lowercase().starts_with(&needle) || alias.starts_with(&needle) {
self.table_state.select(Some(pos));
return;
}
}
}
}
pub(crate) fn quick_jump(&mut self, n: usize) {
// 1..=9 maps to position n-1 in the visible env rows.
let Some(target_env) = self
.view
.display()
.iter()
.filter(|r| matches!(r, DisplayRow::Env(_)))
.nth(n.saturating_sub(1))
else {
return;
};
if let Some(pos) = self
.view
.display()
.iter()
.position(|r| std::ptr::eq(r, target_env))
{
self.table_state.select(Some(pos));
}
}
}