From 56fe5081221220c77e69d44628fccecf736511a6 Mon Sep 17 00:00:00 2001 From: TomZz Date: Tue, 23 Jun 2026 10:22:39 +0800 Subject: [PATCH] refactor: optimize keyword highlighting by caching results and moving logic out of custom_blocks --- src/terminal/custom_blocks.rs | 389 ---------------------------------- src/terminal/element.rs | 10 +- src/terminal/highlight.rs | 358 +++++++++++++++++++++++++++++++ src/terminal/mod.rs | 22 +- 4 files changed, 385 insertions(+), 394 deletions(-) create mode 100644 src/terminal/highlight.rs diff --git a/src/terminal/custom_blocks.rs b/src/terminal/custom_blocks.rs index 141764a..8e82bca 100644 --- a/src/terminal/custom_blocks.rs +++ b/src/terminal/custom_blocks.rs @@ -1,9 +1,5 @@ -use std::collections::HashMap; - use gpui::{Bounds, Hsla, Path, Pixels, Window, fill, point, px, size}; -use super::RenderCell; - pub fn is_custom_block_supported(c: char) -> bool { match c as u32 { 0x2580..=0x258F | 0x2590 | 0x2594..=0x259F => true, // Block Elements @@ -340,389 +336,4 @@ pub fn paint_custom_block( painted } -// --------------------------------------------------------------------------- -// Terminal keyword / pattern highlighting -// --------------------------------------------------------------------------- -fn hsla(r: u8, g: u8, b: u8) -> Hsla { - Hsla { - h: 0.0, - s: 0.0, - l: 0.0, - a: 1.0, - } - .into_rgba_like(r, g, b) -} - -trait HslaExt { - fn into_rgba_like(self, r: u8, g: u8, b: u8) -> Self; -} - -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, - }; - } - 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 } - } -} - -/// Highlight colors for common terminal keywords and patterns. -struct HighlightColors { - error: Hsla, // ERROR, FATAL, CRITICAL, PANIC - success: Hsla, // SUCCESS, OK, PASS - warning: Hsla, // WARN, WARNING - info: Hsla, // INFO, NOTICE - failure: Hsla, // FAIL, FAILED, DENIED, REJECTED, TIMEOUT - network: Hsla, // IP addresses - url: Hsla, // http://, https:// - port: Hsla, // :22, :443, etc. - debug: Hsla, // DEBUG, DBG, TRACE -} - -fn highlight_colors() -> HighlightColors { - HighlightColors { - error: hsla(224, 96, 96), // #E06060 red - success: hsla(126, 198, 153), // #7EC699 green - warning: hsla(232, 201, 122), // #E8C97A yellow - info: hsla(108, 180, 238), // #6CB4EE blue - failure: hsla(232, 168, 124), // #E8A87C orange - network: hsla(199, 146, 234), // #C792EA purple - url: hsla( 86, 212, 199), // #56D4C7 teal - port: hsla(130, 170, 200), // #82AAC8 muted teal - debug: hsla(130, 140, 155), // #828C9B gray - } -} - -/// Check if `c` is a word boundary (not alphanumeric or underscore). -fn is_boundary(c: char) -> bool { - !c.is_ascii_alphanumeric() && c != '_' -} - -/// Scan terminal cells for common keywords and patterns, returning a map of -/// `(row, col) -> highlight_color` for cells that should be recolored. -/// If `search_map` is provided, search match colors take priority over keyword colors. -pub fn highlight_cells( - cells: &[RenderCell], - rows: usize, - search_map: Option<&HashMap<(i32, i32), Hsla>>, -) -> HashMap<(i32, i32), Hsla> { - let colors = highlight_colors(); - - // Build a per-row char array with cell column tracking. - // row_chars[row] = Vec<(col, char)> - let mut row_chars: Vec> = vec![vec![]; rows]; - for rc in cells { - if rc.row < 0 || (rc.row as usize) >= rows { - continue; - } - row_chars[rc.row as usize].push((rc.col, rc.cell.c)); - } - for row in row_chars.iter_mut() { - row.sort_by_key(|&(col, _)| col); - } - - let mut map = HashMap::new(); - - for (row_idx, row) in row_chars.iter().enumerate() { - if row.is_empty() { - continue; - } - let row_i32 = row_idx as i32; - - // Build the text string and a byte-offset → column index lookup. - let mut chars_buf = String::with_capacity(row.len()); - let mut byte_to_col: Vec = Vec::new(); - for &(col, c) in row { - chars_buf.push(c); - // Pad the mapping so every byte of this char points to `col`. - while byte_to_col.len() < chars_buf.len() { - byte_to_col.push(col); - } - } - let text = chars_buf.as_str(); - - // ── 1. Error keywords (highest priority) ────────────────────────── - for kw in &["EMERGENCY", "CRITICAL", "FATAL", "PANIC", "ERROR", "ERR"] { - for m in find_keyword(text, kw) { - let start_col = byte_to_col[m]; - let end_col = byte_to_col[(m + kw.len()).min(byte_to_col.len() - 1)]; - for c in start_col..=end_col { - map.entry((row_i32, c)).or_insert(colors.error); - } - } - } - - // ── 2. Success keywords ─────────────────────────────────────────── - for kw in &["SUCCESS", "SUCCEEDED", "PASSED", "PASS", "OK"] { - for m in find_keyword(text, kw) { - let start_col = byte_to_col[m]; - let end_col = byte_to_col[(m + kw.len()).min(byte_to_col.len() - 1)]; - for c in start_col..=end_col { - map.entry((row_i32, c)).or_insert(colors.success); - } - } - } - - // ── 3. Failure keywords ─────────────────────────────────────────── - for kw in &["FAILED", "FAILURE", "DENIED", "REJECTED", "TIMEOUT", "FAIL"] { - for m in find_keyword(text, kw) { - let start_col = byte_to_col[m]; - let end_col = byte_to_col[(m + kw.len()).min(byte_to_col.len() - 1)]; - for c in start_col..=end_col { - map.entry((row_i32, c)).or_insert(colors.failure); - } - } - } - - // ── 4. Warning keywords ─────────────────────────────────────────── - for kw in &["WARNING", "WARN"] { - for m in find_keyword(text, kw) { - let start_col = byte_to_col[m]; - let end_col = byte_to_col[(m + kw.len()).min(byte_to_col.len() - 1)]; - for c in start_col..=end_col { - map.entry((row_i32, c)).or_insert(colors.warning); - } - } - } - - // ── 5. Info keywords ────────────────────────────────────────────── - for kw in &["NOTICE", "INFO"] { - for m in find_keyword(text, kw) { - let start_col = byte_to_col[m]; - let end_col = byte_to_col[(m + kw.len()).min(byte_to_col.len() - 1)]; - for c in start_col..=end_col { - map.entry((row_i32, c)).or_insert(colors.info); - } - } - } - - // ── 6. Debug keywords ───────────────────────────────────────────── - for kw in &["DEBUG", "DBG", "TRACE"] { - for m in find_keyword(text, kw) { - let start_col = byte_to_col[m]; - let end_col = byte_to_col[(m + kw.len()).min(byte_to_col.len() - 1)]; - for c in start_col..=end_col { - map.entry((row_i32, c)).or_insert(colors.debug); - } - } - } - - // ── 7. IP addresses ─────────────────────────────────────────────── - for m in find_ip_addresses(text) { - let start_col = byte_to_col[m]; - let end_col = byte_to_col[(m + find_ip_len(&text[m..])).min(byte_to_col.len() - 1)]; - for c in start_col..=end_col { - map.entry((row_i32, c)).or_insert(colors.network); - } - } - - // ── 8. URLs ─────────────────────────────────────────────────────── - for m in find_urls(text) { - let url_len = find_url_len(&text[m..]); - let start_col = byte_to_col[m]; - let end_col = byte_to_col[(m + url_len).min(byte_to_col.len() - 1)]; - for c in start_col..=end_col { - map.entry((row_i32, c)).or_insert(colors.url); - } - } - - // ── 9. Port numbers (:digits) ───────────────────────────────────── - for m in find_ports(text) { - let port_len = find_port_len(&text[m..]); - let start_col = byte_to_col[m]; - let end_col = byte_to_col[(m + port_len).min(byte_to_col.len() - 1)]; - for c in start_col..=end_col { - map.entry((row_i32, c)).or_insert(colors.port); - } - } - } - - // Merge search highlight map — search colors override keyword colors. - if let Some(sm) = search_map { - for (key, color) in sm { - map.insert(*key, *color); - } - } - - map -} - -/// Find all occurrences of `keyword` as a whole word in `text`. -/// Returns byte offsets. -fn find_keyword(text: &str, keyword: &str) -> Vec { - let mut positions = Vec::new(); - let mut start = 0; - while let Some(pos) = text[start..].find(keyword) { - let abs = start + pos; - let before_ok = abs == 0 - || text.as_bytes()[abs - 1] == b' ' - || is_boundary(text.as_bytes()[abs - 1] as char); - let after_pos = abs + keyword.len(); - let after_ok = after_pos >= text.len() - || text.as_bytes()[after_pos] == b' ' - || is_boundary(text.as_bytes()[after_pos] as char); - if before_ok && after_ok { - positions.push(abs); - } - start = abs + keyword.len(); - } - positions -} - -/// Check if `text` starts with a valid IP address. Returns byte length if so. -fn find_ip_len(text: &str) -> usize { - let bytes = text.as_bytes(); - let mut dots = 0u8; - let mut digits = 0u8; - let mut len = 0usize; - - for &b in bytes { - match b { - b'0'..=b'9' => { - digits += 1; - if digits > 3 { - return 0; - } - } - b'.' => { - if digits == 0 { - return 0; - } - dots += 1; - if dots > 3 { - return 0; - } - digits = 0; - } - _ => break, - } - len += 1; - } - - if dots == 3 && digits > 0 { - len - } else { - 0 - } -} - -/// Find byte offsets of IP addresses in text. -fn find_ip_addresses(text: &str) -> Vec { - let mut positions = Vec::new(); - let bytes = text.as_bytes(); - let len = bytes.len(); - - for i in 0..len { - if bytes[i].is_ascii_digit() - && (i == 0 || is_boundary(bytes[i - 1] as char)) - { - let remaining = &text[i..]; - let ip_len = find_ip_len(remaining); - if ip_len > 0 { - // Validate each octet is 0-255. - let ip_str = &remaining[..ip_len]; - let valid = ip_str - .split('.') - .all(|octet| octet.parse::().is_ok()); - if valid { - positions.push(i); - } - } - } - } - positions -} - -/// Find byte offsets of URLs starting with http:// or https:// -fn find_urls(text: &str) -> Vec { - let mut positions = Vec::new(); - let mut start = 0; - while let Some(pos) = text[start..].find("http") { - let abs = start + pos; - let remaining = &text[abs..]; - if remaining.starts_with("https://") || remaining.starts_with("http://") { - if abs == 0 || is_boundary(text.as_bytes()[abs - 1] as char) { - positions.push(abs); - } - } - start = abs + 4; - } - positions -} - -/// Get byte length of a URL token (until whitespace or end of string). -fn find_url_len(text: &str) -> usize { - text.find(|c: char| c.is_ascii_whitespace()) - .unwrap_or(text.len()) -} - -/// Find byte offsets of port patterns like `:22`, `:443`, `:8080`. -fn find_ports(text: &str) -> Vec { - let mut positions = Vec::new(); - let bytes = text.as_bytes(); - let len = bytes.len(); - - for i in 0..len { - if bytes[i] == b':' - && i + 1 < len - && bytes[i + 1].is_ascii_digit() - && (i == 0 || is_boundary(bytes[i - 1] as char) || bytes[i - 1] == b' ') - { - let mut j = i + 1; - while j < len && bytes[j].is_ascii_digit() { - j += 1; - } - let port_str = &text[i + 1..j]; - if let Ok(port) = port_str.parse::() { - if port > 0 { - let after_ok = j >= len || is_boundary(bytes[j] as char); - if after_ok { - positions.push(i); - } - } - } - } - } - positions -} - -/// Get byte length of a port token starting at ':'. -fn find_port_len(text: &str) -> usize { - if !text.starts_with(':') { - return 0; - } - let mut len = 1; - for b in text.as_bytes()[1..].iter() { - if b.is_ascii_digit() { - len += 1; - } else { - break; - } - } - len -} diff --git a/src/terminal/element.rs b/src/terminal/element.rs index fb0eada..8930bdc 100644 --- a/src/terminal/element.rs +++ b/src/terminal/element.rs @@ -11,7 +11,7 @@ use gpui::{ use gpui_component::ActiveTheme as _; use crate::Ashell; -use crate::terminal::custom_blocks::{highlight_cells, is_custom_block_supported, paint_custom_block}; +use crate::terminal::custom_blocks::{is_custom_block_supported, paint_custom_block}; use crate::terminal::{RenderSnapshot, ViewportSelection}; #[derive(Clone, Copy)] @@ -357,9 +357,11 @@ impl TerminalElement { let mut custom_blocks = Vec::new(); let mut current_run: Option = None; - // Compute keyword / pattern highlight map once per frame. - let highlights = - highlight_cells(&self.snapshot.cells, self.snapshot.rows, self.search_highlights.as_ref()); + // 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; diff --git a/src/terminal/highlight.rs b/src/terminal/highlight.rs new file mode 100644 index 0000000..61a11e9 --- /dev/null +++ b/src/terminal/highlight.rs @@ -0,0 +1,358 @@ +use std::collections::HashMap; +use gpui::Hsla; +use crate::terminal::RenderCell; + +trait HslaExt { + fn into_rgba_like(self, r: u8, g: u8, b: u8) -> Self; +} + +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 }; + } + 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 } + } +} + +#[derive(Debug, Clone)] +struct HighlightColors { + error: Hsla, + success: Hsla, + warning: Hsla, + info: Hsla, + failure: Hsla, + network: Hsla, + url: Hsla, + port: Hsla, + debug: Hsla, +} + +fn hsla(r: u8, g: u8, b: u8) -> Hsla { + Hsla { + h: 0.0, + s: 0.0, + l: 0.0, + a: 1.0, + } + .into_rgba_like(r, g, b) +} + +fn highlight_colors() -> HighlightColors { + HighlightColors { + error: hsla(224, 96, 96), // #E06060 red + success: hsla(126, 198, 153), // #7EC699 green + warning: hsla(232, 201, 122), // #E8C97A yellow + info: hsla(108, 180, 238), // #6CB4EE blue + failure: hsla(232, 168, 124), // #E8A87C orange + network: hsla(199, 146, 234), // #C792EA purple + url: hsla( 86, 212, 199), // #56D4C7 teal + port: hsla(130, 170, 200), // #82AAC8 muted teal + debug: hsla(130, 140, 155), // #828C9B gray + } +} + +fn is_boundary(c: char) -> bool { + !c.is_ascii_alphanumeric() && c != '_' +} + +pub fn highlight_cells( + cells: &[RenderCell], + rows: usize, +) -> HashMap<(i32, i32), Hsla> { + let colors = highlight_colors(); + + // Pre-allocate the outer vector to the size of rows. + let mut row_chars: Vec> = vec![Vec::with_capacity(128); rows]; + for rc in cells { + if rc.row < 0 || (rc.row as usize) >= rows { + continue; + } + row_chars[rc.row as usize].push((rc.col, rc.cell.c)); + } + for row in row_chars.iter_mut() { + row.sort_by_key(|&(col, _)| col); + } + + let mut map = HashMap::new(); + + // Reusable buffers to avoid allocation inside the loop + let mut chars_buf = String::with_capacity(128); + let mut byte_to_col: Vec = Vec::with_capacity(128); + + for (row_idx, row) in row_chars.iter().enumerate() { + if row.is_empty() { + continue; + } + let row_i32 = row_idx as i32; + + chars_buf.clear(); + byte_to_col.clear(); + + for &(col, c) in row { + chars_buf.push(c); + while byte_to_col.len() < chars_buf.len() { + byte_to_col.push(col); + } + } + let text = chars_buf.as_str(); + + // ── 1. Error keywords ────────────────────────── + for kw in &["EMERGENCY", "CRITICAL", "FATAL", "PANIC", "ERROR", "ERR"] { + for m in find_keyword(text, kw) { + let start_col = byte_to_col[m]; + let end_col = byte_to_col[(m + kw.len()).min(byte_to_col.len() - 1)]; + for c in start_col..=end_col { + map.entry((row_i32, c)).or_insert(colors.error); + } + } + } + + // ── 2. Success keywords ─────────────────────────── + for kw in &["SUCCESS", "SUCCEEDED", "PASSED", "PASS", "OK"] { + for m in find_keyword(text, kw) { + let start_col = byte_to_col[m]; + let end_col = byte_to_col[(m + kw.len()).min(byte_to_col.len() - 1)]; + for c in start_col..=end_col { + map.entry((row_i32, c)).or_insert(colors.success); + } + } + } + + // ── 3. Failure keywords ─────────────────────────── + for kw in &["FAILED", "FAILURE", "DENIED", "REJECTED", "TIMEOUT", "FAIL"] { + for m in find_keyword(text, kw) { + let start_col = byte_to_col[m]; + let end_col = byte_to_col[(m + kw.len()).min(byte_to_col.len() - 1)]; + for c in start_col..=end_col { + map.entry((row_i32, c)).or_insert(colors.failure); + } + } + } + + // ── 4. Warning keywords ─────────────────────────── + for kw in &["WARNING", "WARN"] { + for m in find_keyword(text, kw) { + let start_col = byte_to_col[m]; + let end_col = byte_to_col[(m + kw.len()).min(byte_to_col.len() - 1)]; + for c in start_col..=end_col { + map.entry((row_i32, c)).or_insert(colors.warning); + } + } + } + + // ── 5. Info keywords ────────────────────────────── + for kw in &["NOTICE", "INFO"] { + for m in find_keyword(text, kw) { + let start_col = byte_to_col[m]; + let end_col = byte_to_col[(m + kw.len()).min(byte_to_col.len() - 1)]; + for c in start_col..=end_col { + map.entry((row_i32, c)).or_insert(colors.info); + } + } + } + + // ── 6. Debug keywords ───────────────────────────── + for kw in &["DEBUG", "DBG", "TRACE"] { + for m in find_keyword(text, kw) { + let start_col = byte_to_col[m]; + let end_col = byte_to_col[(m + kw.len()).min(byte_to_col.len() - 1)]; + for c in start_col..=end_col { + map.entry((row_i32, c)).or_insert(colors.debug); + } + } + } + + // ── 7. IP addresses ─────────────────────────────── + for m in find_ip_addresses(text) { + let start_col = byte_to_col[m]; + let end_col = byte_to_col[(m + find_ip_len(&text[m..])).min(byte_to_col.len() - 1)]; + for c in start_col..=end_col { + map.entry((row_i32, c)).or_insert(colors.network); + } + } + + // ── 8. URLs ─────────────────────────────────────── + for m in find_urls(text) { + let url_len = find_url_len(&text[m..]); + let start_col = byte_to_col[m]; + let end_col = byte_to_col[(m + url_len).min(byte_to_col.len() - 1)]; + for c in start_col..=end_col { + map.entry((row_i32, c)).or_insert(colors.url); + } + } + + // ── 9. Port numbers ───────────────────────────────────── + for m in find_ports(text) { + let port_len = find_port_len(&text[m..]); + let start_col = byte_to_col[m]; + let end_col = byte_to_col[(m + port_len).min(byte_to_col.len() - 1)]; + for c in start_col..=end_col { + map.entry((row_i32, c)).or_insert(colors.port); + } + } + } + + map +} + +fn find_keyword(text: &str, keyword: &str) -> Vec { + let mut positions = Vec::new(); + let mut start = 0; + while let Some(pos) = text[start..].find(keyword) { + let abs = start + pos; + let before_ok = abs == 0 + || text.as_bytes()[abs - 1] == b' ' + || is_boundary(text.as_bytes()[abs - 1] as char); + let after_pos = abs + keyword.len(); + let after_ok = after_pos >= text.len() + || text.as_bytes()[after_pos] == b' ' + || is_boundary(text.as_bytes()[after_pos] as char); + if before_ok && after_ok { + positions.push(abs); + } + start = abs + keyword.len(); + } + positions +} + +fn find_ip_len(text: &str) -> usize { + let bytes = text.as_bytes(); + let mut dots = 0u8; + let mut digits = 0u8; + let mut len = 0usize; + + for &b in bytes { + match b { + b'0'..=b'9' => { + digits += 1; + if digits > 3 { + return 0; + } + } + b'.' => { + if digits == 0 { + return 0; + } + dots += 1; + if dots > 3 { + return 0; + } + digits = 0; + } + _ => break, + } + len += 1; + } + + if dots == 3 && digits > 0 { + len + } else { + 0 + } +} + +fn find_ip_addresses(text: &str) -> Vec { + let mut positions = Vec::new(); + let bytes = text.as_bytes(); + let len = bytes.len(); + + for i in 0..len { + if bytes[i].is_ascii_digit() + && (i == 0 || is_boundary(bytes[i - 1] as char)) + { + let remaining = &text[i..]; + let ip_len = find_ip_len(remaining); + if ip_len > 0 { + let ip_str = &remaining[..ip_len]; + let valid = ip_str + .split('.') + .all(|octet| octet.parse::().is_ok()); + if valid { + positions.push(i); + } + } + } + } + positions +} + +fn find_urls(text: &str) -> Vec { + let mut positions = Vec::new(); + let mut start = 0; + while let Some(pos) = text[start..].find("http") { + let abs = start + pos; + let remaining = &text[abs..]; + if remaining.starts_with("https://") || remaining.starts_with("http://") { + if abs == 0 || is_boundary(text.as_bytes()[abs - 1] as char) { + positions.push(abs); + } + } + start = abs + 4; + } + positions +} + +fn find_url_len(text: &str) -> usize { + text.find(|c: char| c.is_ascii_whitespace()) + .unwrap_or(text.len()) +} + +fn find_ports(text: &str) -> Vec { + let mut positions = Vec::new(); + let bytes = text.as_bytes(); + let len = bytes.len(); + + for i in 0..len { + if bytes[i] == b':' + && i + 1 < len + && bytes[i + 1].is_ascii_digit() + && (i == 0 || is_boundary(bytes[i - 1] as char) || bytes[i - 1] == b' ') + { + let mut j = i + 1; + while j < len && bytes[j].is_ascii_digit() { + j += 1; + } + let port_str = &text[i + 1..j]; + if let Ok(port) = port_str.parse::() { + if port > 0 { + let after_ok = j >= len || is_boundary(bytes[j] as char); + if after_ok { + positions.push(i); + } + } + } + } + } + positions +} + +fn find_port_len(text: &str) -> usize { + if !text.starts_with(':') { + return 0; + } + let mut len = 1; + for b in text.as_bytes()[1..].iter() { + if b.is_ascii_digit() { + len += 1; + } else { + break; + } + } + len +} diff --git a/src/terminal/mod.rs b/src/terminal/mod.rs index 80ceac1..bf9b84f 100644 --- a/src/terminal/mod.rs +++ b/src/terminal/mod.rs @@ -1,6 +1,7 @@ pub mod custom_blocks; pub mod element; pub mod input; +pub mod highlight; use std::sync::mpsc::Sender; @@ -144,6 +145,7 @@ pub struct TerminalTab { rows: u16, pub backend: BackendTx, pub scroll_pixel_y: f32, + pub(crate) highlight_cache: std::cell::RefCell, std::collections::HashMap<(i32, i32), gpui::Hsla>)>>, } #[derive(Clone, Copy)] @@ -153,7 +155,7 @@ pub struct CursorState { pub shape: CursorShape, } -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct RenderCell { pub row: i32, pub col: i32, @@ -169,6 +171,7 @@ pub struct RenderSnapshot { pub history_size: usize, pub rows: usize, pub cols: usize, + pub highlights: std::collections::HashMap<(i32, i32), gpui::Hsla>, } #[derive(Clone, Copy)] @@ -251,6 +254,7 @@ impl TerminalTab { rows: 30, backend, scroll_pixel_y: 0.0, + highlight_cache: std::cell::RefCell::new(None), } } @@ -328,6 +332,21 @@ impl TerminalTab { }); } + // Get highlights from cache or recompute + let highlights = { + let mut cache = self.highlight_cache.borrow_mut(); + let cache_valid = cache.as_ref().map_or(false, |(cached_cells, _)| { + cached_cells == &cells + }); + if cache_valid { + cache.as_ref().unwrap().1.clone() + } else { + let computed = self::highlight::highlight_cells(&cells, rows as usize); + *cache = Some((cells.clone(), computed.clone())); + computed + } + }; + RenderSnapshot { cells, cursor: self.cursor_state(), @@ -336,6 +355,7 @@ impl TerminalTab { history_size: self.term.grid().history_size(), rows: self.rows as usize, cols: self.cols as usize, + highlights, } }