Compare commits
7
Commits
f893693492
...
2a578d3238
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a578d3238
|
||
|
|
5d6efd46ec
|
||
|
|
f834f55c16
|
||
|
|
b0dbfd429c
|
||
|
|
c77124812d
|
||
|
|
5caf5aaa40
|
||
|
|
7f2ac3e4eb
|
No files matched your search
+1
-1
@@ -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};
|
||||
+18
-2
@@ -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, Syntax, 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,23 @@ 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<usize>, HighlightStyle)> {
|
||||
code_highlights(text, cx)
|
||||
styles(crate::forms::code_highlights(text, cx))
|
||||
}
|
||||
|
||||
/// A line's colors as `lang` reads it.
|
||||
pub(crate) fn colors_with(
|
||||
text: &str,
|
||||
lang: &Syntax,
|
||||
cx: &App,
|
||||
) -> Vec<(Range<usize>, HighlightStyle)> {
|
||||
styles(code_highlights_with(text, lang, cx))
|
||||
}
|
||||
|
||||
/// Highlights as gpui styles.
|
||||
fn styles(
|
||||
highlights: Vec<(Range<usize>, crate::forms::Highlight)>,
|
||||
) -> Vec<(Range<usize>, HighlightStyle)> {
|
||||
highlights
|
||||
.into_iter()
|
||||
.map(|(range, highlight)| {
|
||||
(
|
||||
|
||||
@@ -142,6 +142,18 @@ fn a_toast_folds_away_after_its_time(cx: &mut TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn the_owner_reads_what_is_shown(cx: &mut TestAppContext) {
|
||||
let (view, cx) = desk(cx);
|
||||
let first = push(&view, Toast::new("Saved"), cx);
|
||||
push(&view, Toast::new("Denied").severity(Severity::Danger), cx);
|
||||
let toaster = view.read_with(cx, |desk, _| desk.toaster.clone());
|
||||
let shown = |cx: &mut VisualTestContext| toaster.read_with(cx, |toaster, _| toaster.shown());
|
||||
assert_eq!(shown(cx), [("Saved".into(), None), ("Denied".into(), Some(Severity::Danger))]);
|
||||
toaster.update(cx, |toaster, cx| toaster.dismiss(first, cx));
|
||||
assert_eq!(shown(cx), [("Denied".into(), Some(Severity::Danger))], "a leaving toast is not shown");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn the_pointer_on_the_stack_holds_the_clock(cx: &mut TestAppContext) {
|
||||
let (view, cx) = desk(cx);
|
||||
|
||||
@@ -125,6 +125,12 @@ impl Toaster {
|
||||
id
|
||||
}
|
||||
|
||||
/// The titles and severities on show, oldest first.
|
||||
pub fn shown(&self) -> Vec<(SharedString, Option<Severity>)> {
|
||||
let staying = self.live.iter().filter(|live| !live.leaving);
|
||||
staying.map(|live| (live.toast.title.clone(), live.toast.severity)).collect()
|
||||
}
|
||||
|
||||
/// Sends a toast away: it fades and folds, then leaves the stack.
|
||||
pub fn dismiss(&mut self, id: u64, cx: &mut Context<Self>) {
|
||||
let Some(live) = self
|
||||
|
||||
+33
-5
@@ -5,7 +5,7 @@ use gpui::{
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
use super::{Highlight, Input, TextInput};
|
||||
use super::{Highlight, Input, TextInput, syntax::Syntax};
|
||||
use crate::theme::{ActiveTheme, ControlSize, TextSize};
|
||||
|
||||
/// What a span of source is.
|
||||
@@ -42,8 +42,21 @@ const CONSTANTS: &[&str] = &[
|
||||
"FALSE",
|
||||
];
|
||||
|
||||
/// Every language at once, for code of unknown kind.
|
||||
const GENERIC: Syntax = Syntax {
|
||||
comment: "//",
|
||||
word_comment: false,
|
||||
keywords: KEYWORDS,
|
||||
constants: CONSTANTS,
|
||||
};
|
||||
|
||||
/// Splits source into spans; spaces fall between them.
|
||||
pub(crate) fn lex(source: &str) -> Vec<(Range<usize>, Kind)> {
|
||||
lex_with(source, &GENERIC)
|
||||
}
|
||||
|
||||
/// Splits source as `syntax` reads it.
|
||||
pub(crate) fn lex_with(source: &str, syntax: &Syntax) -> Vec<(Range<usize>, Kind)> {
|
||||
let mut out = Vec::new();
|
||||
let mut chars = source.char_indices().peekable();
|
||||
while let Some((start, ch)) = chars.next() {
|
||||
@@ -61,7 +74,13 @@ pub(crate) fn lex(source: &str) -> Vec<(Range<usize>, Kind)> {
|
||||
}
|
||||
Kind::Text
|
||||
}
|
||||
'/' if chars.peek().is_some_and(|(_, next)| *next == '/') => {
|
||||
_ if source[start..].starts_with(syntax.comment)
|
||||
&& (!syntax.word_comment
|
||||
|| source[..start]
|
||||
.chars()
|
||||
.next_back()
|
||||
.is_none_or(char::is_whitespace)) =>
|
||||
{
|
||||
while chars.next_if(|(_, next)| *next != '\n').is_some() {}
|
||||
Kind::Comment
|
||||
}
|
||||
@@ -96,8 +115,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.
|
||||
pub fn code_highlights(source: &str, cx: &App) -> Vec<(Range<usize>, Highlight)> {
|
||||
code_highlights_with(source, &GENERIC, cx)
|
||||
}
|
||||
|
||||
/// Colors for code as `lang` reads it.
|
||||
pub(crate) fn code_highlights_with(
|
||||
source: &str,
|
||||
lang: &Syntax,
|
||||
cx: &App,
|
||||
) -> Vec<(Range<usize>, Highlight)> {
|
||||
let syntax = &cx.theme().colors.syntax;
|
||||
let spans = lex(source);
|
||||
let spans = lex_with(source, lang);
|
||||
let next_is = |ix: usize, mark: &str| {
|
||||
spans
|
||||
.get(ix + 1)
|
||||
@@ -109,8 +137,8 @@ pub fn code_highlights(source: &str, cx: &App) -> Vec<(Range<usize>, Highlight)>
|
||||
Kind::Comment => syntax.comment,
|
||||
Kind::Text => syntax.string,
|
||||
Kind::Number => syntax.number,
|
||||
Kind::Word if KEYWORDS.contains(&text) => syntax.keyword,
|
||||
Kind::Word if CONSTANTS.contains(&text) => syntax.constant,
|
||||
Kind::Word if lang.keywords.contains(&text) => syntax.keyword,
|
||||
Kind::Word if lang.constants.contains(&text) => syntax.constant,
|
||||
Kind::Word if next_is(ix, "(") => syntax.function,
|
||||
Kind::Word if text.starts_with(char::is_uppercase) => syntax.type_name,
|
||||
Kind::Word => syntax.variable,
|
||||
|
||||
+3
-1
@@ -37,6 +37,7 @@ mod slider;
|
||||
mod stepper;
|
||||
mod structure;
|
||||
mod switch;
|
||||
mod syntax;
|
||||
mod tags;
|
||||
#[cfg(all(test, feature = "test-support"))]
|
||||
mod tests;
|
||||
@@ -55,7 +56,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;
|
||||
@@ -112,6 +113,7 @@ pub use structure::{
|
||||
DirtyIndicator, Form, FormError, FormField, FormLabel, FormSection, InlineForm,
|
||||
};
|
||||
pub use switch::Switch;
|
||||
pub(crate) use syntax::{Syntax, syntax_of};
|
||||
pub use tags::TagInput;
|
||||
pub(crate) use text::{
|
||||
Backspace, Copy, Cut, Delete, DeleteWordLeft, Down, Enter, Redo, Submit, Undo, Up, bind_keys,
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
//! How one language's code reads, by file name.
|
||||
|
||||
/// A language's comments, keywords and constants.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) struct Syntax {
|
||||
/// The mark that opens a line comment.
|
||||
pub comment: &'static str,
|
||||
/// The mark counts only where a word starts (Shell).
|
||||
pub word_comment: bool,
|
||||
pub keywords: &'static [&'static str],
|
||||
pub constants: &'static [&'static str],
|
||||
}
|
||||
|
||||
impl Syntax {
|
||||
const fn new(
|
||||
comment: &'static str,
|
||||
keywords: &'static [&'static str],
|
||||
constants: &'static [&'static str],
|
||||
) -> Self {
|
||||
Self {
|
||||
comment,
|
||||
word_comment: false,
|
||||
keywords,
|
||||
constants,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
const RUST: Syntax = Syntax::new("//", &[
|
||||
"as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern",
|
||||
"fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref",
|
||||
"return", "static", "struct", "super", "trait", "type", "unsafe", "use", "where", "while",
|
||||
], &["true", "false", "self", "Self", "None", "Some", "Ok", "Err"]);
|
||||
|
||||
#[rustfmt::skip]
|
||||
const SCRIPT: Syntax = Syntax::new("//", &[
|
||||
"as", "async", "await", "break", "case", "catch", "class", "const", "continue", "default",
|
||||
"delete", "do", "else", "export", "extends", "finally", "for", "from", "function", "if",
|
||||
"import", "in", "instanceof", "interface", "let", "new", "of", "return", "static", "switch",
|
||||
"throw", "try", "type", "typeof", "var", "void", "while", "yield",
|
||||
], &["true", "false", "null", "undefined", "this", "NaN"]);
|
||||
|
||||
/// C, C++, Java, Go, Swift, Kotlin, C# and kin.
|
||||
#[rustfmt::skip]
|
||||
pub(crate) const C_LIKE: Syntax = Syntax::new("//", &[
|
||||
"break", "case", "catch", "class", "const", "continue", "default", "defer", "do", "else",
|
||||
"enum", "extends", "final", "for", "func", "fun", "go", "if", "implements", "import",
|
||||
"interface", "let", "namespace", "new", "package", "private", "protected", "public", "return",
|
||||
"static", "struct", "switch", "throw", "try", "typedef", "union", "using", "val", "var",
|
||||
"void", "while",
|
||||
], &["true", "false", "null", "nil", "nullptr", "this", "self"]);
|
||||
|
||||
#[rustfmt::skip]
|
||||
const PYTHON: Syntax = Syntax::new("#", &[
|
||||
"and", "as", "assert", "async", "await", "break", "class", "continue", "def", "del", "elif",
|
||||
"else", "except", "finally", "for", "from", "global", "if", "import", "in", "is", "lambda",
|
||||
"nonlocal", "not", "or", "pass", "raise", "return", "try", "while", "with", "yield",
|
||||
], &["True", "False", "None", "self"]);
|
||||
|
||||
#[rustfmt::skip]
|
||||
const RUBY: Syntax = Syntax::new("#", &[
|
||||
"alias", "and", "begin", "break", "case", "class", "def", "do", "else", "elsif", "end",
|
||||
"ensure", "for", "if", "in", "module", "next", "not", "or", "redo", "rescue", "retry",
|
||||
"return", "then", "unless", "until", "when", "while", "yield",
|
||||
], &["true", "false", "nil", "self"]);
|
||||
|
||||
#[rustfmt::skip]
|
||||
const SHELL: Syntax = Syntax { word_comment: true, ..Syntax::new("#", &[
|
||||
"case", "do", "done", "elif", "else", "esac", "export", "fi", "for", "function", "if", "in",
|
||||
"local", "return", "then", "until", "while",
|
||||
], &["true", "false"]) };
|
||||
|
||||
/// `#` languages, CMake and Make: no word list.
|
||||
const HASH: Syntax = Syntax::new("#", &[], &["true", "false"]);
|
||||
|
||||
/// YAML, whose `#` starts a comment only after a space.
|
||||
const YAML: Syntax = Syntax {
|
||||
word_comment: true,
|
||||
..Syntax::new("#", &[], &["true", "false", "null", "yes", "no"])
|
||||
};
|
||||
|
||||
const INI: Syntax = Syntax::new(";", &[], &["true", "false", "yes", "no", "on", "off"]);
|
||||
|
||||
#[rustfmt::skip]
|
||||
const SQL: Syntax = Syntax::new("--", &[
|
||||
"SELECT", "FROM", "WHERE", "JOIN", "LEFT", "RIGHT", "INNER", "OUTER", "ON", "AND", "OR",
|
||||
"NOT", "AS", "GROUP", "BY", "ORDER", "HAVING", "LIMIT", "OFFSET", "INSERT", "INTO", "VALUES",
|
||||
"UPDATE", "SET", "DELETE", "CREATE", "TABLE", "INDEX", "DROP", "ALTER", "DESC", "ASC",
|
||||
"DISTINCT", "UNION", "WITH", "CASE", "WHEN", "THEN", "ELSE", "END", "IS", "IN", "LIKE",
|
||||
"BETWEEN", "EXISTS",
|
||||
], &["NULL", "TRUE", "FALSE"]);
|
||||
|
||||
#[rustfmt::skip]
|
||||
const LUA: Syntax = Syntax::new("--", &[
|
||||
"and", "break", "do", "else", "elseif", "end", "for", "function", "goto", "if", "in", "local",
|
||||
"not", "or", "repeat", "return", "then", "until", "while",
|
||||
], &["true", "false", "nil"]);
|
||||
|
||||
#[rustfmt::skip]
|
||||
const HASKELL: Syntax = Syntax::new("--", &[
|
||||
"case", "class", "data", "deriving", "do", "else", "if", "import", "in", "instance", "let",
|
||||
"module", "newtype", "of", "then", "type", "where",
|
||||
], &["True", "False"]);
|
||||
|
||||
#[rustfmt::skip]
|
||||
const LISP: Syntax = Syntax::new(";", &[
|
||||
"defun", "defn", "define", "lambda", "let", "if", "cond",
|
||||
], &["nil", "t"]);
|
||||
|
||||
#[rustfmt::skip]
|
||||
const ERLANG: Syntax = Syntax::new("%", &[
|
||||
"after", "begin", "case", "catch", "end", "fun", "if", "of", "receive", "try", "when",
|
||||
], &["true", "false"]);
|
||||
|
||||
const TEX: Syntax = Syntax::new("%", &[], &[]);
|
||||
|
||||
/// Extensions of prose and plain text, which read plain.
|
||||
#[rustfmt::skip]
|
||||
const PROSE: &[&str] = &[
|
||||
"md", "markdown", "mdown", "mkd", "txt", "text", "rst", "adoc", "asciidoc", "org", "csv", "tsv",
|
||||
"log",
|
||||
];
|
||||
|
||||
/// Languages by extension; any other code reads C-like.
|
||||
#[rustfmt::skip]
|
||||
const BY_EXTENSION: &[(&Syntax, &[&str])] = &[
|
||||
(&RUST, &["rs"]),
|
||||
(&SCRIPT, &["js", "mjs", "cjs", "jsx", "ts", "tsx", "mts"]),
|
||||
(&PYTHON, &["py", "pyi", "pyw"]),
|
||||
(&RUBY, &["rb", "rake", "gemspec"]),
|
||||
(&SHELL, &["sh", "bash", "zsh", "fish", "ksh"]),
|
||||
(&HASH, &["pl", "r", "cmake", "mk", "nix", "ex", "exs", "jl", "nim", "cr", "tcl", "ps1",
|
||||
"toml", "conf", "cfg", "gitignore", "env", "properties"]),
|
||||
(&YAML, &["yaml", "yml"]),
|
||||
(&INI, &["ini"]),
|
||||
(&SQL, &["sql"]),
|
||||
(&LUA, &["lua"]),
|
||||
(&HASKELL, &["hs", "lhs", "elm"]),
|
||||
(&LISP, &["clj", "cljs", "lisp", "el", "scm"]),
|
||||
(&ERLANG, &["erl", "hrl"]),
|
||||
(&TEX, &["tex", "sty", "bib"]),
|
||||
];
|
||||
|
||||
/// How the file at `path` reads; none for prose.
|
||||
pub(crate) fn syntax_of(path: &str) -> Option<&'static Syntax> {
|
||||
let name = path.rsplit_once('/').map_or(path, |(_, name)| name);
|
||||
// Named files first: CMakeLists.txt is not prose.
|
||||
match name {
|
||||
"Makefile" | "GNUmakefile" | "Dockerfile" | "CMakeLists.txt" => return Some(&HASH),
|
||||
_ => {}
|
||||
}
|
||||
let ext = name.rsplit_once('.')?.1.to_ascii_lowercase();
|
||||
if PROSE.contains(&ext.as_str()) {
|
||||
return None;
|
||||
}
|
||||
let found = BY_EXTENSION
|
||||
.iter()
|
||||
.find(|(_, exts)| exts.contains(&ext.as_str()));
|
||||
Some(found.map_or(&C_LIKE, |(syntax, _)| *syntax))
|
||||
}
|
||||
+134
-4
@@ -9,7 +9,8 @@ use super::{
|
||||
viewer::DiffLayout,
|
||||
};
|
||||
use crate::{
|
||||
editor::{code_colors, stack},
|
||||
editor::{code_colors_with, stack},
|
||||
forms::Syntax,
|
||||
theme::Palette,
|
||||
};
|
||||
|
||||
@@ -78,10 +79,24 @@ pub(super) fn washes(kind: LineKind, colors: &Palette) -> (Option<Hsla>, Hsla) {
|
||||
}
|
||||
}
|
||||
|
||||
/// A line's code, colored as code, its changed words washed.
|
||||
pub(super) fn code(line: &DiffLine, colors: &Palette, cx: &App) -> StyledText {
|
||||
/// A line's syntax colors; none for prose.
|
||||
pub(super) fn colors_of(
|
||||
text: &str,
|
||||
syntax: Option<&Syntax>,
|
||||
cx: &App,
|
||||
) -> Vec<(Range<usize>, HighlightStyle)> {
|
||||
syntax.map_or_else(Vec::new, |syntax| code_colors_with(text, syntax, cx))
|
||||
}
|
||||
|
||||
/// A line colored as its file reads, changed words washed.
|
||||
pub(super) fn code(
|
||||
line: &DiffLine,
|
||||
colors: &Palette,
|
||||
syntax: Option<&Syntax>,
|
||||
cx: &App,
|
||||
) -> StyledText {
|
||||
let (_, word) = washes(line.kind, colors);
|
||||
let mut styles: Vec<(Range<usize>, HighlightStyle)> = code_colors(&line.text, cx);
|
||||
let mut styles = colors_of(&line.text, syntax, cx);
|
||||
styles.extend(line.words.iter().map(|range| {
|
||||
(
|
||||
range.clone(),
|
||||
@@ -108,6 +123,121 @@ pub(super) fn spoken(line: &DiffLine) -> SharedString {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[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| {
|
||||
let syntax = crate::forms::syntax_of(path);
|
||||
super::colors_of(text, syntax, cx)
|
||||
.into_iter()
|
||||
.map(|(range, style)| (range, style.color))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
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",
|
||||
"CMakeLists.txt",
|
||||
"build.cmake",
|
||||
] {
|
||||
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))));
|
||||
// Each language's own words.
|
||||
let python = color("assert ready and True", "a.py", cx);
|
||||
assert_eq!(
|
||||
[python[0].1, python[2].1, python[3].1],
|
||||
[
|
||||
Some(syntax.keyword),
|
||||
Some(syntax.keyword),
|
||||
Some(syntax.constant)
|
||||
]
|
||||
);
|
||||
assert_ne!(
|
||||
color("return = 42", "config.toml", cx)[0].1,
|
||||
Some(syntax.keyword),
|
||||
"a TOML key"
|
||||
);
|
||||
// Words belong to their own language.
|
||||
assert_ne!(
|
||||
color("let from = 42;", "main.rs", cx)[1].1,
|
||||
Some(syntax.keyword),
|
||||
"Rust's from"
|
||||
);
|
||||
assert_eq!(
|
||||
color("def hello", "a.rb", cx)[0].1,
|
||||
Some(syntax.keyword),
|
||||
"Ruby's def"
|
||||
);
|
||||
assert_eq!(
|
||||
color("-- return 42", "Main.hs", cx),
|
||||
[(0..12, Some(syntax.comment))]
|
||||
);
|
||||
assert_eq!(
|
||||
color("% return 42", "main.erl", cx),
|
||||
[(0..11, Some(syntax.comment))]
|
||||
);
|
||||
// A comment may follow code at once outside Shell.
|
||||
assert_eq!(
|
||||
color("answer = 42# return 99", "config.toml", cx).last(),
|
||||
Some(&(11..22, Some(syntax.comment)))
|
||||
);
|
||||
assert_eq!(
|
||||
color("set(x 1)# return 42", "CMakeLists.txt", cx).last(),
|
||||
Some(&(8..19, Some(syntax.comment)))
|
||||
);
|
||||
// Shell comments start words only.
|
||||
let shell = color("echo foo#bar 42", "run.sh", cx);
|
||||
assert!(
|
||||
shell.iter().all(|(_, c)| *c != Some(syntax.comment)),
|
||||
"{shell:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
color("echo x # note", "run.sh", cx).last(),
|
||||
Some(&(7..13, Some(syntax.comment)))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_viewer_reads_its_file_once_built() {
|
||||
let viewer = |path: &str| {
|
||||
super::super::viewer::DiffViewer::new("d", path.to_string(), "a\n", "b\n").syntax
|
||||
};
|
||||
assert_eq!(viewer("notes.md"), None);
|
||||
assert_eq!(viewer("a.py"), crate::forms::syntax_of("a.py"));
|
||||
assert_eq!(
|
||||
viewer("CMakeLists.txt"),
|
||||
crate::forms::syntax_of("Makefile")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_line_speaks_its_number_change_and_text() {
|
||||
let stretches = super::super::diff::diff("a\nb\n", "a\nc\n", 1);
|
||||
|
||||
+11
-3
@@ -13,6 +13,7 @@ use super::{
|
||||
};
|
||||
use crate::{
|
||||
buttons::{Button, ButtonVariant, SegmentedControl},
|
||||
forms::{Syntax, syntax_of},
|
||||
i18n,
|
||||
primitives::FocusRing,
|
||||
theme::{ActiveTheme, ControlSize, TextSize},
|
||||
@@ -38,6 +39,8 @@ type OnLine = Rc<dyn Fn(Place, bool, &mut Window, &mut App)>;
|
||||
pub struct DiffViewer {
|
||||
id: ElementId,
|
||||
path: SharedString,
|
||||
/// How its lines read, from the path.
|
||||
pub(super) syntax: Option<&'static Syntax>,
|
||||
stretches: Rc<Vec<Stretch>>,
|
||||
stat: (usize, usize),
|
||||
layout: DiffLayout,
|
||||
@@ -57,9 +60,11 @@ impl DiffViewer {
|
||||
old: &str,
|
||||
new: &str,
|
||||
) -> Self {
|
||||
let path: SharedString = path.into();
|
||||
Self {
|
||||
id: id.into(),
|
||||
path: path.into(),
|
||||
syntax: syntax_of(&path),
|
||||
path,
|
||||
stretches: Rc::new(diff(old, new, CONTEXT)),
|
||||
stat: super::diff::stat(old, new),
|
||||
layout: DiffLayout::Unified,
|
||||
@@ -194,6 +199,7 @@ impl RenderOnce for DiffViewer {
|
||||
.len()
|
||||
.max(2);
|
||||
let (id, on_open) = (self.id.clone(), self.on_open.clone());
|
||||
let syntax = self.syntax;
|
||||
let (button, button_height) = (
|
||||
self.hunk_action.is_some(),
|
||||
theme.control_height(ControlSize::Sm),
|
||||
@@ -269,7 +275,9 @@ impl RenderOnce for DiffViewer {
|
||||
.when_some(wash, |side, wash| side.bg(wash))
|
||||
.child(number(shown))
|
||||
.children(line.map(|(_, line)| {
|
||||
div().whitespace_nowrap().child(code(line, &colors, cx))
|
||||
div()
|
||||
.whitespace_nowrap()
|
||||
.child(code(line, &colors, syntax, cx))
|
||||
}));
|
||||
match line {
|
||||
Some((place, line)) => {
|
||||
@@ -328,7 +336,7 @@ impl RenderOnce for DiffViewer {
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.overflow_hidden()
|
||||
.child(code(line, &colors, cx)),
|
||||
.child(code(line, &colors, syntax, cx)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -41,6 +41,16 @@ const EN: &[(&str, &str)] = &[
|
||||
("templates.use", "Use template"),
|
||||
("templates.none", "No template matches"),
|
||||
("comments.reply", "Reply"),
|
||||
("update.title", "Software Update"),
|
||||
("update.available", "Version {version} is available."),
|
||||
("update.downloading", "Downloading {version}… {percent}%"),
|
||||
("update.ready", "Version {version} is ready."),
|
||||
("update.download", "Download"),
|
||||
("update.restart", "Restart to update"),
|
||||
("update.later", "Later"),
|
||||
("update.headline.available", "{name} {version} is available"),
|
||||
("update.headline.ready", "{name} {version} is ready"),
|
||||
("update.current", "You have {current}."),
|
||||
("comments.resolve", "Resolve"),
|
||||
("comments.reopen", "Reopen"),
|
||||
("comments.open", "Open {n}"),
|
||||
@@ -142,6 +152,16 @@ const ZH_CN: &[(&str, &str)] = &[
|
||||
("templates.use", "使用模板"),
|
||||
("templates.none", "没有匹配的模板"),
|
||||
("comments.reply", "回复"),
|
||||
("update.title", "软件更新"),
|
||||
("update.available", "{version} 版可以更新。"),
|
||||
("update.downloading", "正在下载 {version}… {percent}%"),
|
||||
("update.ready", "{version} 版已下载好。"),
|
||||
("update.download", "下载"),
|
||||
("update.restart", "重新启动以更新"),
|
||||
("update.later", "以后"),
|
||||
("update.headline.available", "{name} {version} 可以更新"),
|
||||
("update.headline.ready", "{name} {version} 已下载好"),
|
||||
("update.current", "当前为 {current}。"),
|
||||
("comments.resolve", "解决"),
|
||||
("comments.reopen", "重新打开"),
|
||||
("comments.open", "未解决 {n}"),
|
||||
|
||||
+21
-14
@@ -8,6 +8,7 @@ use gpui::{
|
||||
use super::{Hosted, open_hosted};
|
||||
use crate::{
|
||||
buttons::{Button, ButtonVariant, IconButton},
|
||||
i18n::text,
|
||||
layout::ScrollArea,
|
||||
motion::ProgressBar,
|
||||
primitives::{Icon, IconName},
|
||||
@@ -43,7 +44,7 @@ fn progress_line(state: UpdateState) -> Option<AnyElement> {
|
||||
}
|
||||
}
|
||||
|
||||
fn buttons(state: UpdateState, actions: &Actions, size: ControlSize) -> Vec<Button> {
|
||||
fn buttons(state: UpdateState, actions: &Actions, size: ControlSize, cx: &App) -> Vec<Button> {
|
||||
let wire = |button: Button, action: &Option<Action>| {
|
||||
action.clone().map(|action| {
|
||||
button
|
||||
@@ -52,17 +53,17 @@ fn buttons(state: UpdateState, actions: &Actions, size: ControlSize) -> Vec<Butt
|
||||
})
|
||||
};
|
||||
let later = wire(
|
||||
Button::new("update-later", "Later").variant(ButtonVariant::Ghost),
|
||||
Button::new("update-later", text(cx, "update.later", &[])).variant(ButtonVariant::Ghost),
|
||||
&actions.later,
|
||||
);
|
||||
let main = match state {
|
||||
UpdateState::Available => wire(
|
||||
Button::new("update-download", "Download").primary(),
|
||||
Button::new("update-download", text(cx, "update.download", &[])).primary(),
|
||||
&actions.download,
|
||||
),
|
||||
UpdateState::Downloading(_) => None,
|
||||
UpdateState::Ready => wire(
|
||||
Button::new("update-restart", "Restart to update").primary(),
|
||||
Button::new("update-restart", text(cx, "update.restart", &[])).primary(),
|
||||
&actions.restart,
|
||||
),
|
||||
};
|
||||
@@ -118,14 +119,16 @@ impl UpdateBanner {
|
||||
|
||||
impl RenderOnce for UpdateBanner {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let version = &self.version;
|
||||
let message = match self.state {
|
||||
UpdateState::Available => format!("Version {} is available.", self.version),
|
||||
UpdateState::Available => text(cx, "update.available", &[("version", version)]),
|
||||
UpdateState::Downloading(value) => {
|
||||
format!("Downloading {}… {}%", self.version, (value * 100.0).round())
|
||||
let percent = (value * 100.0).round().to_string();
|
||||
text(cx, "update.downloading", &[("version", version), ("percent", &percent)])
|
||||
}
|
||||
UpdateState::Ready => format!("Version {} is ready.", self.version),
|
||||
UpdateState::Ready => text(cx, "update.ready", &[("version", version)]),
|
||||
};
|
||||
let theme = cx.theme();
|
||||
let dismiss = self.actions.later.clone();
|
||||
div()
|
||||
.id(self.id)
|
||||
@@ -153,6 +156,7 @@ impl RenderOnce for UpdateBanner {
|
||||
..self.actions.clone()
|
||||
},
|
||||
ControlSize::Sm,
|
||||
cx,
|
||||
))
|
||||
.when_some(dismiss, |banner, dismiss| {
|
||||
banner.child(
|
||||
@@ -210,18 +214,20 @@ impl UpdateDialog {
|
||||
|
||||
pub fn open(self, cx: &mut App) -> anyhow::Result<WindowHandle<Hosted<UpdateDialog>>> {
|
||||
let size = cx.theme().dialog_window();
|
||||
open_hosted("Software Update", size, self, cx)
|
||||
open_hosted(text(cx, "update.title", &[]), size, self, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for UpdateDialog {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let args = [("name", self.name.as_ref()), ("version", self.version.as_ref())];
|
||||
let headline = match self.state {
|
||||
UpdateState::Ready => text(cx, "update.headline.ready", &args),
|
||||
_ => text(cx, "update.headline.available", &args),
|
||||
};
|
||||
let current = text(cx, "update.current", &[("current", &self.current)]);
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let headline = match self.state {
|
||||
UpdateState::Ready => format!("{} {} is ready", self.name, self.version),
|
||||
_ => format!("{} {} is available", self.name, self.version),
|
||||
};
|
||||
div()
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
@@ -236,7 +242,7 @@ impl RenderOnce for UpdateDialog {
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(Title::new(headline))
|
||||
.child(Caption::new(format!("You have {}.", self.current))),
|
||||
.child(Caption::new(current)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -265,6 +271,7 @@ impl RenderOnce for UpdateDialog {
|
||||
self.state,
|
||||
&self.actions,
|
||||
ControlSize::Md,
|
||||
cx,
|
||||
)))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user