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