LCOV - code coverage report
Current view: top level - dashql-core-shell/src - session.rs Coverage Total Hit
Test: DashQL d805a4b Lines: 97.9 % 1402 1372
Test Date: 2026-11-09 20:22:13

            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(&quote_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 : }
        

Generated by: LCOV version 2.0-1