Compare commits

...
7 Commits
Author SHA1 Message Date
ZacharyZhang-NY 2a578d3238 Merge remote-tracking branch 'gitea/main'
CI / crate (push) Failing after 2s
CI / platforms (windows-latest) (push) Failing after 10s
CI / platforms (ubuntu-24.04) (push) Failing after 1m59s
CI / site (push) Failing after 24s
CI / platforms (macos-15-intel) (push) Canceled after 0s
CI / platforms (ubuntu-24.04-arm) (push) Canceled after 0s
CI / platforms (windows-11-arm) (push) Canceled after 0s
2026-10-09 15:51:09 -04:00
ZacharyZhang-NY 5d6efd46ec Update banner and dialog: texts from the built-in English and Chinese catalog (Ely UPD-1) 2026-10-09 12:01:50 -04:00
ZacharyZhang-NY f834f55c16 Toaster::shown: the titles and severities on show, for owners and tests 2026-10-09 10:50:01 -04:00
ZacharyZhang-NY b0dbfd429c Code colors: Rust, JavaScript, Ruby, Haskell, Erlang presets; TOML and CMake comments follow code (Ely R2.12) 2026-10-09 07:29:53 -04:00
ZacharyZhang-NY c77124812d Code colors per language: comments, keywords and constants by file; Shell comments start words (Ely R2.12) 2026-10-09 06:52:16 -04:00
ZacharyZhang-NY 5caf5aaa40 DiffViewer: lines color by their file's syntax; prose reads plain (Ely R2.12) 2026-10-09 05:09:41 -04:00
ZacharyZhang-NY 7f2ac3e4eb DiffViewer: prose files (Markdown, plain text) read plain, not as code 2026-10-09 03:24:12 -04:00
11 changed files with 420 additions and 30 deletions

No files matched your search

+1 -1
View File
@@ -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
View File
@@ -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)| {
(
+12
View File
@@ -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);
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+161
View File
@@ -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
View File
@@ -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
View File
@@ -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()
}
+20
View File
@@ -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
View File
@@ -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,
)))
}
}