LCOV - code coverage report
Current view: top level - dashql-core-shell/src - session.rs Coverage Total Hit
Test: DashQL 6e74948 Lines: 97.7 % 1481 1447
Test Date: 2026-11-06 17:46:50

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

Generated by: LCOV version 2.0-1