Line data Source code
1 : //! The shell session.
2 :
3 : use super::prompt_buffer::{find_from, PromptBuffer};
4 : use super::unicode::{display_width, is_valid, next_grapheme};
5 : use super::vt100::{self, append_sequence, Command};
6 : use super::*;
7 : use dashql_core::catalog::ids::CatalogObjectType;
8 : use dashql_core::catalog::Catalog;
9 : use dashql_core::error::Error;
10 : use dashql_core::pack::{pack_completion, pack_tokens};
11 : use dashql_core::script::Script;
12 : use dashql_core::utils::{ci_cmp, ci_find, ci_starts_with, quote_identifier};
13 : use dashql_buffers::completion::{CandidateTag, CompletionCandidateT, CompletionT};
14 : use dashql_buffers::parser::ScannerTokenType;
15 : use dashql_parser::symbol_kind::S_SEMICOLON;
16 : use std::cmp::Ordering;
17 : use std::collections::VecDeque;
18 :
19 : const DEFAULT_DATABASE_NAME: &str = "hyper";
20 : const DEFAULT_TERMINAL_PROMPT: &[u8] = b"dashql> ";
21 : const HISTORY_LIMIT: usize = 1000;
22 : const TERMINAL_PROMPT_CAPACITY: usize = 256;
23 : const TERMINAL_QUERY_SPINNER_FRAMES: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
24 :
25 : /// The message of a core error, the default message of its status code if it has one
26 0 : fn error_message(error: Error) -> String {
27 0 : error.what().to_string()
28 0 : }
29 :
30 1552 : fn token_style(token_type: ScannerTokenType) -> &'static str {
31 1552 : match token_type {
32 545 : ScannerTokenType::KEYWORD => vt100::BOLD_FOREGROUND_PINK,
33 : ScannerTokenType::LITERAL_INTEGER
34 : | ScannerTokenType::LITERAL_FLOAT
35 : | ScannerTokenType::LITERAL_BINARY
36 : | ScannerTokenType::LITERAL_HEX
37 25 : | ScannerTokenType::LITERAL_BOOLEAN => vt100::FOREGROUND_PURPLE,
38 7 : ScannerTokenType::LITERAL_STRING => vt100::FOREGROUND_CORAL,
39 10 : ScannerTokenType::OPERATOR => vt100::FOREGROUND_PINK,
40 568 : ScannerTokenType::IDENTIFIER => vt100::FOREGROUND_SILVER,
41 0 : ScannerTokenType::COMMENT => vt100::FOREGROUND_GRAY,
42 397 : _ => "",
43 : }
44 1552 : }
45 :
46 2009 : fn append_cursor_move(output: &mut Vec<u8>, count: usize, command: Command) {
47 2009 : if count != 0 {
48 718 : append_sequence(output, count, command);
49 1291 : }
50 2009 : }
51 :
52 5553 : fn push(output: &mut Vec<u8>, text: &str) {
53 5553 : output.extend_from_slice(text.as_bytes());
54 5553 : }
55 :
56 : /// The byte offset after the grapheme cluster at `offset`, one byte further if the text is not valid UTF-8
57 51891 : fn next_cluster(utf8: Option<&str>, offset: usize) -> usize {
58 51891 : match utf8 {
59 51891 : Some(text) => next_grapheme(text, offset),
60 0 : None => offset + 1,
61 : }
62 51891 : }
63 :
64 : /// Cut a text to a display width and append "..."
65 10 : fn truncate_display_text(text: &[u8], width: usize) -> Vec<u8> {
66 10 : if display_width(text) <= width {
67 7 : return text.to_vec();
68 3 : }
69 : const MARKER: &[u8] = b"...";
70 3 : let marker_width = width.min(MARKER.len());
71 3 : let content_width = width - marker_width;
72 3 : let mut rendered = 0;
73 3 : let mut end = 0;
74 3 : let utf8 = std::str::from_utf8(text).ok();
75 3 : let mut offset = 0;
76 46 : while offset < text.len() {
77 46 : let next = next_cluster(utf8, offset);
78 46 : let grapheme_width = display_width(&text[offset..next]);
79 46 : if rendered + grapheme_width > content_width {
80 3 : break;
81 43 : }
82 43 : rendered += grapheme_width;
83 43 : end = next;
84 43 : offset = next;
85 : }
86 3 : let mut output = text[..end].to_vec();
87 3 : output.extend_from_slice(&MARKER[..marker_width]);
88 3 : output
89 10 : }
90 :
91 : /// Keep the catalog objects that the variants of a candidate stand for, the first one if it has no variants.
92 : /// An object index of the list is then also a variant index.
93 4707 : fn keep_variant_objects<T>(objects: &mut Vec<T>, variants: &[usize]) {
94 4707 : if variants.is_empty() {
95 4693 : objects.truncate(1);
96 4693 : return;
97 14 : }
98 14 : let mut index = 0;
99 26 : objects.retain(|_| {
100 26 : let keep = variants.contains(&index);
101 26 : index += 1;
102 26 : keep
103 26 : });
104 4707 : }
105 :
106 : /// The extent of a laid out prompt in the terminal
107 : #[derive(Clone, Copy, Debug)]
108 : struct PromptLayout {
109 : /// The number of terminal rows
110 : rows: usize,
111 : /// The column after the last grapheme
112 : column: usize,
113 : }
114 :
115 : impl Default for PromptLayout {
116 1471 : fn default() -> Self {
117 1471 : Self { rows: 1, column: 0 }
118 1471 : }
119 : }
120 :
121 429 : fn append_prompt_prefix(
122 429 : output: &mut Vec<u8>,
123 429 : prefix: &[u8],
124 429 : prefix_style: &[u8],
125 429 : columns: usize,
126 429 : layout: &mut PromptLayout,
127 429 : ) {
128 429 : if !prefix.is_empty() {
129 429 : output.extend_from_slice(prefix_style);
130 429 : }
131 429 : let utf8 = std::str::from_utf8(prefix).ok();
132 429 : let mut offset = 0;
133 3873 : while offset < prefix.len() {
134 3444 : let next = next_cluster(utf8, offset);
135 3444 : let grapheme = &prefix[offset..next];
136 3444 : let width = display_width(grapheme);
137 3444 : if layout.column != 0 && layout.column + width > columns {
138 3 : push(output, vt100::CARRIAGE_RETURN);
139 3 : append_cursor_move(output, 1, Command::CursorDown);
140 3 : layout.rows += 1;
141 3 : layout.column = 0;
142 3441 : }
143 3444 : output.extend_from_slice(grapheme);
144 3444 : layout.column += width;
145 3444 : offset = next;
146 : }
147 429 : if !prefix.is_empty() {
148 429 : push(output, vt100::RESET_ATTRIBUTES);
149 429 : }
150 429 : }
151 :
152 1288 : fn advance_prompt_prefix(prefix: &[u8], columns: usize, layout: &mut PromptLayout) {
153 1288 : let utf8 = std::str::from_utf8(prefix).ok();
154 1288 : let mut offset = 0;
155 11486 : while offset < prefix.len() {
156 10198 : let next = next_cluster(utf8, offset);
157 10198 : let width = display_width(&prefix[offset..next]);
158 10198 : if layout.column != 0 && layout.column + width > columns {
159 6 : layout.rows += 1;
160 6 : layout.column = 0;
161 10192 : }
162 10198 : layout.column += width;
163 10198 : offset = next;
164 : }
165 1288 : }
166 :
167 246 : fn break_prompt_line(
168 246 : output: Option<&mut Vec<u8>>,
169 246 : continuation: &[u8],
170 246 : prefix_style: &[u8],
171 246 : active_style: &[u8],
172 246 : columns: usize,
173 246 : layout: &mut PromptLayout,
174 246 : ) {
175 246 : match output {
176 72 : Some(output) => {
177 72 : if !active_style.is_empty() {
178 35 : push(output, vt100::RESET_ATTRIBUTES);
179 39 : }
180 72 : push(output, vt100::CARRIAGE_RETURN);
181 72 : append_cursor_move(output, 1, Command::CursorDown);
182 72 : layout.rows += 1;
183 72 : layout.column = 0;
184 72 : append_prompt_prefix(output, continuation, prefix_style, columns, layout);
185 72 : if !active_style.is_empty() {
186 35 : output.extend_from_slice(active_style);
187 39 : }
188 : }
189 174 : None => {
190 174 : layout.rows += 1;
191 174 : layout.column = 0;
192 174 : advance_prompt_prefix(continuation, columns, layout);
193 174 : }
194 : }
195 246 : }
196 :
197 1114 : fn layout_prompt(text: &[u8], initial: &[u8], continuation: &[u8], columns: usize) -> PromptLayout {
198 1114 : let columns = columns.max(1);
199 1114 : let mut layout = PromptLayout::default();
200 1114 : advance_prompt_prefix(initial, columns, &mut layout);
201 1114 : let utf8 = std::str::from_utf8(text).ok();
202 1114 : let mut offset = 0;
203 30397 : while offset < text.len() {
204 29283 : if text[offset] == b'\n' {
205 75 : break_prompt_line(None, continuation, &[], &[], columns, &mut layout);
206 75 : offset += 1;
207 75 : continue;
208 29208 : }
209 29208 : let next = next_cluster(utf8, offset);
210 29208 : let width = display_width(&text[offset..next]);
211 29208 : if layout.column != 0 && layout.column + width > columns {
212 99 : break_prompt_line(None, continuation, &[], &[], columns, &mut layout);
213 29109 : }
214 29208 : layout.column += width;
215 29208 : offset = next;
216 : }
217 1114 : layout
218 1114 : }
219 :
220 357 : fn render_highlighted_prompt(
221 357 : output: &mut Vec<u8>,
222 357 : highlighted: &[u8],
223 357 : initial: &[u8],
224 357 : continuation: &[u8],
225 357 : prefix_style: &[u8],
226 357 : columns: usize,
227 357 : ) -> PromptLayout {
228 357 : let columns = columns.max(1);
229 357 : let mut layout = PromptLayout::default();
230 357 : append_prompt_prefix(output, initial, prefix_style, columns, &mut layout);
231 357 : let mut active_style: Vec<u8> = Vec::new();
232 357 : let utf8 = std::str::from_utf8(highlighted).ok();
233 357 : let mut offset = 0;
234 11701 : while offset < highlighted.len() {
235 11344 : if highlighted[offset] == 0x1b && offset + 1 < highlighted.len() && highlighted[offset + 1] == b'[' {
236 2316 : if let Some(end) = find_from(highlighted, b'm', offset + 2) {
237 2316 : let sequence = &highlighted[offset..end + 1];
238 2316 : output.extend_from_slice(sequence);
239 2316 : active_style =
240 2316 : if sequence == vt100::RESET_ATTRIBUTES.as_bytes() { Vec::new() } else { sequence.to_vec() };
241 2316 : offset = end + 1;
242 2316 : continue;
243 0 : }
244 9028 : }
245 9028 : if highlighted[offset] == b'\n' {
246 33 : break_prompt_line(Some(output), continuation, prefix_style, &active_style, columns, &mut layout);
247 33 : offset += 1;
248 33 : continue;
249 8995 : }
250 8995 : let next = next_cluster(utf8, offset);
251 8995 : let grapheme = &highlighted[offset..next];
252 8995 : let width = display_width(grapheme);
253 8995 : if layout.column != 0 && layout.column + width > columns {
254 39 : break_prompt_line(Some(output), continuation, prefix_style, &active_style, columns, &mut layout);
255 8956 : }
256 8995 : output.extend_from_slice(grapheme);
257 8995 : layout.column += width;
258 8995 : offset = next;
259 : }
260 357 : layout
261 357 : }
262 :
263 380 : fn escape_terminal_text(text: &[u8]) -> Vec<u8> {
264 : const HEX: &[u8; 16] = b"0123456789abcdef";
265 380 : let mut output = Vec::with_capacity(text.len());
266 1132 : for &value in text {
267 1132 : if value == b'\t' {
268 1 : output.extend_from_slice(b" ");
269 1131 : } else if value < 0x20 || value == 0x7f {
270 1 : output.extend_from_slice(b"\\x");
271 1 : output.push(HEX[(value >> 4) as usize]);
272 1 : output.push(HEX[(value & 0x0f) as usize]);
273 1130 : } else {
274 1130 : output.push(value);
275 1130 : }
276 : }
277 380 : output
278 380 : }
279 :
280 10 : fn normalize_terminal_progress_message(message: &[u8]) -> Vec<u8> {
281 10 : let mut output = Vec::with_capacity(message.len());
282 10 : let mut pending_space = false;
283 87 : for &value in message {
284 87 : if value == 0x1b {
285 1 : break;
286 86 : }
287 86 : if matches!(value, b' ' | b'\t' | b'\r' | b'\n') {
288 12 : pending_space = !output.is_empty();
289 12 : continue;
290 74 : }
291 74 : if value < 0x20 || value == 0x7f {
292 1 : continue;
293 73 : }
294 73 : if pending_space {
295 8 : output.push(b' ');
296 8 : pending_space = false;
297 65 : }
298 73 : output.push(value);
299 : }
300 10 : output
301 10 : }
302 :
303 3 : fn normalize_error(data: &[u8]) -> Vec<u8> {
304 3 : if data.is_empty() {
305 1 : b"query failed".to_vec()
306 : } else {
307 2 : data.to_vec()
308 : }
309 3 : }
310 :
311 189 : fn ci_eq(a: &[u8], b: &[u8]) -> bool {
312 189 : ci_cmp(a, b) == Ordering::Equal
313 189 : }
314 :
315 3 : fn is_non_default_three_part_name(name: &[u8]) -> bool {
316 3 : let mut separator_count = 0;
317 3 : let mut first_separator = None;
318 3 : let mut quoted = false;
319 3 : let mut i = 0;
320 52 : while i < name.len() {
321 49 : if name[i] == b'"' {
322 4 : if quoted && i + 1 < name.len() && name[i + 1] == b'"' {
323 0 : i += 1;
324 4 : } else {
325 4 : quoted = !quoted;
326 4 : }
327 45 : } else if !quoted && name[i] == b'.' {
328 4 : if first_separator.is_none() {
329 2 : first_separator = Some(i);
330 2 : }
331 4 : separator_count += 1;
332 41 : }
333 49 : i += 1;
334 : }
335 3 : let Some(first_separator) = first_separator else {
336 1 : return false;
337 : };
338 2 : if separator_count != 2 {
339 0 : return false;
340 2 : }
341 2 : let mut database = &name[..first_separator];
342 2 : if database.len() >= 2 && database[0] == b'"' && database[database.len() - 1] == b'"' {
343 1 : database = &database[1..database.len() - 1];
344 1 : }
345 2 : !ci_eq(database, DEFAULT_DATABASE_NAME.as_bytes())
346 3 : }
347 :
348 : /// The first byte that is not in a set
349 641 : fn find_first_not_of(text: &[u8], set: &[u8]) -> Option<usize> {
350 641 : text.iter().position(|c| !set.contains(c))
351 641 : }
352 :
353 : /// The last byte that is not in a set
354 19 : fn find_last_not_of(text: &[u8], set: &[u8]) -> Option<usize> {
355 22 : text.iter().rposition(|c| !set.contains(c))
356 19 : }
357 :
358 : /// The first byte at or after an offset that is in a set
359 302 : fn find_first_of(text: &[u8], set: &[u8], from: usize) -> Option<usize> {
360 302 : if from > text.len() {
361 0 : return None;
362 302 : }
363 1836 : text[from..].iter().position(|c| set.contains(c)).map(|i| i + from)
364 302 : }
365 :
366 : /// The completion of the terminal: the inline hint in the terminal, and the list that the host shows over it
367 : #[derive(Clone, Debug, Default)]
368 : struct TerminalCompletionOverlay {
369 : /// The completion candidates
370 : candidates: Vec<CompletionCandidate>,
371 : /// The packed candidates of the list in the same order, None if only the inline hint is shown
372 : completion: Option<CompletionT>,
373 : /// The index of the selected candidate
374 : selection: usize,
375 : /// The index of the selected qualification variant of the selected candidate
376 : variant_selection: usize,
377 : /// The prompt row of the cursor
378 : cursor_row: usize,
379 : /// The terminal column of the start of the replaced text
380 : anchor_column: usize,
381 : /// The width of the inline hint before the typed text
382 : hint_prefix_width: usize,
383 : /// The width of the inline hint after the cursor
384 : hint_suffix_width: usize,
385 : /// The width of the typed text of the target
386 : hint_current_width: usize,
387 : /// Was the hint suffix inserted before following text instead of written at the line end?
388 : hint_suffix_inserted: bool,
389 : /// Show only the inline hint without the candidate list?
390 : hint_only: bool,
391 : }
392 :
393 : /// The spinner line of a running query
394 : #[derive(Clone, Debug, Default)]
395 : struct TerminalQueryProgress {
396 : /// The current spinner frame
397 : frame: usize,
398 : /// The text after the spinner
399 : message: Vec<u8>,
400 : }
401 :
402 : /// Prefer a real extension over an identity candidate for the passive hint
403 200 : fn select_terminal_completion_hint(overlay: &mut TerminalCompletionOverlay, prompt_text: &[u8]) {
404 200 : if overlay.candidates.is_empty() {
405 0 : return;
406 200 : }
407 200 : let selected = &overlay.candidates[overlay.selection];
408 200 : let selected_end = selected.target_offset as usize + selected.target_length as usize;
409 200 : if selected_end >= prompt_text.len() || !selected.is_identity {
410 198 : return;
411 2 : }
412 2 : let begin = selected.target_offset as usize;
413 2 : let current = &prompt_text[begin..(begin + selected.target_length as usize).min(prompt_text.len())];
414 52 : for (i, candidate) in overlay.candidates.iter().enumerate() {
415 52 : let target_end = candidate.target_offset as usize + candidate.target_length as usize;
416 52 : if target_end == selected_end
417 52 : && candidate.completion_text.as_bytes().starts_with(current)
418 3 : && candidate.completion_text.as_bytes() != current
419 : {
420 1 : overlay.selection = i;
421 1 : return;
422 51 : }
423 : }
424 200 : }
425 :
426 : /// A suspended operation that waits for the host
427 : #[derive(Clone, Debug)]
428 : struct PendingEffect {
429 : /// The effect id that the host passes back on completion
430 : id: u64,
431 : /// The kind of the effect
432 : effect_type: EffectType,
433 : }
434 :
435 : /// An interactive shell session
436 : ///
437 : /// Queries and dot commands suspend once on their effect and finish when it completes. They are
438 : /// pending effects: starting one encodes its envelope, completing it renders the result.
439 : pub struct ShellSession {
440 : /// The prompt
441 : prompt: PromptBuffer,
442 : /// The width of the terminal
443 : terminal_columns: u32,
444 : /// The submitted queries, oldest first
445 : history: VecDeque<String>,
446 : /// The history entry shown in the prompt, the history length when not browsing
447 : history_cursor: usize,
448 : /// The prompt text before browsing the history
449 : history_draft: String,
450 : /// The text deleted by the last kill command, inserted by yank
451 : kill_buffer: String,
452 : /// The id of the next effect
453 : next_effect_id: u64,
454 : /// The effects that wait for the host
455 : pending_effects: Vec<PendingEffect>,
456 : /// Clear the screen when the terminal query finishes, requested by the last command
457 : clear_terminal_after_command: bool,
458 : /// Complete tables of non-default databases with their qualified name?
459 : auto_qualify_non_default_database_tables: bool,
460 : /// The keyword case of completions, 1 is upper case
461 : keyword_case: u32,
462 : /// The names of the dot commands
463 : commands: Vec<String>,
464 : /// The terminal prompt, the default prompt if empty
465 : terminal_prompt: Vec<u8>,
466 : /// The prefix of continuation lines, with `->` below the `>` of the prompt
467 : terminal_continuation: Vec<u8>,
468 : /// The SGR sequence of the prompt and continuation prefixes
469 : terminal_prompt_style: Vec<u8>,
470 : /// The number of terminal rows of the rendered prompt
471 : terminal_prompt_rows: usize,
472 : /// The prompt row of the cursor
473 : terminal_prompt_cursor_row: usize,
474 : /// What the host should do after the last terminal input
475 : terminal_action: PromptInputAction,
476 : /// The completion overlay, if open
477 : overlay: Option<TerminalCompletionOverlay>,
478 : /// Bumped when a completion list gets new candidates
479 : terminal_completion_revision: u64,
480 : /// The progress line of a running query, if shown
481 : progress: Option<TerminalQueryProgress>,
482 : }
483 :
484 : impl ShellSession {
485 111 : pub fn new(catalog: &Catalog, terminal_columns: u32, auto_qualify_non_default_database_tables: bool) -> Self {
486 111 : Self {
487 111 : prompt: PromptBuffer::new(catalog),
488 111 : terminal_columns: terminal_columns.max(1),
489 111 : history: VecDeque::new(),
490 111 : history_cursor: 0,
491 111 : history_draft: String::new(),
492 111 : kill_buffer: String::new(),
493 111 : next_effect_id: 1,
494 111 : pending_effects: Vec::new(),
495 111 : clear_terminal_after_command: false,
496 111 : auto_qualify_non_default_database_tables,
497 111 : keyword_case: 0,
498 111 : commands: Vec::new(),
499 111 : terminal_prompt: Vec::new(),
500 111 : terminal_continuation: b" -> ".to_vec(),
501 111 : terminal_prompt_style: vt100::BOLD.as_bytes().to_vec(),
502 111 : terminal_prompt_rows: 1,
503 111 : terminal_prompt_cursor_row: 0,
504 111 : terminal_action: PromptInputAction::None,
505 111 : overlay: None,
506 111 : terminal_completion_revision: 0,
507 111 : progress: None,
508 111 : }
509 111 : }
510 :
511 : /// The prompt
512 6 : pub fn prompt(&self) -> &PromptBuffer {
513 6 : &self.prompt
514 6 : }
515 :
516 1 : pub fn prompt_mut(&mut self) -> &mut PromptBuffer {
517 1 : &mut self.prompt
518 1 : }
519 :
520 1 : pub fn resize(&mut self, terminal_columns: u32) {
521 1 : self.terminal_columns = terminal_columns.max(1);
522 1 : }
523 :
524 : /// Set the keyword case of completions as a `KeywordCase` value (1 is upper case)
525 5 : pub fn set_keyword_case(&mut self, keyword_case: u32) {
526 5 : self.keyword_case = keyword_case;
527 5 : }
528 :
529 : /// Set the color of the terminal prompt, bold in the default foreground color if none
530 2 : pub fn set_terminal_prompt_color(&mut self, rgb: Option<[u8; 3]>) {
531 2 : self.terminal_prompt_style = match rgb {
532 1 : Some([r, g, b]) => format!("\x1b[1;38;2;{r};{g};{b}m").into_bytes(),
533 1 : None => vt100::BOLD.as_bytes().to_vec(),
534 : };
535 2 : }
536 :
537 252 : fn upper_keywords(&self) -> bool {
538 : // static_cast to the uint8 enum
539 252 : self.keyword_case as u8 == 1
540 252 : }
541 :
542 : /// Register the dot commands, one name per line
543 13 : pub fn set_commands(&mut self, commands: &[u8]) -> ShellStatus {
544 13 : if !is_valid(commands) {
545 1 : return ShellStatus::InvalidArgument;
546 12 : }
547 12 : let mut parsed = Vec::new();
548 12 : let mut begin = 0;
549 22 : while begin < commands.len() {
550 20 : let end = find_from(commands, b'\n', begin);
551 20 : let name = &commands[begin..end.unwrap_or(commands.len())];
552 20 : if name.is_empty()
553 19 : || name[0] == b'.'
554 86 : || !name.iter().all(|&c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'_' || c == b'-')
555 : {
556 4 : return ShellStatus::InvalidArgument;
557 16 : }
558 16 : parsed.push(String::from_utf8(name.to_vec()).unwrap());
559 16 : match end {
560 10 : Some(end) => begin = end + 1,
561 6 : None => break,
562 : }
563 : }
564 8 : self.commands = parsed;
565 8 : ShellStatus::Ok
566 13 : }
567 :
568 1 : pub fn terminal_action(&self) -> PromptInputAction {
569 1 : self.terminal_action
570 1 : }
571 :
572 439 : fn snapshot_prompt(&self, status: ShellStatus, message: &str) -> PromptSnapshot {
573 439 : PromptSnapshot {
574 439 : status,
575 439 : revision: self.prompt.revision(),
576 439 : cursor_byte_offset: self.prompt.cursor_byte_offset() as u32,
577 439 : text: self.prompt.text(),
578 439 : message: message.to_string(),
579 439 : action: PromptInputAction::None,
580 439 : }
581 439 : }
582 :
583 33 : pub fn set_prompt(&mut self, text: &[u8]) -> PromptSnapshot {
584 33 : if !self.prompt.set_text(text) {
585 1 : return self.snapshot_prompt(ShellStatus::InvalidArgument, "prompt must contain valid UTF-8");
586 32 : }
587 32 : self.reset_history_cursor();
588 32 : self.snapshot_prompt(ShellStatus::Ok, "")
589 33 : }
590 :
591 3 : pub fn insert_prompt(&mut self, text: &[u8]) -> PromptSnapshot {
592 3 : if !self.prompt.insert(text) {
593 2 : return self
594 2 : .snapshot_prompt(ShellStatus::InvalidArgument, "prompt insertion must contain valid non-empty UTF-8");
595 1 : }
596 1 : self.reset_history_cursor();
597 1 : self.snapshot_prompt(ShellStatus::Ok, "")
598 3 : }
599 :
600 40 : pub fn move_prompt_left(&mut self) -> PromptSnapshot {
601 40 : self.prompt.move_left();
602 40 : self.snapshot_prompt(ShellStatus::Ok, "")
603 40 : }
604 :
605 21 : pub fn move_prompt_right(&mut self) -> PromptSnapshot {
606 21 : self.prompt.move_right();
607 21 : self.snapshot_prompt(ShellStatus::Ok, "")
608 21 : }
609 :
610 1 : pub fn delete_prompt_backward(&mut self) -> PromptSnapshot {
611 1 : self.prompt.delete_backward();
612 1 : self.snapshot_prompt(ShellStatus::Ok, "")
613 1 : }
614 :
615 1 : pub fn delete_prompt_forward(&mut self) -> PromptSnapshot {
616 1 : self.prompt.delete_forward();
617 1 : self.snapshot_prompt(ShellStatus::Ok, "")
618 1 : }
619 :
620 : /// Complete the prompt at the cursor. Errors are the messages of the core errors (`error_message`).
621 25 : pub fn complete_prompt(&mut self, limit: usize, catalog: &Catalog) -> Result<Vec<CompletionCandidate>, String> {
622 25 : self.complete_prompt_packed(limit, catalog).map(|(candidates, _)| candidates)
623 25 : }
624 :
625 : /// Complete the prompt at the cursor, with the same candidates packed for the list of the host
626 258 : fn complete_prompt_packed(
627 258 : &mut self,
628 258 : limit: usize,
629 258 : catalog: &Catalog,
630 258 : ) -> Result<(Vec<CompletionCandidate>, CompletionT), String> {
631 258 : let prompt_text = self.prompt.text();
632 258 : let text = prompt_text.as_bytes();
633 258 : let cursor = self.prompt.cursor_byte_offset();
634 258 : if let Some(command_begin) = find_first_not_of(text, b" \t") {
635 256 : if text[command_begin] == b'.'
636 7 : && cursor > command_begin
637 7 : && find_first_of(text, b"\r\n", command_begin).is_none()
638 : {
639 7 : let target_end = find_first_of(text, b" \t", command_begin).unwrap_or(text.len());
640 7 : if cursor <= target_end {
641 6 : let prefix = &text[command_begin + 1..cursor];
642 6 : let mut candidates = Vec::with_capacity(limit.min(self.commands.len()));
643 10 : for command in &self.commands {
644 10 : if !command.as_bytes().starts_with(prefix) {
645 4 : continue;
646 6 : }
647 6 : let completion = format!(".{}", command);
648 6 : candidates.push(CompletionCandidate {
649 6 : display_text: completion.clone(),
650 6 : completion_cursor_offset: completion.len() as u32,
651 6 : completion_text: completion,
652 6 : target_offset: command_begin as u32,
653 6 : target_length: (target_end - command_begin) as u32,
654 6 : qualification_target_offset: command_begin as u32,
655 6 : qualification_target_length: (target_end - command_begin) as u32,
656 6 : ..Default::default()
657 6 : });
658 6 : if candidates.len() == limit {
659 0 : break;
660 6 : }
661 : }
662 6 : let mut packed = CompletionT::default();
663 6 : packed.candidates = Some(
664 6 : candidates
665 6 : .iter()
666 8 : .map(|candidate| {
667 6 : let mut packed_candidate = CompletionCandidateT::default();
668 6 : packed_candidate.display_text = Some(candidate.display_text.clone());
669 6 : packed_candidate.completion_text = Some(candidate.completion_text.clone());
670 6 : packed_candidate
671 6 : })
672 6 : .collect(),
673 : );
674 6 : return Ok((candidates, packed));
675 1 : }
676 249 : }
677 2 : }
678 :
679 252 : let upper_keywords = self.upper_keywords();
680 252 : let auto_qualify = self.auto_qualify_non_default_database_tables;
681 252 : let script = self.prompt.script_mut();
682 252 : script.analyze(catalog, true).map_err(error_message)?;
683 252 : let cursor = script.move_cursor(catalog, cursor);
684 252 : let completion = Script::complete_at_cursor(&cursor, limit).map_err(error_message)?;
685 252 : let mut packed = pack_completion(&completion, upper_keywords);
686 252 : let mut candidates = Vec::with_capacity(completion.top_candidates.len());
687 4707 : for (candidate_index, candidate) in completion.top_candidates.iter().enumerate() {
688 4707 : let upper_keyword = upper_keywords && candidate.keyword_symbol.is_some();
689 4707 : let keyword_text =
690 4707 : if upper_keyword { candidate.completion_text.to_ascii_uppercase() } else { String::new() };
691 4707 : let mut completion_text = if upper_keyword {
692 72 : keyword_text.clone()
693 4635 : } else if candidate.completion_text_is_verbatim {
694 53 : candidate.completion_text.to_string()
695 : } else {
696 4582 : quote_identifier(&candidate.completion_text)
697 : };
698 4707 : let objects: Vec<_> =
699 4707 : candidate.catalog_objects.iter().map(|&co| &completion.candidate_objects[co]).collect();
700 4707 : let is_function = objects.iter().any(|o| o.object.object_type() == CatalogObjectType::FunctionDeclaration);
701 4707 : if is_function {
702 2 : completion_text.push_str("()");
703 4705 : }
704 4707 : let mut qualification_texts = Vec::new();
705 4707 : let mut qualified_objects = Vec::new();
706 4707 : for (object_index, object) in objects.iter().enumerate() {
707 111 : let auto_qualify_table = auto_qualify
708 12 : && object.object.object_type() == CatalogObjectType::TableDeclaration
709 12 : && object.qualified_name.len() == 3
710 12 : && !ci_eq(object.qualified_name[0].as_bytes(), DEFAULT_DATABASE_NAME.as_bytes());
711 111 : if (object.prefer_qualified || auto_qualify_table) && !object.qualified_name.is_empty() {
712 41 : let mut qualification_text = String::new();
713 96 : for (i, name) in object.qualified_name.iter().enumerate() {
714 96 : if i > 0 {
715 55 : qualification_text.push('.');
716 55 : }
717 96 : qualification_text.push_str("e_identifier(name));
718 : }
719 41 : if is_function {
720 0 : qualification_text.push_str("()");
721 41 : }
722 41 : let qualification_offset = candidate.target_location_qualified.offset() as usize;
723 41 : let target_offset = candidate.target_location.offset() as usize;
724 41 : let qualifier_already_present =
725 41 : qualification_offset <= target_offset && target_offset <= text.len() && {
726 41 : let mut present = text[qualification_offset..target_offset].to_vec();
727 41 : present.extend_from_slice(completion_text.as_bytes());
728 41 : qualification_text.as_bytes() == present
729 : };
730 41 : if !qualifier_already_present {
731 26 : qualification_texts.push(qualification_text);
732 26 : qualified_objects.push(object_index);
733 26 : }
734 70 : }
735 : }
736 4707 : if let Some(packed_objects) =
737 4707 : packed.candidates.as_mut().and_then(|packed| packed[candidate_index].catalog_objects.as_mut())
738 4707 : {
739 4707 : keep_variant_objects(packed_objects, &qualified_objects);
740 4707 : }
741 4707 : let mut continuation = candidate.keyword_continuation.unwrap_or("").to_string();
742 4707 : if upper_keywords {
743 74 : continuation = continuation.to_ascii_uppercase();
744 4633 : }
745 4707 : let cursor_offset = completion_text.len() - if completion_text.ends_with("()") { 1 } else { 0 };
746 4707 : candidates.push(CompletionCandidate {
747 4707 : display_text: if upper_keyword { keyword_text } else { candidate.completion_text.to_string() },
748 4707 : completion_text,
749 4707 : continuation_text: continuation,
750 4707 : is_identity: candidate.candidate_tags & CandidateTag::IDENTITY.0 != 0,
751 4707 : qualification_texts,
752 4707 : completion_cursor_offset: cursor_offset as u32,
753 4707 : target_offset: candidate.target_location.offset(),
754 4707 : target_length: candidate.target_location.length(),
755 4707 : qualification_target_offset: candidate.target_location_qualified.offset(),
756 4707 : qualification_target_length: candidate.target_location_qualified.length(),
757 : });
758 : }
759 252 : Ok((candidates, packed))
760 258 : }
761 :
762 : /// The completion list that the host shows over the terminal, None if there is none
763 28 : pub fn terminal_completion_list(&self) -> Option<TerminalCompletionList> {
764 28 : let overlay = self.overlay.as_ref()?;
765 21 : overlay.completion.as_ref()?;
766 18 : Some(TerminalCompletionList {
767 18 : revision: self.terminal_completion_revision,
768 18 : selection: overlay.selection as u32,
769 18 : variant: overlay.variant_selection as u32,
770 18 : anchor_column: overlay.anchor_column as u32,
771 18 : })
772 28 : }
773 :
774 : /// The candidates of the completion list, in the order of the prompt candidates
775 11 : pub fn terminal_completion_buffer(&self) -> Option<&CompletionT> {
776 11 : self.overlay.as_ref()?.completion.as_ref()
777 11 : }
778 :
779 : /// Replace the target of a candidate
780 21 : pub fn apply_completion(&mut self, candidate: &CompletionCandidate) -> PromptSnapshot {
781 21 : if !self.prompt.replace_byte_range(
782 21 : candidate.target_offset as usize,
783 21 : candidate.target_length as usize,
784 21 : &candidate.completion_text,
785 21 : ) {
786 1 : return self.snapshot_prompt(ShellStatus::InvalidArgument, "completion target is not on prompt boundaries");
787 20 : }
788 20 : self.prompt.move_to_byte_offset(candidate.target_offset as usize + candidate.completion_cursor_offset as usize);
789 20 : self.snapshot_prompt(ShellStatus::Ok, "")
790 21 : }
791 :
792 : /// Apply a completion whose texts may be invalid UTF-8
793 3 : pub fn apply_completion_bytes(
794 3 : &mut self,
795 3 : display_text: &[u8],
796 3 : completion_text: &[u8],
797 3 : target_offset: u32,
798 3 : target_length: u32,
799 3 : ) -> PromptSnapshot {
800 3 : match std::str::from_utf8(completion_text) {
801 2 : Ok(completion) => self.apply_completion(&CompletionCandidate {
802 2 : display_text: String::from_utf8_lossy(display_text).into_owned(),
803 2 : completion_text: completion.to_string(),
804 2 : completion_cursor_offset: completion_text.len() as u32,
805 2 : target_offset,
806 2 : target_length,
807 2 : qualification_target_offset: target_offset,
808 2 : qualification_target_length: target_length,
809 2 : ..Default::default()
810 2 : }),
811 : Err(_) => {
812 1 : self.snapshot_prompt(ShellStatus::InvalidArgument, "completion target is not on prompt boundaries")
813 : }
814 : }
815 3 : }
816 :
817 : /// Apply a semantic input to the prompt
818 318 : pub fn consume_prompt_input(&mut self, key: PromptInputKey, text: &[u8]) -> PromptSnapshot {
819 318 : let mut action = PromptInputAction::None;
820 318 : match key {
821 : PromptInputKey::Text => {
822 230 : if !self.prompt.insert(text) {
823 2 : return self.snapshot_prompt(
824 2 : ShellStatus::InvalidArgument,
825 2 : "prompt input must contain valid non-empty UTF-8",
826 : );
827 228 : }
828 228 : self.reset_history_cursor();
829 : }
830 : PromptInputKey::Enter => {
831 15 : if self.prompt_is_complete() {
832 7 : action = PromptInputAction::Submit;
833 10 : } else {
834 8 : self.prompt.insert(b"\n");
835 8 : self.reset_history_cursor();
836 8 : }
837 : }
838 : PromptInputKey::ForceSubmit => {
839 3 : if !self.prompt.text().is_empty() {
840 2 : action = PromptInputAction::Submit;
841 2 : }
842 : }
843 2 : PromptInputKey::Tab => action = PromptInputAction::Complete,
844 6 : PromptInputKey::Backspace => {
845 6 : self.prompt.delete_backward();
846 6 : self.reset_history_cursor();
847 6 : }
848 2 : PromptInputKey::Delete => {
849 2 : self.prompt.delete_forward();
850 2 : self.reset_history_cursor();
851 2 : }
852 4 : PromptInputKey::Left => {
853 4 : self.prompt.move_left();
854 4 : }
855 2 : PromptInputKey::Right => {
856 2 : self.prompt.move_right();
857 2 : }
858 2 : PromptInputKey::Start => {
859 2 : self.prompt.move_to_start();
860 2 : }
861 3 : PromptInputKey::End => {
862 3 : self.prompt.move_to_end();
863 3 : }
864 4 : PromptInputKey::LineStart => {
865 4 : self.prompt.move_to_line_start();
866 4 : }
867 2 : PromptInputKey::LineEnd => {
868 2 : self.prompt.move_to_line_end();
869 2 : }
870 4 : PromptInputKey::WordLeft => {
871 4 : self.prompt.move_word_left();
872 4 : }
873 3 : PromptInputKey::WordRight => {
874 3 : self.prompt.move_word_right();
875 3 : }
876 : PromptInputKey::KillLine => {
877 2 : if self.prompt.delete_to_line_end(Some(&mut self.kill_buffer)) {
878 1 : self.reset_history_cursor();
879 1 : }
880 : }
881 : PromptInputKey::BackwardKillLine => {
882 2 : if self.prompt.delete_to_line_start(Some(&mut self.kill_buffer)) {
883 2 : self.reset_history_cursor();
884 2 : }
885 : }
886 : PromptInputKey::KillWord => {
887 2 : if self.prompt.delete_word_forward(Some(&mut self.kill_buffer)) {
888 2 : self.reset_history_cursor();
889 2 : }
890 : }
891 : PromptInputKey::BackwardKillWord => {
892 2 : if self.prompt.delete_word_backward(Some(&mut self.kill_buffer)) {
893 2 : self.reset_history_cursor();
894 2 : }
895 : }
896 : PromptInputKey::Yank => {
897 4 : if !self.kill_buffer.is_empty() {
898 4 : let kill_buffer = self.kill_buffer.clone();
899 4 : self.prompt.insert(kill_buffer.as_bytes());
900 4 : self.reset_history_cursor();
901 4 : }
902 : }
903 : PromptInputKey::Transpose => {
904 3 : if self.prompt.transpose_characters() {
905 2 : self.reset_history_cursor();
906 2 : }
907 : }
908 1 : PromptInputKey::ClearScreen | PromptInputKey::Abort | PromptInputKey::Escape => {}
909 : PromptInputKey::Eof => {
910 3 : if self.prompt.text().is_empty() {
911 2 : action = PromptInputAction::Exit;
912 2 : } else {
913 1 : self.prompt.delete_forward();
914 1 : self.reset_history_cursor();
915 1 : }
916 : }
917 : PromptInputKey::Up | PromptInputKey::HistoryPrevious => {
918 9 : let moved =
919 9 : key == PromptInputKey::Up && self.history_cursor == self.history.len() && self.prompt.move_up();
920 9 : if !moved && !self.history.is_empty() && self.history_cursor > 0 {
921 6 : if self.history_cursor == self.history.len() {
922 3 : self.history_draft = self.prompt.text();
923 3 : }
924 6 : self.history_cursor -= 1;
925 6 : let entry = self.history[self.history_cursor].clone();
926 6 : self.prompt.set_text(entry.as_bytes());
927 3 : }
928 : }
929 : PromptInputKey::Down | PromptInputKey::HistoryNext => {
930 6 : let moved =
931 6 : key == PromptInputKey::Down && self.history_cursor == self.history.len() && self.prompt.move_down();
932 6 : if !moved && self.history_cursor < self.history.len() {
933 5 : self.history_cursor += 1;
934 5 : let entry = if self.history_cursor < self.history.len() {
935 3 : self.history[self.history_cursor].clone()
936 : } else {
937 2 : self.history_draft.clone()
938 : };
939 5 : self.prompt.set_text(entry.as_bytes());
940 1 : }
941 : }
942 2 : PromptInputKey::Cancel => {
943 2 : self.prompt.set_text(b"");
944 2 : self.reset_history_cursor();
945 2 : }
946 : }
947 316 : let mut snapshot = self.snapshot_prompt(ShellStatus::Ok, "");
948 316 : snapshot.action = action;
949 316 : snapshot
950 318 : }
951 :
952 620 : fn terminal_prompt_text(&self) -> &[u8] {
953 620 : if self.terminal_prompt.is_empty() {
954 513 : DEFAULT_TERMINAL_PROMPT
955 : } else {
956 107 : &self.terminal_prompt
957 : }
958 620 : }
959 :
960 386 : fn terminal_output(&self, result: Result<ShellOperation, String>) -> TerminalOutput {
961 386 : match result {
962 386 : Ok(operation) => {
963 386 : TerminalOutput { status: operation.status, action: self.terminal_action, data: operation.data }
964 : }
965 0 : Err(message) => TerminalOutput {
966 0 : status: ShellStatus::InternalError,
967 0 : action: PromptInputAction::None,
968 0 : data: message.into_bytes(),
969 0 : },
970 : }
971 386 : }
972 :
973 : /// Open the terminal with a prompt, "dashql> " if empty
974 65 : pub fn open_terminal(&mut self, prompt: &[u8]) -> TerminalOutput {
975 65 : self.terminal_action = PromptInputAction::None;
976 65 : self.overlay = None;
977 65 : let result = if !is_valid(prompt) {
978 1 : ShellOperation::new(ShellStatus::InvalidArgument, "terminal prompt must contain valid UTF-8")
979 64 : } else if prompt.len() > TERMINAL_PROMPT_CAPACITY {
980 1 : ShellOperation::new(ShellStatus::InvalidArgument, "terminal prompt is too long")
981 : } else {
982 63 : self.terminal_prompt = prompt.to_vec();
983 63 : let rendered_prompt = self.terminal_prompt_text().to_vec();
984 381 : let marker_column = match rendered_prompt.iter().rposition(|&c| c == b'>') {
985 61 : Some(marker) => display_width(&rendered_prompt[..marker]),
986 2 : None => display_width(&rendered_prompt),
987 : };
988 63 : self.terminal_continuation = vec![b' '; marker_column.saturating_sub(1)];
989 63 : self.terminal_continuation.extend_from_slice(b"-> ");
990 63 : self.terminal_prompt_rows = 1;
991 63 : self.terminal_prompt_cursor_row = 0;
992 63 : let mut output = Vec::new();
993 63 : push(&mut output, vt100::DISABLE_AUTO_WRAP);
994 63 : output.extend(self.render_terminal_prompt());
995 63 : ShellOperation::ok(output)
996 : };
997 65 : self.terminal_output(Ok(result))
998 65 : }
999 :
1000 : /// Apply a semantic input to the terminal and render it
1001 298 : pub fn consume_terminal_input(&mut self, key: PromptInputKey, text: &[u8], catalog: &Catalog) -> TerminalOutput {
1002 298 : let result = self.consume_terminal_input_impl(key, text, catalog);
1003 298 : self.terminal_output(result)
1004 298 : }
1005 :
1006 298 : fn consume_terminal_input_impl(
1007 298 : &mut self,
1008 298 : key: PromptInputKey,
1009 298 : text: &[u8],
1010 298 : catalog: &Catalog,
1011 298 : ) -> Result<ShellOperation, String> {
1012 298 : self.terminal_action = PromptInputAction::None;
1013 298 : if let Some(overlay) = &self.overlay {
1014 198 : if !overlay.hint_only {
1015 175 : let variant_count = overlay.candidates[overlay.selection].qualification_texts.len();
1016 175 : if key == PromptInputKey::HistoryPrevious || key == PromptInputKey::Up {
1017 4 : return Ok(self.move_terminal_completion(-1));
1018 171 : }
1019 171 : if key == PromptInputKey::HistoryNext || key == PromptInputKey::Down {
1020 7 : return Ok(self.move_terminal_completion(1));
1021 164 : }
1022 164 : if variant_count > 1 && key == PromptInputKey::Left {
1023 3 : return Ok(self.move_terminal_completion_variant(-1));
1024 161 : }
1025 161 : if variant_count > 1 && key == PromptInputKey::Right {
1026 2 : return Ok(self.move_terminal_completion_variant(1));
1027 159 : }
1028 23 : }
1029 182 : let hint_only = overlay.hint_only;
1030 182 : if key == PromptInputKey::Tab || (key == PromptInputKey::Enter && !hint_only) {
1031 18 : return Ok(self.accept_terminal_completion());
1032 164 : }
1033 164 : if key == PromptInputKey::Escape || key == PromptInputKey::Abort {
1034 6 : let mut output = self.clear_terminal_completion_overlay();
1035 6 : self.overlay = None;
1036 6 : output.extend(self.render_terminal_prompt());
1037 6 : return Ok(ShellOperation::ok(output));
1038 158 : }
1039 100 : }
1040 258 : if key == PromptInputKey::Abort {
1041 1 : return Ok(ShellOperation::ok(Vec::new()));
1042 257 : }
1043 257 : if key == PromptInputKey::Escape {
1044 2 : self.terminal_action = PromptInputAction::Exit;
1045 2 : return Ok(ShellOperation::ok(vt100::ENABLE_AUTO_WRAP));
1046 255 : }
1047 255 : let mut output_prefix = Vec::new();
1048 255 : if self.overlay.is_some() {
1049 158 : output_prefix = self.clear_terminal_completion_overlay();
1050 158 : self.overlay = None;
1051 175 : }
1052 255 : if key == PromptInputKey::ClearScreen {
1053 1 : output_prefix.clear();
1054 1 : self.terminal_prompt_rows = 1;
1055 1 : self.terminal_prompt_cursor_row = 0;
1056 1 : push(&mut output_prefix, vt100::CLEAR_SCREEN);
1057 1 : output_prefix.extend(self.render_terminal_prompt());
1058 1 : return Ok(ShellOperation::ok(output_prefix));
1059 254 : }
1060 254 : let snapshot = self.consume_prompt_input(key, text);
1061 254 : if snapshot.status != ShellStatus::Ok {
1062 1 : return Ok(ShellOperation::new(snapshot.status, snapshot.message));
1063 253 : }
1064 253 : let action = snapshot.action;
1065 253 : if action == PromptInputAction::Exit {
1066 1 : self.terminal_action = action;
1067 1 : push(&mut output_prefix, vt100::ENABLE_AUTO_WRAP);
1068 1 : return Ok(ShellOperation::ok(output_prefix));
1069 252 : }
1070 252 : if action == PromptInputAction::Submit {
1071 4 : if !output_prefix.is_empty() {
1072 0 : output_prefix.extend(self.render_terminal_prompt());
1073 4 : }
1074 4 : self.terminal_action = action;
1075 : // The cursor row can be stale after prompt edits outside of the terminal, the row count then wraps around
1076 4 : let rows_below = self.terminal_prompt_rows.wrapping_sub(self.terminal_prompt_cursor_row).wrapping_sub(1);
1077 4 : append_cursor_move(&mut output_prefix, rows_below, Command::CursorDown);
1078 4 : self.terminal_prompt_rows = 1;
1079 4 : self.terminal_prompt_cursor_row = 0;
1080 4 : push(&mut output_prefix, vt100::ENABLE_AUTO_WRAP);
1081 4 : push(&mut output_prefix, vt100::NEW_LINE);
1082 4 : return Ok(ShellOperation::ok(output_prefix));
1083 248 : }
1084 248 : if action == PromptInputAction::Complete {
1085 2 : let (candidates, completion) = self.complete_prompt_packed(50, catalog)?;
1086 2 : if candidates.len() == 1 {
1087 1 : let candidate = candidates.into_iter().next().unwrap();
1088 1 : return Ok(self.apply_terminal_candidate(candidate, 0, output_prefix));
1089 1 : }
1090 1 : if candidates.len() > 1 {
1091 1 : output_prefix.extend(self.open_terminal_completion_overlay(candidates, completion));
1092 1 : return Ok(ShellOperation::ok(output_prefix));
1093 0 : }
1094 246 : }
1095 246 : if key == PromptInputKey::Cancel {
1096 1 : self.terminal_prompt_rows = 1;
1097 1 : self.terminal_prompt_cursor_row = 0;
1098 1 : let mut output = output_prefix;
1099 1 : output.extend_from_slice(b"^C");
1100 1 : push(&mut output, vt100::NEW_LINE);
1101 1 : output.extend(self.render_terminal_prompt());
1102 1 : return Ok(ShellOperation::ok(output));
1103 245 : }
1104 245 : output_prefix.extend(self.render_terminal_prompt());
1105 245 : let navigation_key = matches!(
1106 245 : key,
1107 : PromptInputKey::Left
1108 : | PromptInputKey::Right
1109 : | PromptInputKey::Up
1110 : | PromptInputKey::Down
1111 : | PromptInputKey::Start
1112 : | PromptInputKey::End
1113 : | PromptInputKey::LineStart
1114 : | PromptInputKey::LineEnd
1115 : | PromptInputKey::WordLeft
1116 : | PromptInputKey::WordRight
1117 : );
1118 245 : if key != PromptInputKey::ForceSubmit && key != PromptInputKey::Tab && !navigation_key {
1119 231 : output_prefix.extend(self.refresh_terminal_completion_overlay(catalog)?);
1120 14 : }
1121 245 : Ok(ShellOperation::ok(output_prefix))
1122 298 : }
1123 :
1124 : /// Print the output of a query and a fresh prompt
1125 6 : pub fn finish_terminal_query(&mut self, output: &[u8], error: bool) -> TerminalOutput {
1126 6 : self.terminal_action = PromptInputAction::None;
1127 6 : let mut rendered = self.clear_terminal_completion_overlay();
1128 6 : self.overlay = None;
1129 6 : rendered.extend(self.clear_terminal_query_progress_output());
1130 6 : push(&mut rendered, vt100::ENABLE_AUTO_WRAP);
1131 6 : if self.clear_terminal_after_command {
1132 1 : push(&mut rendered, vt100::CLEAR_SCREEN);
1133 1 : self.clear_terminal_after_command = false;
1134 1 : } else {
1135 5 : if error {
1136 1 : push(&mut rendered, vt100::FOREGROUND_RED);
1137 4 : }
1138 5 : rendered.extend_from_slice(output);
1139 5 : if error {
1140 1 : push(&mut rendered, vt100::RESET_ATTRIBUTES);
1141 4 : }
1142 5 : push(&mut rendered, vt100::NEW_LINE);
1143 : }
1144 6 : self.prompt.set_text(b"");
1145 6 : self.terminal_prompt_rows = 1;
1146 6 : self.terminal_prompt_cursor_row = 0;
1147 6 : push(&mut rendered, vt100::DISABLE_AUTO_WRAP);
1148 6 : rendered.extend(self.render_terminal_prompt());
1149 6 : self.terminal_output(Ok(ShellOperation::ok(rendered)))
1150 6 : }
1151 :
1152 : /// Render the spinner line of a running query
1153 11 : pub fn render_terminal_query_progress(&mut self, message: &[u8], advance_frame: bool) -> TerminalOutput {
1154 11 : self.terminal_action = PromptInputAction::None;
1155 11 : if !is_valid(message) {
1156 1 : return self.terminal_output(Ok(ShellOperation::new(
1157 1 : ShellStatus::InvalidArgument,
1158 1 : "terminal query progress must contain valid UTF-8",
1159 1 : )));
1160 10 : }
1161 10 : let normalized = normalize_terminal_progress_message(message);
1162 10 : let active = self.progress.is_some();
1163 10 : let progress = self.progress.get_or_insert_with(Default::default);
1164 10 : if !normalized.is_empty() {
1165 5 : progress.message = normalized;
1166 6 : }
1167 10 : if !active {
1168 6 : progress.frame = 0;
1169 6 : if progress.message.is_empty() {
1170 3 : progress.message = b"Executing query".to_vec();
1171 5 : }
1172 4 : } else if advance_frame {
1173 2 : progress.frame = (progress.frame + 1) % TERMINAL_QUERY_SPINNER_FRAMES.len();
1174 3 : }
1175 10 : let mut line = TERMINAL_QUERY_SPINNER_FRAMES[progress.frame].as_bytes().to_vec();
1176 10 : line.push(b' ');
1177 10 : line.extend_from_slice(&progress.message);
1178 10 : let line = truncate_display_text(&line, (self.terminal_columns as usize).max(1));
1179 10 : let mut output = Vec::new();
1180 10 : push(&mut output, vt100::DISABLE_AUTO_WRAP);
1181 10 : push(&mut output, vt100::CARRIAGE_RETURN);
1182 10 : push(&mut output, vt100::ERASE_ENTIRE_LINE);
1183 10 : output.extend(line);
1184 10 : self.terminal_output(Ok(ShellOperation::ok(output)))
1185 11 : }
1186 :
1187 4 : pub fn clear_terminal_query_progress(&mut self) -> TerminalOutput {
1188 4 : self.terminal_action = PromptInputAction::None;
1189 4 : let output = self.clear_terminal_query_progress_output();
1190 4 : self.terminal_output(Ok(ShellOperation::ok(output)))
1191 4 : }
1192 :
1193 12 : fn clear_terminal_query_progress_output(&mut self) -> Vec<u8> {
1194 12 : if self.progress.take().is_none() {
1195 9 : return Vec::new();
1196 3 : }
1197 3 : let mut output = Vec::new();
1198 3 : push(&mut output, vt100::CARRIAGE_RETURN);
1199 3 : push(&mut output, vt100::ERASE_ENTIRE_LINE);
1200 3 : push(&mut output, vt100::ENABLE_AUTO_WRAP);
1201 3 : output
1202 12 : }
1203 :
1204 : /// Print a status line and a fresh prompt
1205 2 : pub fn render_terminal_status(&mut self, message: &[u8]) -> TerminalOutput {
1206 2 : self.terminal_action = PromptInputAction::None;
1207 2 : let mut output = self.clear_terminal_completion_overlay();
1208 2 : self.overlay = None;
1209 2 : output.extend(self.clear_terminal_query_progress_output());
1210 2 : push(&mut output, vt100::ENABLE_AUTO_WRAP);
1211 2 : push(&mut output, vt100::NEW_LINE);
1212 2 : push(&mut output, vt100::FOREGROUND_BRIGHT_BLACK);
1213 2 : output.extend_from_slice(message);
1214 2 : push(&mut output, vt100::RESET_ATTRIBUTES);
1215 2 : push(&mut output, vt100::NEW_LINE);
1216 2 : self.terminal_prompt_rows = 1;
1217 2 : self.terminal_prompt_cursor_row = 0;
1218 2 : push(&mut output, vt100::DISABLE_AUTO_WRAP);
1219 2 : output.extend(self.render_terminal_prompt());
1220 2 : self.terminal_output(Ok(ShellOperation::ok(output)))
1221 2 : }
1222 :
1223 : /// Execute the prompt as a dot command or a query
1224 12 : pub fn submit_prompt(&mut self, catalog: &Catalog) -> ShellOperation {
1225 12 : let query = self.prompt.text();
1226 12 : if query.is_empty() {
1227 1 : return ShellOperation::new(ShellStatus::InvalidArgument, "query must not be empty");
1228 11 : }
1229 11 : self.remember_prompt(&query);
1230 11 : let bytes = query.as_bytes();
1231 11 : if let Some(query_begin) = find_first_not_of(bytes, b" \t\r\n") {
1232 11 : if bytes[query_begin] == b'.' {
1233 4 : return self.suspend_effect(EffectType::ExecuteCommand, bytes[query_begin..].to_vec());
1234 7 : }
1235 0 : }
1236 7 : if let Some(query_end) = find_last_not_of(bytes, b" \t\r\n") {
1237 7 : if bytes[query_end] == b';' {
1238 3 : return self.start_query(&bytes[..query_end], catalog);
1239 4 : }
1240 0 : }
1241 4 : self.start_query(bytes, catalog)
1242 12 : }
1243 :
1244 : /// Is the prompt a complete statement or dot command?
1245 15 : fn prompt_is_complete(&mut self) -> bool {
1246 15 : let text = self.prompt.text();
1247 15 : let bytes = text.as_bytes();
1248 15 : let ws = b" \t\r\n";
1249 15 : let Some(begin) = find_first_not_of(bytes, ws) else {
1250 3 : return false;
1251 : };
1252 12 : let end = find_last_not_of(bytes, ws);
1253 12 : if bytes[begin] == b'.' {
1254 3 : return find_first_of(bytes, b"\r\n", begin).is_none();
1255 9 : }
1256 9 : if end.is_none_or(|end| bytes[end] != b';') {
1257 2 : return false;
1258 7 : }
1259 7 : let script = self.prompt.script_mut();
1260 7 : script.parse();
1261 7 : let Some(parsed) = script.parsed.as_ref() else {
1262 0 : return false;
1263 : };
1264 7 : let symbols = &parsed.scanned.symbols;
1265 7 : if symbols.len() < 2 || symbols[symbols.len() - 2].kind != S_SEMICOLON {
1266 1 : return false;
1267 6 : }
1268 : // The remote grammar may accept syntax that the local parser does not recognize
1269 6 : if !parsed.errors.is_empty() || !parsed.scanned.errors.is_empty() {
1270 1 : return true;
1271 5 : }
1272 5 : parsed.statements.len() == 1
1273 15 : }
1274 :
1275 310 : fn reset_history_cursor(&mut self) {
1276 310 : self.history_cursor = self.history.len();
1277 310 : self.history_draft.clear();
1278 310 : }
1279 :
1280 11 : fn remember_prompt(&mut self, query: &str) {
1281 11 : if self.history.back().is_none_or(|last| last != query) {
1282 10 : self.history.push_back(query.to_string());
1283 10 : }
1284 12 : while self.history.len() > HISTORY_LIMIT {
1285 1 : self.history.pop_front();
1286 1 : }
1287 11 : self.reset_history_cursor();
1288 11 : }
1289 :
1290 : /// The history as length-prefixed (u32 little-endian) entries
1291 8 : pub fn export_history(&self) -> Vec<u8> {
1292 8 : let mut output = Vec::new();
1293 2005 : for entry in &self.history {
1294 2005 : output.extend_from_slice(&(entry.len() as u32).to_le_bytes());
1295 2005 : output.extend_from_slice(entry.as_bytes());
1296 2005 : }
1297 8 : output
1298 8 : }
1299 :
1300 : /// Replace the history with exported entries
1301 9 : pub fn import_history(&mut self, data: &[u8]) -> ShellStatus {
1302 9 : let mut history = VecDeque::new();
1303 9 : let mut offset = 0;
1304 1021 : while offset < data.len() {
1305 1015 : if data.len() - offset < 4 {
1306 1 : return ShellStatus::InvalidArgument;
1307 1014 : }
1308 1014 : let size = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
1309 1014 : offset += 4;
1310 1014 : if size > data.len() - offset {
1311 1 : return ShellStatus::InvalidArgument;
1312 1013 : }
1313 1013 : let Ok(entry) = std::str::from_utf8(&data[offset..offset + size]) else {
1314 1 : return ShellStatus::InvalidArgument;
1315 : };
1316 1012 : history.push_back(entry.to_string());
1317 1012 : offset += size;
1318 : }
1319 11 : while history.len() > HISTORY_LIMIT {
1320 5 : history.pop_front();
1321 5 : }
1322 6 : self.history = history;
1323 6 : self.reset_history_cursor();
1324 6 : ShellStatus::Ok
1325 9 : }
1326 :
1327 : /// Start a query: returns the effect envelope of the query
1328 24 : pub fn start_query(&mut self, query: &[u8], _catalog: &Catalog) -> ShellOperation {
1329 24 : if !self.pending_effects.is_empty() {
1330 2 : return ShellOperation::new(ShellStatus::Busy, "the shell already has an active operation");
1331 22 : }
1332 22 : if query.is_empty() {
1333 1 : return ShellOperation::new(ShellStatus::InvalidArgument, "query must not be empty");
1334 21 : }
1335 21 : if std::str::from_utf8(query).is_err() {
1336 : // Invalid UTF-8 is an internal error with the message of the rope
1337 1 : return ShellOperation::new(ShellStatus::InternalError, "rope input must contain valid UTF-8");
1338 20 : }
1339 20 : self.suspend_effect(EffectType::ExecuteQuery, query.to_vec())
1340 24 : }
1341 :
1342 : /// Suspend an operation on an effect and encode its envelope
1343 24 : fn suspend_effect(&mut self, effect_type: EffectType, payload: Vec<u8>) -> ShellOperation {
1344 24 : let id = self.next_effect_id;
1345 24 : self.next_effect_id += 1;
1346 24 : let mut encoded = Vec::with_capacity(EFFECT_ENVELOPE_HEADER_SIZE + payload.len());
1347 24 : encoded.extend_from_slice(&EFFECT_ENVELOPE_VERSION.to_le_bytes());
1348 24 : encoded.extend_from_slice(&(effect_type as u32).to_le_bytes());
1349 24 : encoded.extend_from_slice(&id.to_le_bytes());
1350 24 : encoded.extend_from_slice(&payload);
1351 24 : self.pending_effects.push(PendingEffect { id, effect_type });
1352 24 : ShellOperation::new(ShellStatus::Pending, encoded)
1353 24 : }
1354 :
1355 : /// Resume the operation of an effect with its result
1356 17 : pub fn complete_effect(
1357 17 : &mut self,
1358 17 : effect_id: u64,
1359 17 : status: EffectCompletionStatus,
1360 17 : data: &[u8],
1361 17 : ) -> ShellOperation {
1362 17 : let Some(index) = self.pending_effects.iter().position(|e| e.id == effect_id) else {
1363 4 : return ShellOperation::new(ShellStatus::StaleEffect, "effect is no longer pending");
1364 : };
1365 13 : let effect = self.pending_effects.remove(index);
1366 13 : match (effect.effect_type, status) {
1367 1 : (EffectType::ExecuteQuery, EffectCompletionStatus::Success) => ShellOperation::ok(data.to_vec()),
1368 : (EffectType::ExecuteCommand, EffectCompletionStatus::Success) => {
1369 3 : if data.is_empty() {
1370 1 : return ShellOperation::new(ShellStatus::InternalError, "invalid shell command result");
1371 2 : }
1372 2 : self.clear_terminal_after_command = (data[0] & 1) != 0;
1373 2 : ShellOperation::ok(data[1..].to_vec())
1374 : }
1375 3 : (_, EffectCompletionStatus::Error) => ShellOperation::ok(normalize_error(data)),
1376 6 : (_, EffectCompletionStatus::Cancelled) => ShellOperation::ok("Cancelled"),
1377 : }
1378 17 : }
1379 :
1380 : /// Cancel an effect
1381 6 : pub fn cancel_effect(&mut self, effect_id: u64) -> ShellOperation {
1382 6 : self.complete_effect(effect_id, EffectCompletionStatus::Cancelled, &[])
1383 6 : }
1384 :
1385 : /// Redraw the prompt with highlighting
1386 357 : fn render_terminal_prompt(&mut self) -> Vec<u8> {
1387 357 : let prompt_text = self.prompt.text();
1388 357 : let text = prompt_text.as_bytes();
1389 357 : let terminal_prompt = self.terminal_prompt_text().to_vec();
1390 357 : let mut highlighted = Vec::new();
1391 357 : let command_begin = find_first_not_of(text, b" \t");
1392 357 : let command_end = command_begin.and_then(|begin| find_first_of(text, b" \t\r\n", begin));
1393 357 : let command_name_end = command_end.unwrap_or(text.len());
1394 357 : let command_name: &[u8] = match command_begin {
1395 285 : Some(begin) if text[begin] == b'.' => &text[begin + 1..command_name_end],
1396 349 : _ => &[],
1397 : };
1398 357 : let is_registered_command = self.commands.iter().any(|c| c.as_bytes() == command_name);
1399 357 : if is_registered_command {
1400 3 : let command_begin = command_begin.unwrap_or(text.len());
1401 3 : highlighted.extend_from_slice(&text[..command_begin]);
1402 3 : push(&mut highlighted, vt100::BOLD);
1403 3 : highlighted.extend_from_slice(&text[command_begin..command_name_end]);
1404 3 : push(&mut highlighted, vt100::RESET_ATTRIBUTES);
1405 3 : highlighted.extend_from_slice(&text[command_name_end..]);
1406 3 : } else {
1407 354 : let script = self.prompt.script_mut();
1408 354 : if script.parsed.as_ref().is_none_or(|p| p.scanned.text_version != script.text_version()) {
1409 295 : script.parse();
1410 295 : }
1411 354 : let parsed = script.parsed.as_ref().unwrap();
1412 354 : let packed = pack_tokens(parsed);
1413 354 : let offsets = packed.token_offsets.unwrap_or_default();
1414 354 : let lengths = packed.token_lengths.unwrap_or_default();
1415 354 : let types = packed.token_types.unwrap_or_default();
1416 354 : let comments = &parsed.scanned.comments;
1417 354 : highlighted.reserve(text.len() + (offsets.len() + comments.len()) * 16);
1418 5310 : let clamp = |i: usize| i.min(text.len());
1419 354 : let mut offset = 0;
1420 354 : let mut token_idx = 0;
1421 354 : let mut comment_idx = 0;
1422 1906 : while token_idx < offsets.len() || comment_idx < comments.len() {
1423 1552 : let token_begin = offsets.get(token_idx).map_or(usize::MAX, |&o| o as usize);
1424 1552 : let comment_begin = comments.get(comment_idx).map_or(usize::MAX, |c| c.offset as usize);
1425 1552 : let is_comment = comment_begin < token_begin;
1426 1552 : let begin = if is_comment { comment_begin } else { token_begin };
1427 1552 : let length =
1428 1552 : if is_comment { comments[comment_idx].length as usize } else { lengths[token_idx] as usize };
1429 1552 : let token_type = if is_comment { ScannerTokenType::COMMENT } else { types[token_idx] };
1430 1552 : let end = begin + length;
1431 1552 : if begin > offset {
1432 926 : highlighted.extend_from_slice(&text[clamp(offset)..clamp(begin)]);
1433 926 : }
1434 1552 : let style = token_style(token_type);
1435 1552 : push(&mut highlighted, style);
1436 1552 : highlighted.extend_from_slice(&text[clamp(begin)..clamp(end)]);
1437 1552 : if !style.is_empty() {
1438 1155 : push(&mut highlighted, vt100::RESET_ATTRIBUTES);
1439 1155 : }
1440 1552 : offset = end;
1441 1552 : if is_comment {
1442 0 : comment_idx += 1;
1443 1552 : } else {
1444 1552 : token_idx += 1;
1445 1552 : }
1446 : }
1447 354 : highlighted.extend_from_slice(&text[clamp(offset)..]);
1448 : }
1449 :
1450 357 : let terminal_columns = self.terminal_columns as usize;
1451 357 : let continuation = self.terminal_continuation.clone();
1452 357 : let desired_layout = layout_prompt(text, &terminal_prompt, &continuation, terminal_columns);
1453 357 : let previous_rows = self.terminal_prompt_rows;
1454 357 : let rows_to_clear = previous_rows.max(desired_layout.rows);
1455 :
1456 357 : let mut output = Vec::new();
1457 357 : append_cursor_move(&mut output, self.terminal_prompt_cursor_row, Command::CursorUp);
1458 399 : for i in 0..previous_rows {
1459 399 : push(&mut output, vt100::CARRIAGE_RETURN);
1460 399 : push(&mut output, vt100::ERASE_ENTIRE_LINE);
1461 399 : if i + 1 < previous_rows {
1462 42 : append_cursor_move(&mut output, 1, Command::CursorDown);
1463 357 : }
1464 : }
1465 357 : for _ in previous_rows..desired_layout.rows {
1466 36 : push(&mut output, vt100::NEW_LINE);
1467 36 : }
1468 357 : append_cursor_move(&mut output, rows_to_clear - 1, Command::CursorUp);
1469 357 : if previous_rows > 1 || desired_layout.rows > 1 {
1470 44 : push(&mut output, vt100::CARRIAGE_RETURN);
1471 313 : }
1472 :
1473 357 : let rendered_layout = render_highlighted_prompt(
1474 357 : &mut output,
1475 357 : &highlighted,
1476 357 : &terminal_prompt,
1477 357 : &continuation,
1478 357 : &self.terminal_prompt_style,
1479 357 : terminal_columns,
1480 : );
1481 357 : self.terminal_prompt_rows = rendered_layout.rows;
1482 :
1483 357 : let cursor = self.prompt.cursor_byte_offset();
1484 357 : let cursor_layout = layout_prompt(&text[..cursor], &terminal_prompt, &continuation, terminal_columns);
1485 357 : self.terminal_prompt_cursor_row = cursor_layout.rows - 1;
1486 357 : if let Some(overlay) = self.overlay.as_mut() {
1487 14 : overlay.cursor_row = self.terminal_prompt_cursor_row;
1488 343 : }
1489 357 : push(&mut output, vt100::CARRIAGE_RETURN);
1490 357 : append_cursor_move(&mut output, self.terminal_prompt_rows - 1, Command::CursorUp);
1491 357 : append_cursor_move(&mut output, self.terminal_prompt_cursor_row, Command::CursorDown);
1492 357 : append_cursor_move(&mut output, cursor_layout.column, Command::CursorForward);
1493 357 : output
1494 357 : }
1495 :
1496 200 : fn open_terminal_completion_overlay(
1497 200 : &mut self,
1498 200 : candidates: Vec<CompletionCandidate>,
1499 200 : completion: CompletionT,
1500 200 : ) -> Vec<u8> {
1501 200 : let mut overlay = TerminalCompletionOverlay { candidates, ..Default::default() };
1502 200 : let prompt_text = self.prompt.text();
1503 200 : let text = prompt_text.as_bytes();
1504 200 : let target = (overlay.candidates[0].target_offset as usize).min(text.len());
1505 200 : let follows_dot = target > 0 && text[target - 1] == b'.';
1506 200 : overlay.hint_only = !follows_dot && (overlay.candidates.len() == 1 || overlay.candidates[0].target_length == 0);
1507 200 : let terminal_prompt = self.terminal_prompt_text().to_vec();
1508 200 : let terminal_columns = self.terminal_columns as usize;
1509 200 : let target_layout =
1510 200 : layout_prompt(&text[..target], &terminal_prompt, &self.terminal_continuation, terminal_columns);
1511 200 : let cursor_layout = layout_prompt(
1512 200 : &text[..self.prompt.cursor_byte_offset()],
1513 200 : &terminal_prompt,
1514 200 : &self.terminal_continuation,
1515 200 : terminal_columns,
1516 : );
1517 200 : overlay.cursor_row = cursor_layout.rows - 1;
1518 200 : overlay.anchor_column = target_layout.column;
1519 200 : select_terminal_completion_hint(&mut overlay, text);
1520 200 : if !overlay.hint_only {
1521 174 : overlay.completion = Some(completion);
1522 174 : self.terminal_completion_revision += 1;
1523 177 : }
1524 200 : self.overlay = Some(overlay);
1525 200 : self.render_terminal_completion_overlay()
1526 200 : }
1527 :
1528 231 : fn refresh_terminal_completion_overlay(&mut self, catalog: &Catalog) -> Result<Vec<u8>, String> {
1529 231 : let (candidates, completion) = self.complete_prompt_packed(50, catalog)?;
1530 231 : if candidates.is_empty() {
1531 32 : self.overlay = None;
1532 32 : return Ok(Vec::new());
1533 199 : }
1534 199 : Ok(self.open_terminal_completion_overlay(candidates, completion))
1535 231 : }
1536 :
1537 : /// Render the inline hint of the selected candidate, the host shows the list
1538 216 : fn render_terminal_completion_overlay(&mut self) -> Vec<u8> {
1539 216 : let mut output = self.clear_terminal_completion_overlay();
1540 216 : if self.overlay.is_none() {
1541 0 : return output;
1542 216 : }
1543 216 : if !output.is_empty() {
1544 14 : output.extend(self.render_terminal_prompt());
1545 202 : }
1546 216 : output.extend(self.render_terminal_completion_hint());
1547 216 : output
1548 216 : }
1549 :
1550 406 : fn clear_terminal_completion_overlay(&mut self) -> Vec<u8> {
1551 406 : let Some(overlay) = self.overlay.as_mut() else {
1552 8 : return Vec::new();
1553 : };
1554 398 : let mut output = Vec::new();
1555 398 : if overlay.hint_suffix_width > 0 {
1556 163 : if overlay.hint_suffix_inserted {
1557 4 : append_cursor_move(&mut output, overlay.hint_suffix_width, Command::DeleteCharacter);
1558 159 : } else {
1559 159 : push(&mut output, vt100::ERASE_LINE_FROM_CURSOR);
1560 159 : }
1561 163 : overlay.hint_suffix_width = 0;
1562 163 : overlay.hint_suffix_inserted = false;
1563 235 : }
1564 398 : if overlay.hint_prefix_width > 0 {
1565 14 : append_cursor_move(
1566 14 : &mut output,
1567 14 : overlay.hint_current_width + overlay.hint_prefix_width,
1568 14 : Command::CursorBackward,
1569 14 : );
1570 14 : append_cursor_move(&mut output, overlay.hint_prefix_width, Command::DeleteCharacter);
1571 14 : append_cursor_move(&mut output, overlay.hint_current_width, Command::CursorForward);
1572 14 : overlay.hint_prefix_width = 0;
1573 14 : overlay.hint_current_width = 0;
1574 384 : }
1575 398 : output
1576 406 : }
1577 :
1578 219 : fn render_terminal_completion_hint(&mut self) -> Vec<u8> {
1579 219 : let cursor = self.prompt.cursor_byte_offset();
1580 219 : let prompt_text = self.prompt.text();
1581 219 : let text = prompt_text.as_bytes();
1582 219 : let Some(overlay) = self.overlay.as_mut() else {
1583 0 : return Vec::new();
1584 : };
1585 219 : let candidate = &overlay.candidates[overlay.selection];
1586 219 : let target_end = candidate.target_offset as usize + candidate.target_length as usize;
1587 219 : if target_end != cursor {
1588 0 : return Vec::new();
1589 219 : }
1590 219 : let current = &text[candidate.target_offset as usize..target_end];
1591 219 : let completion_text = candidate.completion_text.as_bytes();
1592 219 : let mut prefix: Vec<u8> = Vec::new();
1593 219 : let mut suffix: Vec<u8> = Vec::new();
1594 219 : if overlay.hint_only {
1595 29 : match find_bytes(completion_text, current) {
1596 29 : Some(completion) => {
1597 29 : prefix = completion_text[..completion].to_vec();
1598 29 : suffix = completion_text[completion + current.len()..].to_vec();
1599 29 : }
1600 0 : None => suffix = completion_text.to_vec(),
1601 : }
1602 190 : } else if !candidate.qualification_texts.is_empty() {
1603 15 : let variant = overlay.variant_selection.min(candidate.qualification_texts.len() - 1);
1604 15 : let qualified = candidate.qualification_texts[variant].as_bytes();
1605 15 : let completion = find_bytes(qualified, completion_text);
1606 15 : let typed = ci_find(completion_text, current);
1607 15 : if let (Some(completion), Some(typed)) = (completion, typed) {
1608 15 : prefix = qualified[..completion + typed].to_vec();
1609 15 : suffix = completion_text[typed + current.len()..].to_vec();
1610 15 : suffix.extend_from_slice(&qualified[completion + completion_text.len()..]);
1611 15 : }
1612 175 : } else if ci_eq(completion_text, current) && !candidate.continuation_text.is_empty() {
1613 4 : suffix = format!(" {}", candidate.continuation_text).into_bytes();
1614 171 : } else if ci_starts_with(completion_text, current) {
1615 168 : suffix = completion_text[current.len()..].to_vec();
1616 168 : }
1617 219 : if prefix.is_empty() && suffix.is_empty() && !candidate.continuation_text.is_empty() {
1618 0 : suffix = format!(" {}", candidate.continuation_text).into_bytes();
1619 219 : }
1620 219 : if prefix.is_empty() && suffix.is_empty() {
1621 29 : return Vec::new();
1622 190 : }
1623 : // Names can hold control characters, the hint shows them escaped
1624 190 : let prefix = escape_terminal_text(&prefix);
1625 190 : let suffix = escape_terminal_text(&suffix);
1626 190 : overlay.hint_prefix_width = display_width(&prefix);
1627 190 : overlay.hint_suffix_width = display_width(&suffix);
1628 190 : overlay.hint_current_width = display_width(current);
1629 190 : let mut output = Vec::new();
1630 190 : if !prefix.is_empty() {
1631 17 : append_cursor_move(&mut output, overlay.hint_current_width, Command::CursorBackward);
1632 17 : append_cursor_move(&mut output, overlay.hint_prefix_width, Command::InsertCharacter);
1633 17 : push(&mut output, vt100::FOREGROUND_BRIGHT_BLACK);
1634 17 : output.extend_from_slice(&prefix);
1635 17 : push(&mut output, vt100::RESET_ATTRIBUTES);
1636 17 : append_cursor_move(&mut output, overlay.hint_current_width, Command::CursorForward);
1637 173 : }
1638 190 : if !suffix.is_empty() {
1639 182 : push(&mut output, vt100::SAVE_CURSOR);
1640 182 : if target_end < text.len() {
1641 6 : append_cursor_move(&mut output, overlay.hint_suffix_width, Command::InsertCharacter);
1642 6 : overlay.hint_suffix_inserted = true;
1643 176 : }
1644 182 : push(&mut output, vt100::FOREGROUND_BRIGHT_BLACK);
1645 182 : output.extend_from_slice(&suffix);
1646 182 : push(&mut output, vt100::RESET_ATTRIBUTES);
1647 182 : push(&mut output, vt100::RESTORE_CURSOR);
1648 8 : }
1649 190 : output
1650 219 : }
1651 :
1652 18 : fn accept_terminal_completion(&mut self) -> ShellOperation {
1653 18 : let output = self.clear_terminal_completion_overlay();
1654 18 : let overlay = self.overlay.take().unwrap();
1655 18 : let variant_selection = overlay.variant_selection;
1656 18 : let candidate = overlay.candidates.into_iter().nth(overlay.selection).unwrap();
1657 18 : self.apply_terminal_candidate(candidate, variant_selection, output)
1658 18 : }
1659 :
1660 : /// Apply a candidate after the output so far. The list stays closed, like in the editor: only the hint of the next
1661 : /// step remains, a qualification or a keyword continuation.
1662 19 : fn apply_terminal_candidate(
1663 19 : &mut self,
1664 19 : mut candidate: CompletionCandidate,
1665 19 : variant_selection: usize,
1666 19 : mut output: Vec<u8>,
1667 19 : ) -> ShellOperation {
1668 19 : let continuation = candidate.continuation_text.clone();
1669 19 : let qualification = if candidate.qualification_texts.is_empty() {
1670 15 : String::new()
1671 : } else {
1672 4 : candidate.qualification_texts[variant_selection.min(candidate.qualification_texts.len() - 1)].clone()
1673 : };
1674 19 : let apply_qualification = self.auto_qualify_non_default_database_tables
1675 3 : && !candidate.completion_text.ends_with("()")
1676 3 : && is_non_default_three_part_name(qualification.as_bytes());
1677 19 : if apply_qualification && !qualification.is_empty() {
1678 2 : candidate.completion_text = qualification.clone();
1679 2 : candidate.completion_cursor_offset = qualification.len() as u32;
1680 2 : candidate.target_offset = candidate.qualification_target_offset;
1681 2 : candidate.target_length = candidate.qualification_target_length;
1682 17 : }
1683 19 : let snapshot = self.apply_completion(&candidate);
1684 19 : if snapshot.status != ShellStatus::Ok {
1685 0 : return ShellOperation::new(snapshot.status, snapshot.message);
1686 19 : }
1687 19 : self.prompt.script_mut().parse();
1688 19 : output.extend(self.render_terminal_prompt());
1689 19 : if !apply_qualification && !qualification.is_empty() && qualification != candidate.completion_text {
1690 2 : let cursor = self.prompt.cursor_byte_offset();
1691 2 : let prompt_text = self.prompt.text();
1692 2 : let completion_length = candidate.completion_text.len() as u32;
1693 2 : self.overlay = Some(TerminalCompletionOverlay {
1694 : hint_only: true,
1695 370 : cursor_row: prompt_text.as_bytes()[..cursor].iter().filter(|&&c| c == b'\n').count(),
1696 2 : candidates: vec![CompletionCandidate {
1697 2 : display_text: candidate.completion_text,
1698 2 : completion_text: qualification.clone(),
1699 2 : continuation_text: continuation,
1700 2 : completion_cursor_offset: qualification.len() as u32,
1701 2 : target_offset: candidate.qualification_target_offset,
1702 2 : target_length: completion_length,
1703 2 : qualification_target_offset: candidate.qualification_target_offset,
1704 2 : qualification_target_length: completion_length,
1705 2 : ..Default::default()
1706 2 : }],
1707 2 : ..Default::default()
1708 : });
1709 2 : output.extend(self.render_terminal_completion_hint());
1710 17 : } else if !continuation.is_empty() {
1711 1 : let cursor = self.prompt.cursor_byte_offset();
1712 1 : let prompt_text = self.prompt.text();
1713 1 : self.overlay = Some(TerminalCompletionOverlay {
1714 : hint_only: true,
1715 28 : cursor_row: prompt_text.as_bytes()[..cursor].iter().filter(|&&c| c == b'\n').count(),
1716 1 : candidates: vec![CompletionCandidate {
1717 1 : display_text: continuation.clone(),
1718 1 : completion_text: format!(" {}", continuation),
1719 1 : completion_cursor_offset: (continuation.len() + 1) as u32,
1720 1 : target_offset: cursor as u32,
1721 1 : target_length: 0,
1722 1 : qualification_target_offset: cursor as u32,
1723 1 : qualification_target_length: 0,
1724 1 : ..Default::default()
1725 1 : }],
1726 1 : ..Default::default()
1727 : });
1728 1 : output.extend(self.render_terminal_completion_hint());
1729 16 : }
1730 19 : ShellOperation::ok(output)
1731 19 : }
1732 :
1733 11 : fn move_terminal_completion(&mut self, direction: i32) -> ShellOperation {
1734 11 : let overlay = self.overlay.as_mut().unwrap();
1735 11 : let count = overlay.candidates.len();
1736 11 : if direction < 0 {
1737 4 : overlay.selection = if overlay.selection == 0 { count - 1 } else { overlay.selection - 1 };
1738 7 : } else {
1739 7 : overlay.selection = (overlay.selection + 1) % count;
1740 7 : }
1741 11 : overlay.variant_selection = 0;
1742 11 : ShellOperation::ok(self.render_terminal_completion_overlay())
1743 11 : }
1744 :
1745 5 : fn move_terminal_completion_variant(&mut self, direction: i32) -> ShellOperation {
1746 5 : let overlay = self.overlay.as_mut().unwrap();
1747 5 : let count = overlay.candidates[overlay.selection].qualification_texts.len();
1748 5 : if count < 2 {
1749 0 : return ShellOperation::ok(Vec::new());
1750 5 : }
1751 5 : if direction < 0 {
1752 : overlay.variant_selection =
1753 3 : if overlay.variant_selection == 0 { count - 1 } else { overlay.variant_selection - 1 };
1754 2 : } else {
1755 2 : overlay.variant_selection = (overlay.variant_selection + 1) % count;
1756 2 : }
1757 5 : ShellOperation::ok(self.render_terminal_completion_overlay())
1758 5 : }
1759 : }
1760 :
1761 : /// The first occurrence of a needle, 0 for an empty needle
1762 44 : fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
1763 44 : if needle.is_empty() {
1764 25 : return Some(0);
1765 19 : }
1766 164 : haystack.windows(needle.len()).position(|w| w == needle)
1767 44 : }
|