mirror of
https://github.com/rust-kotlin/ashell.git
synced 2026-09-22 08:01:00 +00:00
refactor: optimize keyword highlighting by caching results and moving logic out of custom_blocks
This commit is contained in:
@@ -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<(i32, char)>> = 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<i32> = 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<usize> {
|
||||
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<usize> {
|
||||
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::<u8>().is_ok());
|
||||
if valid {
|
||||
positions.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
positions
|
||||
}
|
||||
|
||||
/// Find byte offsets of URLs starting with http:// or https://
|
||||
fn find_urls(text: &str) -> Vec<usize> {
|
||||
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<usize> {
|
||||
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::<u16>() {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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<BatchedTextRun> = 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;
|
||||
|
||||
@@ -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<(i32, char)>> = 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<i32> = 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<usize> {
|
||||
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<usize> {
|
||||
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::<u8>().is_ok());
|
||||
if valid {
|
||||
positions.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
positions
|
||||
}
|
||||
|
||||
fn find_urls(text: &str) -> Vec<usize> {
|
||||
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<usize> {
|
||||
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::<u16>() {
|
||||
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
|
||||
}
|
||||
+21
-1
@@ -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<Option<(Vec<RenderCell>, 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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user