mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-26 00:02:34 +00:00
feat(prompt-editor): start input on its own row when the prompt leaves too little room (#767)
With a long prompt the inline editor started at the prompt's end column, leaving e.g. 24 of 143 columns for the first row of input. When fewer than max(20, cols/3) columns remain (and the prompt is at least as wide as what it left, so a new row actually gains room), the input overlay now starts at column 0 of the row below the prompt. The start position is decided by one pure fn, input_start(), and fed to everything that lays out the input: the overlay rows and overflow shift, click-to-index mapping, vertical caret movement, the rendered bar, the completion menu anchor and the IME candidate anchor.
This commit is contained in:
@@ -2193,7 +2193,15 @@ impl Element for TerminalElement {
|
||||
invert_cursor_cell(&mut buf, geom.cols, row, col, &colors);
|
||||
}
|
||||
|
||||
let cursor_bounds = cursor_cell.map(|(row, col)| geom.cell_rect(row, col, 1));
|
||||
// With the input bar up, the IME composes into the bar, which starts a
|
||||
// row below the prompt when the prompt left it too little room.
|
||||
let ime_cell = cursor.map(|c| {
|
||||
self.view
|
||||
.read(cx)
|
||||
.input_ime_cell(c.row, c.col, geom.cols)
|
||||
.unwrap_or((c.row, c.ime_col))
|
||||
});
|
||||
let cursor_bounds = ime_cell.map(|(row, col)| geom.cell_rect(row, col, 1));
|
||||
let focus_handle = self.view.read(cx).focus_handle.clone();
|
||||
window.handle_input(
|
||||
&focus_handle,
|
||||
|
||||
+172
-32
@@ -2933,9 +2933,10 @@ impl TerminalView {
|
||||
let cols = self.terminal.term.lock().columns().max(1);
|
||||
let chars: Vec<char> = self.cmd.text().chars().collect();
|
||||
let len = chars.len();
|
||||
let (positions, _r, _c) = input_char_positions(&chars, scol, cols);
|
||||
let start = input_start(scol, cols);
|
||||
let (positions, _r, _c) = input_char_positions(&chars, start, cols);
|
||||
let end_caret = if len == 0 {
|
||||
(0usize, scol)
|
||||
start
|
||||
} else {
|
||||
let (r, c, w) = positions[len - 1];
|
||||
if chars[len - 1] == '\n' {
|
||||
@@ -2950,11 +2951,15 @@ impl TerminalView {
|
||||
} else {
|
||||
end_caret
|
||||
};
|
||||
let mut max_row = positions.iter().map(|&(r, _, _)| r).max().unwrap_or(0);
|
||||
let mut max_row = positions
|
||||
.iter()
|
||||
.map(|&(r, _, _)| r)
|
||||
.max()
|
||||
.unwrap_or(start.0);
|
||||
if chars.last() == Some(&'\n') {
|
||||
max_row += 1;
|
||||
}
|
||||
if (down && cur_row >= max_row) || (!down && cur_row == 0) {
|
||||
if (down && cur_row >= max_row) || (!down && cur_row <= start.0) {
|
||||
self.editor_goal_col = None;
|
||||
return false;
|
||||
}
|
||||
@@ -4192,6 +4197,26 @@ impl TerminalView {
|
||||
(row >= 0).then_some((row as usize, col))
|
||||
}
|
||||
|
||||
/// Where the IME should compose when the input bar has moved below the
|
||||
/// prompt (see [`input_start`]): the start of the bar's first row. `None`
|
||||
/// when the bar is beside the prompt, or not up at all — the prompt's own
|
||||
/// cursor cell is right then. Takes the cursor as the frame snapshot saw
|
||||
/// it rather than locking the terminal again mid-paint.
|
||||
pub(super) fn input_ime_cell(
|
||||
&self,
|
||||
row: usize,
|
||||
col: usize,
|
||||
cols: usize,
|
||||
) -> Option<(usize, usize)> {
|
||||
if !self.input_active() || self.reverse_search.is_some() {
|
||||
return None;
|
||||
}
|
||||
match input_start(col, cols.max(1)) {
|
||||
(0, _) => None,
|
||||
(rows, col) => Some((row + rows, col)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn input_scroll_rows(&self) -> usize {
|
||||
if !self.input_active() || self.reverse_search.is_some() {
|
||||
return 0;
|
||||
@@ -4215,7 +4240,7 @@ impl TerminalView {
|
||||
&chars,
|
||||
self.cmd.cursor(),
|
||||
&self.marked_text,
|
||||
ccol,
|
||||
input_start(ccol, cols.max(1)),
|
||||
cols.max(1),
|
||||
);
|
||||
input_overflow_shift(crow, caret_vrow, visual_rows, rows)
|
||||
@@ -4231,7 +4256,14 @@ impl TerminalView {
|
||||
}
|
||||
let cols = self.terminal.term.lock().columns().max(1);
|
||||
let chars: Vec<char> = self.cmd.text().chars().collect();
|
||||
wrapped_click_index(&chars, scol, cols, col, row - srow, clamp)
|
||||
wrapped_click_index(
|
||||
&chars,
|
||||
input_start(scol, cols),
|
||||
cols,
|
||||
col,
|
||||
row - srow,
|
||||
clamp,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn editor_click(
|
||||
@@ -6568,8 +6600,13 @@ impl TerminalView {
|
||||
|
||||
let blank = move |w: gpui::Pixels| div().flex_none().w(w).h(lh);
|
||||
|
||||
let mut lines: Vec<Vec<gpui::AnyElement>> =
|
||||
vec![vec![blank(cell_w * (ccol as f32)).into_any_element()]];
|
||||
// The prompt's row, then the input's. Starting below the prompt leaves
|
||||
// that row empty — the prompt shows through it — and the input begins
|
||||
// at column 0 of the next.
|
||||
let cols = self.terminal.term.lock().columns().max(1);
|
||||
let (start_row, start_col) = input_start(ccol, cols);
|
||||
let mut lines: Vec<Vec<gpui::AnyElement>> = (0..start_row).map(|_| Vec::new()).collect();
|
||||
lines.push(vec![blank(cell_w * (start_col as f32)).into_any_element()]);
|
||||
|
||||
let is_multiline = chars.contains(&'\n');
|
||||
|
||||
@@ -6686,13 +6723,16 @@ impl TerminalView {
|
||||
return None;
|
||||
}
|
||||
let (srow, scol) = self.cursor_cell()?;
|
||||
let srow = srow.saturating_sub(self.input_scroll_rows());
|
||||
|
||||
const MAX_ROWS: usize = 10;
|
||||
let (total_rows, total_cols) = {
|
||||
let term = self.terminal.term.lock();
|
||||
(term.screen_lines(), term.columns())
|
||||
};
|
||||
// Anchored to where the input starts, which is a row below the prompt
|
||||
// when the prompt left it too little room.
|
||||
let (start_row, scol) = input_start(scol, total_cols.max(1));
|
||||
let srow = (srow + start_row).saturating_sub(self.input_scroll_rows());
|
||||
// How wide the menu ends up, decided before the rows so their
|
||||
// descriptions can be elided against it. A menu wider than the pane
|
||||
// has its right-hand column clipped by the pane, and the clip takes
|
||||
@@ -7869,8 +7909,41 @@ fn menu_layout(
|
||||
(place_above, visible, first)
|
||||
}
|
||||
|
||||
/// The fewest columns the input is given beside the prompt, however wide the
|
||||
/// pane. Below this nearly any command with an argument or two wraps on its
|
||||
/// first row, and a row that short is harder to read than a row lower down.
|
||||
const INPUT_MIN_BESIDE_PROMPT: usize = 20;
|
||||
|
||||
/// Where the input bar starts, as `(row, column)` with the prompt's own row as
|
||||
/// row 0: right after the prompt, or — when the prompt has left too little of
|
||||
/// its row — at column 0 of the row below, with the whole width to itself
|
||||
/// (#767).
|
||||
///
|
||||
/// "Too little" is under a third of the pane, and never under
|
||||
/// [`INPUT_MIN_BESIDE_PROMPT`]. A third keeps an ordinary prompt on its own
|
||||
/// line in any pane you would type in (`user@host ~/src/app % ` is about 25
|
||||
/// columns; an 80-column pane still leaves 55), and only moves the input when
|
||||
/// a deep path or a busy theme has eaten most of the row — the report behind
|
||||
/// this had a 119-column prompt in a 143-column pane, 24 columns left. The
|
||||
/// floor covers narrow panes, where a third is a handful of columns.
|
||||
///
|
||||
/// The move also has to gain something: in a pane barely wider than the
|
||||
/// floor, a short prompt leaves under 20 columns and a new row would hardly
|
||||
/// give more. So it only happens when the prompt is at least as wide as what
|
||||
/// it left — the new row at least doubles the room.
|
||||
fn input_start(scol: usize, cols: usize) -> (usize, usize) {
|
||||
let left = cols.saturating_sub(scol);
|
||||
let floor = INPUT_MIN_BESIDE_PROMPT.max(cols / 3);
|
||||
if left < floor && scol >= left {
|
||||
(1, 0)
|
||||
} else {
|
||||
(0, scol)
|
||||
}
|
||||
}
|
||||
|
||||
/// Where each character of the input bar lands: `(row, column, width)`, one
|
||||
/// entry per character, plus the row and column the text ends on.
|
||||
/// entry per character, plus the row and column the text ends on. `start` is
|
||||
/// where the first one goes, from [`input_start`].
|
||||
///
|
||||
/// Walks the same cells the bar draws rather than re-deriving widths per
|
||||
/// character — an emoji presentation sequence is two columns and a stranded
|
||||
@@ -7882,12 +7955,11 @@ fn menu_layout(
|
||||
/// caret takes after the cell, which is where a caret sitting on one belongs.
|
||||
fn input_char_positions(
|
||||
chars: &[char],
|
||||
scol: usize,
|
||||
start: (usize, usize),
|
||||
cols: usize,
|
||||
) -> (Vec<(usize, usize, usize)>, usize, usize) {
|
||||
let mut positions: Vec<(usize, usize, usize)> = Vec::with_capacity(chars.len());
|
||||
let mut r = 0usize;
|
||||
let mut c = scol;
|
||||
let (mut r, mut c) = start;
|
||||
for cell in input_cells(chars) {
|
||||
if chars[cell.start] == '\n' {
|
||||
positions.push((r, c, 0));
|
||||
@@ -7912,7 +7984,7 @@ fn input_overlay_rows(
|
||||
chars: &[char],
|
||||
cursor: usize,
|
||||
marked: &str,
|
||||
scol: usize,
|
||||
start: (usize, usize),
|
||||
cols: usize,
|
||||
) -> (usize, usize) {
|
||||
let mut merged: Vec<char> = Vec::with_capacity(chars.len() + marked.len());
|
||||
@@ -7920,7 +7992,7 @@ fn input_overlay_rows(
|
||||
merged.extend_from_slice(&chars[..cursor]);
|
||||
merged.extend(marked.chars());
|
||||
merged.extend_from_slice(&chars[cursor..]);
|
||||
let (positions, r, c) = input_char_positions(&merged, scol, cols);
|
||||
let (positions, r, c) = input_char_positions(&merged, start, cols);
|
||||
let end_row = if cursor >= chars.len() && marked.is_empty() && c >= cols {
|
||||
r + 1
|
||||
} else {
|
||||
@@ -7938,14 +8010,14 @@ fn input_overflow_shift(crow: usize, caret_vrow: usize, visual_rows: usize, rows
|
||||
|
||||
fn wrapped_click_index(
|
||||
chars: &[char],
|
||||
scol: usize,
|
||||
start: (usize, usize),
|
||||
cols: usize,
|
||||
col: usize,
|
||||
target: usize,
|
||||
clamp: bool,
|
||||
) -> Option<usize> {
|
||||
let len = chars.len();
|
||||
let (positions, r, c) = input_char_positions(chars, scol, cols);
|
||||
let (positions, r, c) = input_char_positions(chars, start, cols);
|
||||
let end_row = if c >= cols { r + 1 } else { r };
|
||||
if target > end_row {
|
||||
return clamp.then_some(len);
|
||||
@@ -8169,9 +8241,9 @@ mod tests {
|
||||
use super::{
|
||||
description_budget, drag_scroll_step, elide, encode_mouse, expand_file_command_template,
|
||||
fallback_chain, fig_icon_emoji, fig_icon_glyph, focus_report_bytes, highlight_runs,
|
||||
input_cells, input_char_positions, input_overflow_shift, input_overlay_rows, menu_layout,
|
||||
paste_bytes, select_end_copy, should_show_context_menu, smooth_scroll_step, submit_bytes,
|
||||
trim_trailing_spaces, wheel_route, wrapped_click_index,
|
||||
input_cells, input_char_positions, input_overflow_shift, input_overlay_rows, input_start,
|
||||
menu_layout, paste_bytes, select_end_copy, should_show_context_menu, smooth_scroll_step,
|
||||
submit_bytes, trim_trailing_spaces, wheel_route, wrapped_click_index,
|
||||
};
|
||||
use alacritty_terminal::term::TermMode;
|
||||
use gpui::{ClipboardEntry, ClipboardItem, ExternalPaths, Modifiers};
|
||||
@@ -9540,7 +9612,7 @@ mod tests {
|
||||
#[test]
|
||||
fn input_char_positions_reserve_two_columns_for_wide_chars() {
|
||||
let chars: Vec<char> = "a🀄b".chars().collect();
|
||||
let (positions, _, _) = input_char_positions(&chars, 0, 80);
|
||||
let (positions, _, _) = input_char_positions(&chars, (0, 0), 80);
|
||||
assert_eq!(positions, vec![(0, 0, 1), (0, 1, 2), (0, 3, 1)]);
|
||||
}
|
||||
|
||||
@@ -9557,7 +9629,7 @@ mod tests {
|
||||
"a\nb",
|
||||
] {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let (positions, _, _) = input_char_positions(&chars, 0, 80);
|
||||
let (positions, _, _) = input_char_positions(&chars, (0, 0), 80);
|
||||
assert_eq!(positions.len(), chars.len(), "{text:?}");
|
||||
for cell in input_cells(&chars) {
|
||||
let drawn = if chars[cell.start] == '\n' {
|
||||
@@ -9578,7 +9650,7 @@ mod tests {
|
||||
#[test]
|
||||
fn input_char_positions_reserve_two_columns_for_an_emoji_presentation_sequence() {
|
||||
let chars: Vec<char> = "\u{2764}\u{FE0F}X".chars().collect();
|
||||
let (positions, _, _) = input_char_positions(&chars, 0, 80);
|
||||
let (positions, _, _) = input_char_positions(&chars, (0, 0), 80);
|
||||
assert_eq!(positions, vec![(0, 0, 2), (0, 2, 0), (0, 2, 1)]);
|
||||
assert_eq!(click("\u{2764}\u{FE0F}X", 0, 80, 0, 0), Some(0));
|
||||
assert_eq!(click("\u{2764}\u{FE0F}X", 0, 80, 1, 0), Some(0));
|
||||
@@ -9590,7 +9662,7 @@ mod tests {
|
||||
#[test]
|
||||
fn input_char_positions_give_a_stranded_combining_mark_a_column() {
|
||||
let chars: Vec<char> = "\u{0301}ab".chars().collect();
|
||||
let (positions, _, _) = input_char_positions(&chars, 0, 80);
|
||||
let (positions, _, _) = input_char_positions(&chars, (0, 0), 80);
|
||||
assert_eq!(positions, vec![(0, 0, 1), (0, 1, 1), (0, 2, 1)]);
|
||||
}
|
||||
|
||||
@@ -9599,7 +9671,7 @@ mod tests {
|
||||
#[test]
|
||||
fn input_char_positions_wrap_a_cell_without_splitting_it() {
|
||||
let chars: Vec<char> = "abc\u{2764}\u{FE0F}".chars().collect();
|
||||
let (positions, r, c) = input_char_positions(&chars, 0, 4);
|
||||
let (positions, r, c) = input_char_positions(&chars, (0, 0), 4);
|
||||
assert_eq!(positions[3], (1, 0, 2));
|
||||
assert_eq!((r, c), (1, 2));
|
||||
}
|
||||
@@ -9688,7 +9760,7 @@ mod tests {
|
||||
|
||||
fn click(text: &str, scol: usize, cols: usize, col: usize, row: usize) -> Option<usize> {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
wrapped_click_index(&chars, scol, cols, col, row, false)
|
||||
wrapped_click_index(&chars, (0, scol), cols, col, row, false)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -9720,10 +9792,10 @@ mod tests {
|
||||
#[test]
|
||||
fn wrapped_click_index_rows_past_the_input_need_clamp() {
|
||||
let chars: Vec<char> = "ls".chars().collect();
|
||||
assert_eq!(wrapped_click_index(&chars, 4, 80, 3, 2, false), None);
|
||||
assert_eq!(wrapped_click_index(&chars, 4, 80, 3, 2, true), Some(2));
|
||||
assert_eq!(wrapped_click_index(&[], 4, 80, 30, 0, false), Some(0));
|
||||
assert_eq!(wrapped_click_index(&chars, 4, 80, 3, 1, false), None);
|
||||
assert_eq!(wrapped_click_index(&chars, (0, 4), 80, 3, 2, false), None);
|
||||
assert_eq!(wrapped_click_index(&chars, (0, 4), 80, 3, 2, true), Some(2));
|
||||
assert_eq!(wrapped_click_index(&[], (0, 4), 80, 30, 0, false), Some(0));
|
||||
assert_eq!(wrapped_click_index(&chars, (0, 4), 80, 3, 1, false), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -9731,7 +9803,7 @@ mod tests {
|
||||
assert_eq!(click("abcdef", 4, 10, 0, 1), Some(6));
|
||||
assert_eq!(click("abcdef", 4, 10, 7, 1), Some(6));
|
||||
let chars: Vec<char> = "abcdef".chars().collect();
|
||||
assert_eq!(wrapped_click_index(&chars, 4, 10, 0, 2, false), None);
|
||||
assert_eq!(wrapped_click_index(&chars, (0, 4), 10, 0, 2, false), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -9749,7 +9821,7 @@ mod tests {
|
||||
fn input_overlay_rows_counts_wraps_slot_marked_and_newlines() {
|
||||
let rows = |text: &str, cursor: usize, marked: &str, scol: usize, cols: usize| {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
input_overlay_rows(&chars, cursor, marked, scol, cols)
|
||||
input_overlay_rows(&chars, cursor, marked, (0, scol), cols)
|
||||
};
|
||||
assert_eq!(rows("", 0, "", 3, 8), (1, 0));
|
||||
assert_eq!(rows("aaaaaaaaaa", 10, "", 6, 8), (3, 2));
|
||||
@@ -9758,6 +9830,74 @@ mod tests {
|
||||
assert_eq!(rows("ab", 1, "漢", 6, 8), (2, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_input_stays_beside_an_ordinary_prompt() {
|
||||
// `user@host ~/src/app % ` in an 80-column pane: 55 columns left.
|
||||
assert_eq!(input_start(25, 80), (0, 25));
|
||||
// No prompt at all, and a bare `$ `.
|
||||
assert_eq!(input_start(0, 80), (0, 0));
|
||||
assert_eq!(input_start(2, 143), (0, 2));
|
||||
// Exactly a third left is enough.
|
||||
assert_eq!(input_start(60, 90), (0, 60));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_prompt_that_eats_the_row_pushes_the_input_below_it() {
|
||||
// #767: a 119-column prompt in a 143-column pane left 24.
|
||||
assert_eq!(input_start(119, 143), (1, 0));
|
||||
// One column short of a third.
|
||||
assert_eq!(input_start(61, 90), (1, 0));
|
||||
// A narrow pane falls back to the 20-column floor, not a third of it.
|
||||
assert_eq!(input_start(25, 40), (1, 0));
|
||||
assert_eq!(input_start(20, 40), (0, 20));
|
||||
// A cursor parked in the last column, or past it, has no room at all.
|
||||
assert_eq!(input_start(79, 80), (1, 0));
|
||||
assert_eq!(input_start(80, 80), (1, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_row_has_to_gain_something() {
|
||||
// A 20-column pane: a `$ ` prompt leaves 18, under the floor, but a
|
||||
// fresh row would give only two more.
|
||||
assert_eq!(input_start(2, 20), (0, 2));
|
||||
// Half the row is the break-even point.
|
||||
assert_eq!(input_start(9, 20), (0, 9));
|
||||
assert_eq!(input_start(10, 20), (1, 0));
|
||||
}
|
||||
|
||||
/// Everything that reads the layout — the overlay's height, the caret's
|
||||
/// row, clicks — sees the input a row down, with the prompt's row empty.
|
||||
#[test]
|
||||
fn input_below_the_prompt_is_laid_out_from_the_next_row() {
|
||||
let start = input_start(119, 143);
|
||||
let chars: Vec<char> = "git status".chars().collect();
|
||||
let (positions, r, c) = input_char_positions(&chars, start, 143);
|
||||
assert_eq!(positions[0], (1, 0, 1));
|
||||
assert_eq!((r, c), (1, 10));
|
||||
// Two rows tall — the prompt's and the input's — with the caret on
|
||||
// the second, so an overflow shift keeps the input on screen.
|
||||
assert_eq!(input_overlay_rows(&chars, 10, "", start, 143), (2, 1));
|
||||
assert_eq!(input_overlay_rows(&[], 0, "", start, 143), (2, 1));
|
||||
// Wrapping uses the whole width of the new row.
|
||||
let long: Vec<char> = "a".repeat(150).chars().collect();
|
||||
assert_eq!(input_overlay_rows(&long, 150, "", start, 143), (3, 2));
|
||||
// A click on the prompt's row lands before the first character; on
|
||||
// the input row it hits the character under it.
|
||||
assert_eq!(
|
||||
wrapped_click_index(&chars, start, 143, 130, 0, false),
|
||||
Some(0)
|
||||
);
|
||||
assert_eq!(
|
||||
wrapped_click_index(&chars, start, 143, 4, 1, false),
|
||||
Some(4)
|
||||
);
|
||||
assert_eq!(
|
||||
wrapped_click_index(&chars, start, 143, 60, 1, false),
|
||||
Some(10)
|
||||
);
|
||||
assert_eq!(wrapped_click_index(&chars, start, 143, 0, 2, false), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_overflow_shift_keeps_the_tail_and_caret_visible() {
|
||||
assert_eq!(input_overflow_shift(5, 2, 3, 22), 0);
|
||||
|
||||
Reference in New Issue
Block a user