From 5caf5aaa4014b738017d4713dc96c055dcdd0ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 9 Oct 2026 05:09:41 -0400 Subject: [PATCH] DiffViewer: lines color by their file's syntax; prose reads plain (Ely R2.12) --- src/editor/mod.rs | 2 +- src/editor/syntax.rs | 13 ++++- src/forms/code.rs | 18 +++++- src/forms/mod.rs | 2 +- src/git/rows.rs | 133 ++++++++++++++++++++++++++++++++++--------- src/git/viewer.rs | 8 +-- 6 files changed, 138 insertions(+), 38 deletions(-) diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 6d3a4ac..f8220f1 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -60,6 +60,6 @@ pub use status::{ LspStatus, NotificationBell, }; pub use symbols::{Call, CallHierarchy, GoToSymbol, Symbol, SymbolKind, SymbolOutline}; -pub(crate) use syntax::colors as code_colors; +pub(crate) use syntax::{colors as code_colors, colors_with as code_colors_with}; pub use tasks::{Task, TaskRunner, TaskState}; pub use welcome::{Project, ProjectSwitcher, RecentProjects, WelcomePage}; diff --git a/src/editor/syntax.rs b/src/editor/syntax.rs index bd8910c..3df8917 100644 --- a/src/editor/syntax.rs +++ b/src/editor/syntax.rs @@ -3,7 +3,7 @@ use std::ops::Range; use gpui::{App, HighlightStyle}; use super::buffer::Buffer; -use crate::forms::{Kind, code_highlights, lex}; +use crate::forms::{Kind, code_highlights_with, lex}; /// Each line that opens a block, and the last line the block holds, read from indentation. pub(crate) fn folds(buffer: &Buffer) -> Vec<(usize, usize)> { @@ -110,7 +110,16 @@ pub(crate) fn matched(brackets: &[Bracket], caret: usize) -> Option<(usize, usiz /// A line's syntax colors as highlight styles. pub(crate) fn colors(text: &str, cx: &App) -> Vec<(Range, HighlightStyle)> { - code_highlights(text, cx) + colors_with(text, "//", cx) +} + +/// Colors for a line whose comments start with `comment`. +pub(crate) fn colors_with( + text: &str, + comment: &str, + cx: &App, +) -> Vec<(Range, HighlightStyle)> { + code_highlights_with(text, comment, cx) .into_iter() .map(|(range, highlight)| { ( diff --git a/src/forms/code.rs b/src/forms/code.rs index 937809f..e45073e 100644 --- a/src/forms/code.rs +++ b/src/forms/code.rs @@ -44,6 +44,11 @@ const CONSTANTS: &[&str] = &[ /// Splits source into spans; spaces fall between them. pub(crate) fn lex(source: &str) -> Vec<(Range, Kind)> { + lex_with(source, "//") +} + +/// Splits source whose line comments start with `comment`. +pub(crate) fn lex_with(source: &str, comment: &str) -> Vec<(Range, Kind)> { let mut out = Vec::new(); let mut chars = source.char_indices().peekable(); while let Some((start, ch)) = chars.next() { @@ -61,7 +66,7 @@ pub(crate) fn lex(source: &str) -> Vec<(Range, Kind)> { } Kind::Text } - '/' if chars.peek().is_some_and(|(_, next)| *next == '/') => { + _ if source[start..].starts_with(comment) => { while chars.next_if(|(_, next)| *next != '\n').is_some() {} Kind::Comment } @@ -96,8 +101,17 @@ fn painted(spans: impl Iterator, Hsla)>) -> Vec<(Range Vec<(Range, Highlight)> { + code_highlights_with(source, "//", cx) +} + +/// Colors for code whose line comments start with `comment`. +pub(crate) fn code_highlights_with( + source: &str, + comment: &str, + cx: &App, +) -> Vec<(Range, Highlight)> { let syntax = &cx.theme().colors.syntax; - let spans = lex(source); + let spans = lex_with(source, comment); let next_is = |ix: usize, mark: &str| { spans .get(ix + 1) diff --git a/src/forms/mod.rs b/src/forms/mod.rs index 8cbdae6..e1e8298 100644 --- a/src/forms/mod.rs +++ b/src/forms/mod.rs @@ -55,7 +55,7 @@ pub use check::{CheckState, Checkbox, CheckboxGroup}; pub use checked::{EmailInput, UrlInput, is_email, is_url}; pub use chips::ChoiceChips; pub use code::{CodeInput, JsonInput, code_highlights, json_highlights}; -pub(crate) use code::{Kind, lex}; +pub(crate) use code::{Kind, code_highlights_with, lex}; pub use color::{ColorPalette, ColorPicker, ColorSwatch, EyeDropper, GradientEditor, GradientStop}; pub(crate) use color::{hex, parse_hex}; pub use combobox::Combobox; diff --git a/src/git/rows.rs b/src/git/rows.rs index 04976a0..e6dded7 100644 --- a/src/git/rows.rs +++ b/src/git/rows.rs @@ -9,7 +9,7 @@ use super::{ viewer::DiffLayout, }; use crate::{ - editor::{code_colors, stack}, + editor::{code_colors_with, stack}, theme::Palette, }; @@ -84,23 +84,71 @@ const PROSE: [&str; 13] = [ "tsv", "log", ]; -/// Whether `path` holds prose, read plain rather than as code. -pub(super) fn prose(path: &str) -> bool { +/// Line comment marks by extension; others take `//`. +const COMMENTS: [(&str, &[&str]); 4] = [ + ( + "#", + &[ + "py", + "rb", + "sh", + "bash", + "zsh", + "fish", + "toml", + "yaml", + "yml", + "pl", + "r", + "conf", + "nix", + "ex", + "exs", + "cmake", + "mk", + "ps1", + "tcl", + "jl", + "cr", + "nim", + "gitignore", + ], + ), + ("--", &["sql", "lua", "hs", "elm", "ada"]), + ( + ";", + &["ini", "clj", "cljs", "lisp", "el", "scm", "asm", "s"], + ), + ("%", &["tex", "sty", "erl", "m"]), +]; + +/// How `path` reads: none for prose, else its comment mark. +pub(super) fn syntax(path: &str) -> Option<&'static str> { let name = path.rsplit_once('/').map_or(path, |(_, name)| name); - match name.rsplit_once('.') { - Some((stem, ext)) if !stem.is_empty() => PROSE.contains(&ext.to_ascii_lowercase().as_str()), - _ => true, + let ext = match name.rsplit_once('.') { + Some((stem, ext)) if !stem.is_empty() => ext.to_ascii_lowercase(), + Some(("", dotfile)) => dotfile.to_ascii_lowercase(), + _ if matches!(name, "Makefile" | "Dockerfile" | "CMakeLists.txt") => return Some("#"), + _ => return None, + }; + if PROSE.contains(&ext.as_str()) { + return None; } + let found = COMMENTS + .iter() + .find(|(_, exts)| exts.contains(&ext.as_str())); + Some(found.map_or("//", |(mark, _)| *mark)) } -/// A line's text, colored as code unless prose, changed words washed. -pub(super) fn code(line: &DiffLine, colors: &Palette, prose: bool, cx: &App) -> StyledText { +/// A line's syntax colors as the file at `path` reads. +pub(super) fn colors_of(text: &str, path: &str, cx: &App) -> Vec<(Range, HighlightStyle)> { + syntax(path).map_or_else(Vec::new, |comment| code_colors_with(text, comment, cx)) +} + +/// A line colored as its file reads, changed words washed. +pub(super) fn code(line: &DiffLine, colors: &Palette, path: &str, cx: &App) -> StyledText { let (_, word) = washes(line.kind, colors); - let mut styles: Vec<(Range, HighlightStyle)> = if prose { - Vec::new() - } else { - code_colors(&line.text, cx) - }; + let mut styles = colors_of(&line.text, path, cx); styles.extend(line.words.iter().map(|range| { ( range.clone(), @@ -127,21 +175,50 @@ pub(super) fn spoken(line: &DiffLine) -> SharedString { #[cfg(test)] mod tests { - #[test] - fn prose_reads_plain_and_code_reads_as_code() { - for path in [ - "docs/README.md", - "notes.MARKDOWN", - "LICENSE", - ".gitignore", - "a/b.txt", - "data.csv", - ] { - assert!(super::prose(path), "{path}"); - } - for path in ["src/main.rs", "web/app.js", "a.md/b.py", "Cargo.toml"] { - assert!(!super::prose(path), "{path}"); - } + #[gpui::test] + fn each_file_colors_as_its_kind_reads(cx: &mut gpui::TestAppContext) { + cx.update(|cx| { + crate::init_for_tests(cx); + let syntax = crate::theme::ActiveTheme::theme(cx).colors.syntax.clone(); + let color = |text: &str, path: &str, cx: &gpui::App| { + super::colors_of(text, path, cx) + .into_iter() + .map(|(range, style)| (range, style.color)) + .collect::>() + }; + for path in [ + "docs/README.md", + "notes.MARKDOWN", + "LICENSE", + "a/b.txt", + "data.csv", + ] { + assert!( + color("# return 42", path, cx).is_empty(), + "{path} reads plain" + ); + } + for path in [ + "script.py", + "config.toml", + "run.sh", + ".gitignore", + "Makefile", + ] { + assert_eq!( + color("# return 42", path, cx), + [(0..11, Some(syntax.comment))], + "{path}" + ); + } + assert_eq!( + color("-- return 42", "q.sql", cx), + [(0..12, Some(syntax.comment))] + ); + let rust = color("return 42 // done", "src/main.rs", cx); + assert_eq!(rust[0], (0..6, Some(syntax.keyword)), "{rust:?}"); + assert_eq!(rust.last(), Some(&(10..17, Some(syntax.comment)))); + }); } #[test] diff --git a/src/git/viewer.rs b/src/git/viewer.rs index d66b243..620dac7 100644 --- a/src/git/viewer.rs +++ b/src/git/viewer.rs @@ -9,7 +9,7 @@ use gpui::{ use super::{ badges::DiffStat, diff::{DiffLine, LineKind, Stretch, diff, side_numbers}, - rows::{Place, Shown, code, prose, shown, spoken, washes}, + rows::{Place, Shown, code, shown, spoken, washes}, }; use crate::{ buttons::{Button, ButtonVariant, SegmentedControl}, @@ -194,7 +194,7 @@ impl RenderOnce for DiffViewer { .len() .max(2); let (id, on_open) = (self.id.clone(), self.on_open.clone()); - let prose = prose(&self.path); + let path = self.path.clone(); let (button, button_height) = ( self.hunk_action.is_some(), theme.control_height(ControlSize::Sm), @@ -272,7 +272,7 @@ impl RenderOnce for DiffViewer { .children(line.map(|(_, line)| { div() .whitespace_nowrap() - .child(code(line, &colors, prose, cx)) + .child(code(line, &colors, &path, cx)) })); match line { Some((place, line)) => { @@ -331,7 +331,7 @@ impl RenderOnce for DiffViewer { .flex_1() .min_w_0() .overflow_hidden() - .child(code(line, &colors, prose, cx)), + .child(code(line, &colors, &path, cx)), ) .into_any_element() }