DiffViewer: lines color by their file's syntax; prose reads plain (Ely R2.12)
This commit is contained in:
6 files changed
+138
-38
No files matched your search
+1
-1
@@ -60,6 +60,6 @@ pub use status::{
|
|||||||
LspStatus, NotificationBell,
|
LspStatus, NotificationBell,
|
||||||
};
|
};
|
||||||
pub use symbols::{Call, CallHierarchy, GoToSymbol, Symbol, SymbolKind, SymbolOutline};
|
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 tasks::{Task, TaskRunner, TaskState};
|
||||||
pub use welcome::{Project, ProjectSwitcher, RecentProjects, WelcomePage};
|
pub use welcome::{Project, ProjectSwitcher, RecentProjects, WelcomePage};
|
||||||
+11
-2
@@ -3,7 +3,7 @@ use std::ops::Range;
|
|||||||
use gpui::{App, HighlightStyle};
|
use gpui::{App, HighlightStyle};
|
||||||
|
|
||||||
use super::buffer::Buffer;
|
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.
|
/// 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)> {
|
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.
|
/// A line's syntax colors as highlight styles.
|
||||||
pub(crate) fn colors(text: &str, cx: &App) -> Vec<(Range<usize>, HighlightStyle)> {
|
pub(crate) fn colors(text: &str, cx: &App) -> Vec<(Range<usize>, 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<usize>, HighlightStyle)> {
|
||||||
|
code_highlights_with(text, comment, cx)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(range, highlight)| {
|
.map(|(range, highlight)| {
|
||||||
(
|
(
|
||||||
|
|||||||
+16
-2
@@ -44,6 +44,11 @@ const CONSTANTS: &[&str] = &[
|
|||||||
|
|
||||||
/// Splits source into spans; spaces fall between them.
|
/// Splits source into spans; spaces fall between them.
|
||||||
pub(crate) fn lex(source: &str) -> Vec<(Range<usize>, Kind)> {
|
pub(crate) fn lex(source: &str) -> Vec<(Range<usize>, Kind)> {
|
||||||
|
lex_with(source, "//")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Splits source whose line comments start with `comment`.
|
||||||
|
pub(crate) fn lex_with(source: &str, comment: &str) -> Vec<(Range<usize>, Kind)> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
let mut chars = source.char_indices().peekable();
|
let mut chars = source.char_indices().peekable();
|
||||||
while let Some((start, ch)) = chars.next() {
|
while let Some((start, ch)) = chars.next() {
|
||||||
@@ -61,7 +66,7 @@ pub(crate) fn lex(source: &str) -> Vec<(Range<usize>, Kind)> {
|
|||||||
}
|
}
|
||||||
Kind::Text
|
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() {}
|
while chars.next_if(|(_, next)| *next != '\n').is_some() {}
|
||||||
Kind::Comment
|
Kind::Comment
|
||||||
}
|
}
|
||||||
@@ -96,8 +101,17 @@ fn painted(spans: impl Iterator<Item = (Range<usize>, Hsla)>) -> Vec<(Range<usiz
|
|||||||
|
|
||||||
/// Colors for one line of code in most languages, from the syntax palette.
|
/// Colors for one line of code in most languages, from the syntax palette.
|
||||||
pub fn code_highlights(source: &str, cx: &App) -> Vec<(Range<usize>, Highlight)> {
|
pub fn code_highlights(source: &str, cx: &App) -> Vec<(Range<usize>, 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<usize>, Highlight)> {
|
||||||
let syntax = &cx.theme().colors.syntax;
|
let syntax = &cx.theme().colors.syntax;
|
||||||
let spans = lex(source);
|
let spans = lex_with(source, comment);
|
||||||
let next_is = |ix: usize, mark: &str| {
|
let next_is = |ix: usize, mark: &str| {
|
||||||
spans
|
spans
|
||||||
.get(ix + 1)
|
.get(ix + 1)
|
||||||
|
|||||||
+1
-1
@@ -55,7 +55,7 @@ pub use check::{CheckState, Checkbox, CheckboxGroup};
|
|||||||
pub use checked::{EmailInput, UrlInput, is_email, is_url};
|
pub use checked::{EmailInput, UrlInput, is_email, is_url};
|
||||||
pub use chips::ChoiceChips;
|
pub use chips::ChoiceChips;
|
||||||
pub use code::{CodeInput, JsonInput, code_highlights, json_highlights};
|
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 use color::{ColorPalette, ColorPicker, ColorSwatch, EyeDropper, GradientEditor, GradientStop};
|
||||||
pub(crate) use color::{hex, parse_hex};
|
pub(crate) use color::{hex, parse_hex};
|
||||||
pub use combobox::Combobox;
|
pub use combobox::Combobox;
|
||||||
|
|||||||
+105
-28
@@ -9,7 +9,7 @@ use super::{
|
|||||||
viewer::DiffLayout,
|
viewer::DiffLayout,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
editor::{code_colors, stack},
|
editor::{code_colors_with, stack},
|
||||||
theme::Palette,
|
theme::Palette,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -84,23 +84,71 @@ const PROSE: [&str; 13] = [
|
|||||||
"tsv", "log",
|
"tsv", "log",
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Whether `path` holds prose, read plain rather than as code.
|
/// Line comment marks by extension; others take `//`.
|
||||||
pub(super) fn prose(path: &str) -> bool {
|
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);
|
let name = path.rsplit_once('/').map_or(path, |(_, name)| name);
|
||||||
match name.rsplit_once('.') {
|
let ext = match name.rsplit_once('.') {
|
||||||
Some((stem, ext)) if !stem.is_empty() => PROSE.contains(&ext.to_ascii_lowercase().as_str()),
|
Some((stem, ext)) if !stem.is_empty() => ext.to_ascii_lowercase(),
|
||||||
_ => true,
|
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.
|
/// A line's syntax colors as the file at `path` reads.
|
||||||
pub(super) fn code(line: &DiffLine, colors: &Palette, prose: bool, cx: &App) -> StyledText {
|
pub(super) fn colors_of(text: &str, path: &str, cx: &App) -> Vec<(Range<usize>, 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 (_, word) = washes(line.kind, colors);
|
||||||
let mut styles: Vec<(Range<usize>, HighlightStyle)> = if prose {
|
let mut styles = colors_of(&line.text, path, cx);
|
||||||
Vec::new()
|
|
||||||
} else {
|
|
||||||
code_colors(&line.text, cx)
|
|
||||||
};
|
|
||||||
styles.extend(line.words.iter().map(|range| {
|
styles.extend(line.words.iter().map(|range| {
|
||||||
(
|
(
|
||||||
range.clone(),
|
range.clone(),
|
||||||
@@ -127,21 +175,50 @@ pub(super) fn spoken(line: &DiffLine) -> SharedString {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
#[test]
|
#[gpui::test]
|
||||||
fn prose_reads_plain_and_code_reads_as_code() {
|
fn each_file_colors_as_its_kind_reads(cx: &mut gpui::TestAppContext) {
|
||||||
for path in [
|
cx.update(|cx| {
|
||||||
"docs/README.md",
|
crate::init_for_tests(cx);
|
||||||
"notes.MARKDOWN",
|
let syntax = crate::theme::ActiveTheme::theme(cx).colors.syntax.clone();
|
||||||
"LICENSE",
|
let color = |text: &str, path: &str, cx: &gpui::App| {
|
||||||
".gitignore",
|
super::colors_of(text, path, cx)
|
||||||
"a/b.txt",
|
.into_iter()
|
||||||
"data.csv",
|
.map(|(range, style)| (range, style.color))
|
||||||
] {
|
.collect::<Vec<_>>()
|
||||||
assert!(super::prose(path), "{path}");
|
};
|
||||||
}
|
for path in [
|
||||||
for path in ["src/main.rs", "web/app.js", "a.md/b.py", "Cargo.toml"] {
|
"docs/README.md",
|
||||||
assert!(!super::prose(path), "{path}");
|
"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]
|
#[test]
|
||||||
|
|||||||
+4
-4
@@ -9,7 +9,7 @@ use gpui::{
|
|||||||
use super::{
|
use super::{
|
||||||
badges::DiffStat,
|
badges::DiffStat,
|
||||||
diff::{DiffLine, LineKind, Stretch, diff, side_numbers},
|
diff::{DiffLine, LineKind, Stretch, diff, side_numbers},
|
||||||
rows::{Place, Shown, code, prose, shown, spoken, washes},
|
rows::{Place, Shown, code, shown, spoken, washes},
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
buttons::{Button, ButtonVariant, SegmentedControl},
|
buttons::{Button, ButtonVariant, SegmentedControl},
|
||||||
@@ -194,7 +194,7 @@ impl RenderOnce for DiffViewer {
|
|||||||
.len()
|
.len()
|
||||||
.max(2);
|
.max(2);
|
||||||
let (id, on_open) = (self.id.clone(), self.on_open.clone());
|
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) = (
|
let (button, button_height) = (
|
||||||
self.hunk_action.is_some(),
|
self.hunk_action.is_some(),
|
||||||
theme.control_height(ControlSize::Sm),
|
theme.control_height(ControlSize::Sm),
|
||||||
@@ -272,7 +272,7 @@ impl RenderOnce for DiffViewer {
|
|||||||
.children(line.map(|(_, line)| {
|
.children(line.map(|(_, line)| {
|
||||||
div()
|
div()
|
||||||
.whitespace_nowrap()
|
.whitespace_nowrap()
|
||||||
.child(code(line, &colors, prose, cx))
|
.child(code(line, &colors, &path, cx))
|
||||||
}));
|
}));
|
||||||
match line {
|
match line {
|
||||||
Some((place, line)) => {
|
Some((place, line)) => {
|
||||||
@@ -331,7 +331,7 @@ impl RenderOnce for DiffViewer {
|
|||||||
.flex_1()
|
.flex_1()
|
||||||
.min_w_0()
|
.min_w_0()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(code(line, &colors, prose, cx)),
|
.child(code(line, &colors, &path, cx)),
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user