mirror of
https://github.com/rust-kotlin/ashell.git
synced 2026-09-22 00:00:59 +00:00
fix(app): resolve connection export modal and terminal history glitches (#115)
- Fix CSV connection export confirmation dialog not opening within settings page - Remove redundant connect button on connection rows in sidebar edit mode - Enhance terminal syntax highlighting with background color, inverse, dim, and CJK wide char support - Improve command history tracking accuracy using buffer-relative coordinates and residue filtering
This commit is contained in:
@@ -632,7 +632,8 @@ impl Ashell {
|
||||
}
|
||||
|
||||
pub(crate) fn export_connections(&mut self, window: &mut gpui::Window, cx: &mut Context<Self>) {
|
||||
if self.active_dialog.is_some() {
|
||||
let parent_dialog = self.active_dialog;
|
||||
if parent_dialog.is_some() && parent_dialog != Some(DialogKind::Settings) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -695,7 +696,7 @@ impl Ashell {
|
||||
let sessions = sessions_for_export.clone();
|
||||
view_for_ok.update(cx, |this, cx| {
|
||||
if this.active_dialog == Some(DialogKind::ConnectionExport) {
|
||||
this.active_dialog = None;
|
||||
this.active_dialog = parent_dialog;
|
||||
}
|
||||
this.start_connection_csv_export(sessions, window, cx);
|
||||
cx.notify();
|
||||
@@ -707,7 +708,7 @@ impl Ashell {
|
||||
move |_, _, cx| {
|
||||
view.update(cx, |this, cx| {
|
||||
if this.active_dialog == Some(DialogKind::ConnectionExport) {
|
||||
this.active_dialog = None;
|
||||
this.active_dialog = parent_dialog;
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
+1
-30
@@ -3336,7 +3336,6 @@ impl Ashell {
|
||||
active_session_id: Option<&str>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> gpui::AnyElement {
|
||||
let connect_id = session.id.clone();
|
||||
let edit_id = session.id.clone();
|
||||
let delete_id = session.id.clone();
|
||||
let management_mode = self.connection_management_mode;
|
||||
@@ -3347,8 +3346,6 @@ impl Ashell {
|
||||
let selection_id = session.id.clone();
|
||||
let row_selection_id = selection_id.clone();
|
||||
let row_id = ElementId::Name(format!("saved-connect-{}", session.id).into());
|
||||
let connect_button_id =
|
||||
ElementId::Name(format!("connect-saved-session-{}", session.id).into());
|
||||
|
||||
div()
|
||||
.id(row_id)
|
||||
@@ -3477,33 +3474,7 @@ impl Ashell {
|
||||
.text_size(ui_rems(0.75))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(detail),
|
||||
)
|
||||
.when(management_mode, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.w(px(24.))
|
||||
.ml_1()
|
||||
.flex_none()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_mouse_down(MouseButton::Right, |_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.child(
|
||||
pointer_button(connect_button_id)
|
||||
.ghost()
|
||||
.small()
|
||||
.icon(IconName::ExternalLink)
|
||||
.tooltip(t!("connect").to_string())
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.connect_saved_session(connect_id.clone(), window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
+129
-40
@@ -12,6 +12,7 @@ use gpui_component::ActiveTheme as _;
|
||||
|
||||
use crate::Ashell;
|
||||
use crate::terminal::custom_blocks::{is_custom_block_supported, paint_custom_block};
|
||||
use crate::terminal::highlight::HighlightStyle;
|
||||
use crate::terminal::{RenderSnapshot, ViewportSelection};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -338,21 +339,16 @@ impl TerminalElement {
|
||||
}
|
||||
}
|
||||
|
||||
fn cell_run_style(&self, cell: &alacritty_terminal::term::cell::Cell, cx: &App) -> TextRun {
|
||||
let mut fg = color_to_hsla(cell.fg, true, cx);
|
||||
let mut bg = color_to_hsla(cell.bg, false, cx);
|
||||
if cell.flags.contains(Flags::INVERSE) {
|
||||
std::mem::swap(&mut fg, &mut bg);
|
||||
}
|
||||
if cell.flags.contains(Flags::DIM) {
|
||||
fg.a *= 0.7;
|
||||
}
|
||||
|
||||
fn cell_run_style(
|
||||
&self,
|
||||
cell: &alacritty_terminal::term::cell::Cell,
|
||||
foreground: Hsla,
|
||||
) -> TextRun {
|
||||
let underline = cell
|
||||
.flags
|
||||
.intersects(Flags::ALL_UNDERLINES)
|
||||
.then(|| UnderlineStyle {
|
||||
color: Some(fg),
|
||||
color: Some(foreground),
|
||||
thickness: px(1.0),
|
||||
wavy: cell.flags.contains(Flags::UNDERCURL),
|
||||
});
|
||||
@@ -360,7 +356,7 @@ impl TerminalElement {
|
||||
.flags
|
||||
.contains(Flags::STRIKEOUT)
|
||||
.then(|| StrikethroughStyle {
|
||||
color: Some(fg),
|
||||
color: Some(foreground),
|
||||
thickness: px(1.0),
|
||||
});
|
||||
|
||||
@@ -377,7 +373,7 @@ impl TerminalElement {
|
||||
|
||||
TextRun {
|
||||
len: cell.c.len_utf8(),
|
||||
color: fg,
|
||||
color: foreground,
|
||||
background_color: None,
|
||||
font: Font {
|
||||
family: self.font_family.clone(),
|
||||
@@ -408,12 +404,6 @@ impl TerminalElement {
|
||||
let mut underlines = Vec::new();
|
||||
let mut current_run: Option<BatchedTextRun> = None;
|
||||
|
||||
// Retrieve cached keyword highlights and merge with search highlights
|
||||
let mut highlights = self.snapshot.highlights.clone();
|
||||
if let Some(sm) = self.search_highlights.as_ref() {
|
||||
highlights.extend(sm.iter().map(|(k, v)| (*k, *v)));
|
||||
}
|
||||
|
||||
for render_cell in &self.snapshot.cells {
|
||||
let cell = &render_cell.cell;
|
||||
if cell.flags.intersects(
|
||||
@@ -425,24 +415,54 @@ impl TerminalElement {
|
||||
let selected = self.snapshot.selection.is_some_and(|selection| {
|
||||
selection_contains(selection, render_cell.row, render_cell.col)
|
||||
});
|
||||
let bg = color_to_hsla(cell.bg, false, cx);
|
||||
if selected || !is_default_bg(cell.bg) || cell.flags.contains(Flags::INVERSE) {
|
||||
let cell_position = (render_cell.row, render_cell.col);
|
||||
let keyword_highlight = self.snapshot.highlights.get(&cell_position).copied();
|
||||
let search_highlight = self
|
||||
.search_highlights
|
||||
.as_ref()
|
||||
.and_then(|highlights| highlights.get(&cell_position))
|
||||
.copied();
|
||||
let (foreground, background) = resolve_cell_colors(
|
||||
color_to_hsla(cell.fg, true, cx),
|
||||
color_to_hsla(cell.bg, false, cx),
|
||||
keyword_highlight,
|
||||
cell.flags,
|
||||
);
|
||||
let cell_span = if cell.flags.contains(Flags::WIDE_CHAR) {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
if selected {
|
||||
rects.push(LayoutRect {
|
||||
row: render_cell.row,
|
||||
col: render_cell.col,
|
||||
cells: if cell.flags.contains(Flags::WIDE_CHAR) {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
},
|
||||
color: if selected {
|
||||
cx.theme().selection
|
||||
} else if cell.flags.contains(Flags::INVERSE) {
|
||||
color_to_hsla(cell.fg, true, cx)
|
||||
} else {
|
||||
bg
|
||||
},
|
||||
cells: cell_span,
|
||||
color: cx.theme().selection,
|
||||
});
|
||||
} else {
|
||||
let has_highlight_background =
|
||||
keyword_highlight.is_some_and(|style| style.background.is_some());
|
||||
if !is_default_bg(cell.bg)
|
||||
|| cell.flags.contains(Flags::INVERSE)
|
||||
|| has_highlight_background
|
||||
{
|
||||
rects.push(LayoutRect {
|
||||
row: render_cell.row,
|
||||
col: render_cell.col,
|
||||
cells: cell_span,
|
||||
color: background,
|
||||
});
|
||||
}
|
||||
if let Some(search_background) = search_highlight {
|
||||
rects.push(LayoutRect {
|
||||
row: render_cell.row,
|
||||
col: render_cell.col,
|
||||
cells: cell_span,
|
||||
color: search_background,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if is_blank(cell) {
|
||||
@@ -452,12 +472,7 @@ impl TerminalElement {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut style = self.cell_run_style(cell, cx);
|
||||
|
||||
// Apply keyword highlight color if this cell was matched.
|
||||
if let Some(&hl_color) = highlights.get(&(render_cell.row, render_cell.col)) {
|
||||
style.color = hl_color;
|
||||
}
|
||||
let style = self.cell_run_style(cell, foreground);
|
||||
|
||||
// Apply hover underline if mouse is hovering over this URL
|
||||
if let Some(hu) = &hovered_url {
|
||||
@@ -871,6 +886,31 @@ fn selection_contains(selection: ViewportSelection, row: i32, col: i32) -> bool
|
||||
after_start && before_end
|
||||
}
|
||||
|
||||
fn resolve_cell_colors(
|
||||
mut foreground: Hsla,
|
||||
mut background: Hsla,
|
||||
highlight: Option<HighlightStyle>,
|
||||
flags: Flags,
|
||||
) -> (Hsla, Hsla) {
|
||||
if let Some(highlight) = highlight {
|
||||
if let Some(highlight_foreground) = highlight.foreground {
|
||||
foreground = highlight_foreground;
|
||||
}
|
||||
if let Some(highlight_background) = highlight.background {
|
||||
background = highlight_background;
|
||||
}
|
||||
}
|
||||
|
||||
if flags.contains(Flags::INVERSE) {
|
||||
std::mem::swap(&mut foreground, &mut background);
|
||||
}
|
||||
if flags.contains(Flags::DIM) {
|
||||
foreground.a *= 0.7;
|
||||
}
|
||||
|
||||
(foreground, background)
|
||||
}
|
||||
|
||||
fn is_blank(cell: &alacritty_terminal::term::cell::Cell) -> bool {
|
||||
cell.c == ' '
|
||||
&& cell.zerowidth().is_none()
|
||||
@@ -965,7 +1005,14 @@ fn named_color(named: NamedColor, _foreground: bool, cx: &App) -> Hsla {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::should_render_cursor;
|
||||
use super::{resolve_cell_colors, should_render_cursor};
|
||||
use crate::terminal::highlight::HighlightStyle;
|
||||
use alacritty_terminal::term::cell::Flags;
|
||||
use gpui::Hsla;
|
||||
|
||||
fn color(h: f32, s: f32, l: f32) -> Hsla {
|
||||
Hsla { h, s, l, a: 1.0 }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_cursor_only_for_focused_pane_in_focused_terminal_window() {
|
||||
@@ -974,4 +1021,46 @@ mod tests {
|
||||
assert!(!should_render_cursor(true, false, true));
|
||||
assert!(!should_render_cursor(true, true, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn applies_highlight_channels_before_inverse_terminal_style() {
|
||||
let original_foreground = color(0.0, 0.0, 0.8);
|
||||
let original_background = color(0.0, 0.0, 0.1);
|
||||
let highlight_foreground = color(0.1, 0.8, 0.5);
|
||||
let highlight_background = color(0.6, 0.8, 0.4);
|
||||
|
||||
let (foreground, background) = resolve_cell_colors(
|
||||
original_foreground,
|
||||
original_background,
|
||||
Some(HighlightStyle {
|
||||
foreground: Some(highlight_foreground),
|
||||
background: Some(highlight_background),
|
||||
}),
|
||||
Flags::INVERSE,
|
||||
);
|
||||
|
||||
assert_eq!(foreground, highlight_background);
|
||||
assert_eq!(background, highlight_foreground);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn foreground_highlight_preserves_background_and_dim_style() {
|
||||
let original_foreground = color(0.0, 0.0, 0.8);
|
||||
let original_background = color(0.0, 0.0, 0.1);
|
||||
let highlight_foreground = color(0.1, 0.8, 0.5);
|
||||
|
||||
let (foreground, background) = resolve_cell_colors(
|
||||
original_foreground,
|
||||
original_background,
|
||||
Some(HighlightStyle {
|
||||
foreground: Some(highlight_foreground),
|
||||
background: None,
|
||||
}),
|
||||
Flags::DIM,
|
||||
);
|
||||
|
||||
assert_eq!(foreground.alpha(1.0), highlight_foreground);
|
||||
assert!((foreground.a - 0.7).abs() < f32::EPSILON);
|
||||
assert_eq!(background, original_background);
|
||||
}
|
||||
}
|
||||
|
||||
+76
-63
@@ -1,41 +1,19 @@
|
||||
use crate::terminal::RenderCell;
|
||||
use gpui::Hsla;
|
||||
use gpui::{Hsla, Rgba};
|
||||
use std::collections::HashMap;
|
||||
|
||||
trait HslaExt {
|
||||
fn into_rgba_like(self, r: u8, g: u8, b: u8) -> Self;
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
pub struct HighlightStyle {
|
||||
pub foreground: Option<Hsla>,
|
||||
pub background: Option<Hsla>,
|
||||
}
|
||||
|
||||
impl HslaExt for Hsla {
|
||||
fn into_rgba_like(self, r: u8, g: u8, b: u8) -> Self {
|
||||
let rf = r as f32 / 255.0;
|
||||
let gf = g as f32 / 255.0;
|
||||
let bf = b as f32 / 255.0;
|
||||
let max = rf.max(gf).max(bf);
|
||||
let min = rf.min(gf).min(bf);
|
||||
let l = (max + min) / 2.0;
|
||||
if max == min {
|
||||
return Hsla {
|
||||
h: 0.0,
|
||||
s: 0.0,
|
||||
l,
|
||||
a: 1.0,
|
||||
};
|
||||
impl HighlightStyle {
|
||||
fn from_foreground(color: Hsla) -> Self {
|
||||
Self {
|
||||
foreground: Some(color),
|
||||
background: None,
|
||||
}
|
||||
let d = max - min;
|
||||
let s = if l > 0.5 {
|
||||
d / (2.0 - max - min)
|
||||
} else {
|
||||
d / (max + min)
|
||||
};
|
||||
let h = if max == rf {
|
||||
((gf - bf) / d + if gf < bf { 6.0 } else { 0.0 }) / 6.0
|
||||
} else if max == gf {
|
||||
((bf - rf) / d + 2.0) / 6.0
|
||||
} else {
|
||||
((rf - gf) / d + 4.0) / 6.0
|
||||
};
|
||||
Hsla { h, s, l, a: 1.0 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,13 +75,12 @@ struct HighlightColors {
|
||||
}
|
||||
|
||||
fn hsla(r: u8, g: u8, b: u8) -> Hsla {
|
||||
Hsla {
|
||||
h: 0.0,
|
||||
s: 0.0,
|
||||
l: 0.0,
|
||||
Hsla::from(Rgba {
|
||||
r: r as f32 / 255.0,
|
||||
g: g as f32 / 255.0,
|
||||
b: b as f32 / 255.0,
|
||||
a: 1.0,
|
||||
}
|
||||
.into_rgba_like(r, g, b)
|
||||
})
|
||||
}
|
||||
|
||||
fn highlight_colors() -> HighlightColors {
|
||||
@@ -168,7 +145,7 @@ fn highlight_colors() -> HighlightColors {
|
||||
struct KeywordMatch {
|
||||
start_col: i32,
|
||||
end_col: i32,
|
||||
color: Hsla,
|
||||
style: HighlightStyle,
|
||||
priority: usize,
|
||||
}
|
||||
|
||||
@@ -184,7 +161,7 @@ impl KeywordMatch {
|
||||
|
||||
#[derive(Default)]
|
||||
struct KeywordHighlights {
|
||||
colors: HashMap<(i32, i32), Hsla>,
|
||||
styles: HashMap<(i32, i32), HighlightStyle>,
|
||||
pending: Vec<KeywordMatch>,
|
||||
next_priority: usize,
|
||||
}
|
||||
@@ -214,7 +191,7 @@ impl KeywordHighlights {
|
||||
|
||||
for candidate in selected {
|
||||
for col in candidate.start_col..=candidate.end_col {
|
||||
self.colors.insert((row_i32, col), candidate.color);
|
||||
self.styles.insert((row_i32, col), candidate.style);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,7 +239,7 @@ fn highlight_keywords(
|
||||
map.pending.push(KeywordMatch {
|
||||
start_col,
|
||||
end_col,
|
||||
color,
|
||||
style: HighlightStyle::from_foreground(color),
|
||||
priority,
|
||||
});
|
||||
start = abs + kw.len();
|
||||
@@ -276,7 +253,7 @@ fn highlight_keywords(
|
||||
/// Highlight HTTP status codes (200, 301, 404, 500, etc.)
|
||||
/// Only matches specific common HTTP codes, not all 3-digit numbers.
|
||||
fn highlight_http_codes(
|
||||
map: &mut HashMap<(i32, i32), Hsla>,
|
||||
map: &mut HashMap<(i32, i32), HighlightStyle>,
|
||||
text: &str,
|
||||
byte_to_col: &[i32],
|
||||
row_i32: i32,
|
||||
@@ -352,12 +329,13 @@ fn highlight_http_codes(
|
||||
let start_col = byte_to_col[i];
|
||||
let end_col = byte_to_col[(i + 2).min(byte_to_col.len() - 1)];
|
||||
for c in start_col..=end_col {
|
||||
map.entry((row_i32, c)).or_insert(color);
|
||||
map.entry((row_i32, c))
|
||||
.or_insert_with(|| HighlightStyle::from_foreground(color));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn highlight_cells(cells: &[RenderCell], rows: usize) -> HashMap<(i32, i32), Hsla> {
|
||||
pub fn highlight_cells(cells: &[RenderCell], rows: usize) -> HashMap<(i32, i32), HighlightStyle> {
|
||||
let colors = highlight_colors();
|
||||
|
||||
let mut row_chars: Vec<Vec<(i32, char)>> = vec![Vec::with_capacity(128); rows];
|
||||
@@ -365,7 +343,9 @@ pub fn highlight_cells(cells: &[RenderCell], rows: usize) -> HashMap<(i32, i32),
|
||||
if rc.row < 0 || (rc.row as usize) >= rows {
|
||||
continue;
|
||||
}
|
||||
row_chars[rc.row as usize].push((rc.col, rc.cell.c));
|
||||
if let Some(character) = rendered_text_character(&rc.cell) {
|
||||
row_chars[rc.row as usize].push((rc.col, character));
|
||||
}
|
||||
}
|
||||
for row in row_chars.iter_mut() {
|
||||
row.sort_by_key(|&(col, _)| col);
|
||||
@@ -852,7 +832,7 @@ pub fn highlight_cells(cells: &[RenderCell], rows: usize) -> HashMap<(i32, i32),
|
||||
map.apply_row(row_i32);
|
||||
|
||||
// ── 30. HTTP status codes ──────────────────────────────
|
||||
highlight_http_codes(&mut map.colors, text, &byte_to_col, row_i32, &colors);
|
||||
highlight_http_codes(&mut map.styles, text, &byte_to_col, row_i32, &colors);
|
||||
|
||||
// ── 31. IP addresses ───────────────────────────────────
|
||||
for m in find_ip_addresses(text) {
|
||||
@@ -860,7 +840,9 @@ pub fn highlight_cells(cells: &[RenderCell], rows: usize) -> HashMap<(i32, i32),
|
||||
let start_col = byte_to_col[m];
|
||||
let end_col = byte_to_col[(m + ip_len - 1).min(byte_to_col.len() - 1)];
|
||||
for c in start_col..=end_col {
|
||||
map.colors.entry((row_i32, c)).or_insert(colors.network);
|
||||
map.styles
|
||||
.entry((row_i32, c))
|
||||
.or_insert_with(|| HighlightStyle::from_foreground(colors.network));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -870,7 +852,9 @@ pub fn highlight_cells(cells: &[RenderCell], rows: usize) -> HashMap<(i32, i32),
|
||||
let start_col = byte_to_col[m];
|
||||
let end_col = byte_to_col[(m + port_len - 1).min(byte_to_col.len() - 1)];
|
||||
for c in start_col..=end_col {
|
||||
map.colors.entry((row_i32, c)).or_insert(colors.port);
|
||||
map.styles
|
||||
.entry((row_i32, c))
|
||||
.or_insert_with(|| HighlightStyle::from_foreground(colors.port));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -884,13 +868,15 @@ pub fn highlight_cells(cells: &[RenderCell], rows: usize) -> HashMap<(i32, i32),
|
||||
let idx = m + i;
|
||||
if idx < line.byte_to_cell.len() {
|
||||
let (r, c) = line.byte_to_cell[idx];
|
||||
map.colors.entry((r as i32, c as i32)).or_insert(colors.url);
|
||||
map.styles
|
||||
.entry((r as i32, c as i32))
|
||||
.or_insert_with(|| HighlightStyle::from_foreground(colors.url));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
map.colors
|
||||
map.styles
|
||||
}
|
||||
|
||||
fn find_ip_len(text: &str) -> usize {
|
||||
@@ -1090,10 +1076,12 @@ pub fn build_logical_lines<'a>(cells: &'a [RenderCell], rows: usize) -> Vec<Logi
|
||||
});
|
||||
|
||||
for rc in row_cells {
|
||||
line.text.push(rc.cell.c);
|
||||
let end_len = line.text.len();
|
||||
while line.byte_to_cell.len() < end_len {
|
||||
line.byte_to_cell.push((rc.row as usize, rc.col as usize));
|
||||
if let Some(character) = rendered_text_character(&rc.cell) {
|
||||
line.text.push(character);
|
||||
let end_len = line.text.len();
|
||||
while line.byte_to_cell.len() < end_len {
|
||||
line.byte_to_cell.push((rc.row as usize, rc.col as usize));
|
||||
}
|
||||
}
|
||||
line.row_cells.push(rc);
|
||||
}
|
||||
@@ -1108,6 +1096,21 @@ pub fn build_logical_lines<'a>(cells: &'a [RenderCell], rows: usize) -> Vec<Logi
|
||||
logical_lines
|
||||
}
|
||||
|
||||
fn rendered_text_character(cell: &alacritty_terminal::term::cell::Cell) -> Option<char> {
|
||||
use alacritty_terminal::term::cell::Flags;
|
||||
|
||||
if cell
|
||||
.flags
|
||||
.intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER)
|
||||
{
|
||||
None
|
||||
} else if cell.flags.contains(Flags::HIDDEN) {
|
||||
Some(' ')
|
||||
} else {
|
||||
Some(cell.c)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_url_at_cell(
|
||||
cells: &[RenderCell],
|
||||
rows: usize,
|
||||
@@ -1142,7 +1145,7 @@ pub fn find_url_at_cell(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{KeywordHighlights, KeywordMatch, find_url_len, hsla};
|
||||
use super::{HighlightStyle, KeywordHighlights, KeywordMatch, find_url_len, hsla};
|
||||
|
||||
fn detected_url(text: &str) -> &str {
|
||||
&text[..find_url_len(text)]
|
||||
@@ -1181,21 +1184,26 @@ mod tests {
|
||||
KeywordMatch {
|
||||
start_col: 3,
|
||||
end_col: 11,
|
||||
color: short_color,
|
||||
style: HighlightStyle::from_foreground(short_color),
|
||||
priority: 0,
|
||||
},
|
||||
KeywordMatch {
|
||||
start_col: 0,
|
||||
end_col: 11,
|
||||
color: long_color,
|
||||
style: HighlightStyle::from_foreground(long_color),
|
||||
priority: 1,
|
||||
},
|
||||
];
|
||||
|
||||
highlights.apply_row(0);
|
||||
|
||||
assert_eq!(highlights.colors.len(), 12);
|
||||
assert!(highlights.colors.values().all(|color| *color == long_color));
|
||||
assert_eq!(highlights.styles.len(), 12);
|
||||
assert!(
|
||||
highlights
|
||||
.styles
|
||||
.values()
|
||||
.all(|style| style.foreground == Some(long_color) && style.background.is_none())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1208,8 +1216,13 @@ mod tests {
|
||||
super::highlight_keywords(&mut highlights, text, &byte_to_col, 0, &["BOOT"], color);
|
||||
highlights.apply_row(0);
|
||||
|
||||
assert_eq!(highlights.colors.len(), 4);
|
||||
assert!((0..9).all(|col| !highlights.colors.contains_key(&(0, col))));
|
||||
assert!((10..14).all(|col| highlights.colors.get(&(0, col)) == Some(&color)));
|
||||
assert_eq!(highlights.styles.len(), 4);
|
||||
assert!((0..9).all(|col| !highlights.styles.contains_key(&(0, col))));
|
||||
assert!((10..14).all(|col| {
|
||||
highlights
|
||||
.styles
|
||||
.get(&(0, col))
|
||||
.is_some_and(|style| style.foreground == Some(color) && style.background.is_none())
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
+99
-24
@@ -345,7 +345,7 @@ impl Ashell {
|
||||
};
|
||||
(
|
||||
session.id.clone(),
|
||||
tab.cursor_state().map(|cursor| (cursor.row, cursor.col)),
|
||||
tab.buffer_cursor_position(),
|
||||
tab.is_alternate_screen_active(),
|
||||
)
|
||||
};
|
||||
@@ -1284,6 +1284,11 @@ fn terminal_command_text(
|
||||
start: (usize, usize),
|
||||
end: Option<(usize, usize)>,
|
||||
) -> Option<String> {
|
||||
let start = buffer_position_in_viewport(snapshot, start)?;
|
||||
let end = match end {
|
||||
Some(position) => Some(buffer_position_in_viewport(snapshot, position)?),
|
||||
None => None,
|
||||
};
|
||||
let logical_lines =
|
||||
crate::terminal::highlight::build_logical_lines(&snapshot.cells, snapshot.rows);
|
||||
for line in logical_lines {
|
||||
@@ -1315,42 +1320,38 @@ fn terminal_command_text(
|
||||
None
|
||||
}
|
||||
|
||||
fn buffer_position_in_viewport(
|
||||
snapshot: &crate::terminal::RenderSnapshot,
|
||||
position: (usize, usize),
|
||||
) -> Option<(usize, usize)> {
|
||||
let viewport_start = snapshot
|
||||
.history_size
|
||||
.saturating_sub(snapshot.display_offset);
|
||||
let row = position.0.checked_sub(viewport_start)?;
|
||||
(row < snapshot.rows && position.1 < snapshot.cols).then_some((row, position.1))
|
||||
}
|
||||
|
||||
fn command_history_text(rendered: Option<&str>, buffered: &str, input_uncertain: bool) -> String {
|
||||
let rendered = rendered.unwrap_or_default().trim();
|
||||
let buffered = buffered.trim();
|
||||
if !input_uncertain && !buffered.is_empty() {
|
||||
return buffered.to_string();
|
||||
}
|
||||
merge_command_text(rendered, buffered)
|
||||
}
|
||||
|
||||
fn merge_command_text(rendered: Option<&str>, buffered: &str) -> String {
|
||||
let rendered = rendered.unwrap_or_default().trim();
|
||||
let buffered = buffered.trim();
|
||||
if rendered.is_empty() {
|
||||
return buffered.to_string();
|
||||
}
|
||||
if buffered.is_empty() {
|
||||
return rendered.to_string();
|
||||
}
|
||||
if rendered.starts_with(buffered) || rendered.ends_with(buffered) {
|
||||
return rendered.to_string();
|
||||
}
|
||||
if buffered.starts_with(rendered) || buffered.ends_with(rendered) {
|
||||
// Completion extends the current token; a new argument after exact raw input is stale content.
|
||||
if rendered
|
||||
.strip_prefix(buffered)
|
||||
.and_then(|suffix| suffix.chars().next())
|
||||
.is_some_and(char::is_whitespace)
|
||||
{
|
||||
return buffered.to_string();
|
||||
}
|
||||
|
||||
let overlap = buffered
|
||||
.char_indices()
|
||||
.map(|(index, _)| index)
|
||||
.chain(std::iter::once(buffered.len()))
|
||||
.filter(|index| *index > 0 && rendered.ends_with(&buffered[..*index]))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
if overlap > 0 {
|
||||
format!("{rendered}{}", &buffered[overlap..])
|
||||
} else {
|
||||
rendered.to_string()
|
||||
}
|
||||
rendered.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1581,6 +1582,58 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_whitespace_delimited_screen_suffix_when_input_is_uncertain() {
|
||||
let command = "sh /site/jimureport/jimureport-restart.sh";
|
||||
let rendered = format!("{command} /sijiji-r");
|
||||
|
||||
assert_eq!(
|
||||
command_history_text(Some(rendered.as_str()), command, true),
|
||||
command
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_append_tab_completion_keystroke_fragments() {
|
||||
let command = "sh /site/jimureport/jimureport-restart.sh";
|
||||
let buffered = "sh /sijiji-r";
|
||||
|
||||
assert_eq!(command_history_text(Some(command), buffered, true), command);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn falls_back_to_raw_input_when_uncertain_screen_text_is_unavailable() {
|
||||
let command = "sh /site/jimureport/jimureport-restart.sh";
|
||||
|
||||
assert_eq!(command_history_text(None, command, true), command);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_screen_completion_that_extends_the_current_argument() {
|
||||
let buffered = "sh /site/jimu";
|
||||
let rendered = "sh /site/jimureport/jimureport-restart.sh";
|
||||
|
||||
assert_eq!(
|
||||
command_history_text(Some(rendered), buffered, true),
|
||||
rendered
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn excludes_hidden_screen_residue_from_rendered_command() {
|
||||
let command = "sh /site/jimureport/jimureport-restart.sh";
|
||||
let rendered = format!("$ {command} /sijiji-r");
|
||||
let mut snapshot = snapshot(&[rendered.as_str()], rendered.len());
|
||||
for cell in snapshot.cells.iter_mut().skip(2 + command.chars().count()) {
|
||||
cell.cell.flags.insert(Flags::HIDDEN);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
terminal_command_text(&snapshot, (0, 2), None),
|
||||
Some(command.to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncates_rendered_command_at_the_submission_cursor() {
|
||||
let command = "sh /site/vocano/vocano-restart.sh";
|
||||
@@ -1592,4 +1645,26 @@ mod tests {
|
||||
Some(command.to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_command_coordinates_stable_when_wrapping_scrolls_the_viewport() {
|
||||
let first_row = "$ sh /site/jimureport/jimureport-";
|
||||
let second_row = "restart.sh /sijiji-r";
|
||||
let mut snapshot = snapshot(&[first_row, second_row], first_row.len());
|
||||
snapshot.history_size = 3;
|
||||
snapshot.display_offset = 2;
|
||||
snapshot
|
||||
.cells
|
||||
.iter_mut()
|
||||
.find(|cell| cell.row == 0 && cell.col == first_row.len() as i32 - 1)
|
||||
.unwrap()
|
||||
.cell
|
||||
.flags
|
||||
.insert(Flags::WRAPLINE);
|
||||
|
||||
assert_eq!(
|
||||
terminal_command_text(&snapshot, (1, 2), Some((2, "restart.sh".len()))),
|
||||
Some("sh /site/jimureport/jimureport-restart.sh".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -887,7 +887,7 @@ pub struct TerminalTab {
|
||||
type HighlightCache = std::cell::RefCell<
|
||||
Option<(
|
||||
Vec<RenderCell>,
|
||||
std::collections::HashMap<(i32, i32), gpui::Hsla>,
|
||||
std::collections::HashMap<(i32, i32), self::highlight::HighlightStyle>,
|
||||
)>,
|
||||
>;
|
||||
|
||||
@@ -940,7 +940,7 @@ pub struct RenderSnapshot {
|
||||
pub history_size: usize,
|
||||
pub rows: usize,
|
||||
pub cols: usize,
|
||||
pub highlights: std::collections::HashMap<(i32, i32), gpui::Hsla>,
|
||||
pub highlights: std::collections::HashMap<(i32, i32), self::highlight::HighlightStyle>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -2136,7 +2136,7 @@ impl TerminalTab {
|
||||
let _ = self.cursor_state_for_click_at(now);
|
||||
}
|
||||
|
||||
fn buffer_cursor_position(&self) -> Option<(usize, usize)> {
|
||||
pub(crate) fn buffer_cursor_position(&self) -> Option<(usize, usize)> {
|
||||
let grid = self.term.grid();
|
||||
let row = usize::try_from(grid.cursor.point.line.0).ok()?;
|
||||
Some((
|
||||
|
||||
Reference in New Issue
Block a user