From a4bf6965f9dd4802ca37cb3da5d338b360a169d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Wed, 14 Oct 2026 20:41:47 -0400 Subject: [PATCH] i18n: the find box, sort and filter builders and password toggle in Chinese The find counts, toggles, replace buttons, the builders' add, join and direction labels, the filter tests and the password tooltip read the builtin catalog. Test::label becomes Test::key, the choice's value; its label is the catalog's. The shipped-copy scan checks button, segment and match-arm text on every match, and zh-CN render tests cover both. Co-Authored-By: Claude Opus 5.5 (1M context) --- AGENTS.md | 2 +- src/editor/find.rs | 77 +++++++++++++++++++++++-------------- src/editor/tests/chinese.rs | 40 +++++++++++++++++++ src/editor/tests/mod.rs | 1 + src/forms/input.rs | 14 ++++--- src/i18n/builtin.rs | 50 ++++++++++++++++++++++++ src/i18n/tests/shipped.rs | 44 +++++++++++++++++---- src/tables/builders.rs | 76 +++++++++++++++++++++--------------- src/tables/rules.rs | 15 ++++---- src/tables/tests/chinese.rs | 62 +++++++++++++++++++++++++++++ src/tables/tests/mod.rs | 1 + 11 files changed, 303 insertions(+), 79 deletions(-) create mode 100644 src/editor/tests/chinese.rs create mode 100644 src/tables/tests/chinese.rs diff --git a/AGENTS.md b/AGENTS.md index b8faa2f..bd73968 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -226,7 +226,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark. - A pane the window's blur shows through is `shell::Vibrancy`, filled with the palette's `glass`, in a window opened `WindowBackgroundAppearance::Blurred`: vibrancy on macOS, acrylic on Windows. gpui's `MicaBackdrop` and `MicaAltBackdrop` are DWM backdrops from Windows 11 build 22621; older builds get none, in silence, and macOS plain transparency. - The chart's hues go by `theme::HUE_NAMES`, for an event's color and a label's alike. - Text whose every character counts, encoded data and keys, goes through `typography::literal`: JetBrains Mono draws `==` and `->` as ligatures (`devtools::Encoder`). -- Messages go through `i18n::I18n`, a global the app sets: `cx.global::().text(key, args)` fills each `{name}`. Every catalog holds the first one's keys; a missing key, argument or catalog fails. Copy a component draws, tooltips and default placeholders too, reads `i18n::text` (app catalog, then the builtin English and Chinese); `i18n/tests/shipped.rs` fails on a fixed English literal in a component Ely ships. A right-to-left locale mirrors a stack with `Direction::row` and sets its text with `Direction::align`; CoreText orders mixed Hebrew and Latin runs on macOS, but gpui sets no paragraph direction, so a line takes its first strong character's: `text` leads a right-to-left message with U+200F, so its words suit labels, not a field's value, and `Locale::number` a right-to-left minus with U+200E, as CLDR does. gpui wraps shaped glyphs in the order they are drawn, on the web as on macOS, so a right-to-left line that wraps comes out garbled: right-to-left text stays on one line, an `Ellipsis`, which cuts by characters and sits by its text's alignment. +- Messages go through `i18n::I18n`, a global the app sets: `cx.global::().text(key, args)` fills each `{name}`. Every catalog holds the first one's keys; a missing key, argument or catalog fails. Copy a component draws, tooltips and default placeholders too, reads `i18n::text` (app catalog, then the builtin English and Chinese); `i18n/tests/shipped.rs` fails on a fixed English literal in a component Ely ships (labels, tooltips, button and segment text, match arms, capitalized literal arguments); a choice's value stays its key, its label the catalog's (`tables::Test::key`). A right-to-left locale mirrors a stack with `Direction::row` and sets its text with `Direction::align`; CoreText orders mixed Hebrew and Latin runs on macOS, but gpui sets no paragraph direction, so a line takes its first strong character's: `text` leads a right-to-left message with U+200F, so its words suit labels, not a field's value, and `Locale::number` a right-to-left minus with U+200E, as CLDR does. gpui wraps shaped glyphs in the order they are drawn, on the web as on macOS, so a right-to-left line that wraps comes out garbled: right-to-left text stays on one line, an `Ellipsis`, which cuts by characters and sits by its text's alignment. - gpui orders Tab stops by `tab_index` before paint order, and registers none for an `invisible()` element. A stop that must come first where it sits, yet draw over what paints after it, stays in flow at zero size and raises what it shows (`i18n::SkipLink`): a lower `tab_index` would sort it before the root scope's own handle, so Tab from the root would pass it. - Color-blind safe charts swap Ely's chart hues for a set fitted to stay apart under Machado's protanopia, deuteranopia and tritanopia; a test holds every pair 0.10 apart in Oklab and each hue 3:1 on its page (`Theme::set_color_blind_safe`, in Ely's own palettes alone). - gpui packs pictures in its atlas without padding and samples them with a linear filter, on the web as on macOS, so a scaled picture's edge takes color from its atlas neighbor, and pictures laid edge to edge show seams. A map tile carries a one-pixel rim copied from its own edge and draws one of its pixels larger each side, clipped to its box (`maps::tile`). diff --git a/src/editor/find.rs b/src/editor/find.rs index 08a38fb..e783f03 100644 --- a/src/editor/find.rs +++ b/src/editor/find.rs @@ -149,6 +149,26 @@ impl FindWidget { } } +/// The count shown, and whether it reads as a miss. +pub(crate) fn count( + matches: &Result<(Option, usize), SharedString>, + cx: &App, +) -> (SharedString, bool) { + match matches { + Ok((_, 0)) => (i18n::text(cx, "find.none", &[]), true), + Ok((Some(current), total)) => { + let (at, total) = ((current + 1).to_string(), total.to_string()); + let args = [("at", at.as_str()), ("total", total.as_str())]; + (i18n::text(cx, "find.at", &args), false) + } + Ok((None, total)) => { + let total = total.to_string(); + (i18n::text(cx, "find.found", &[("total", &total)]), false) + } + Err(message) => (message.clone(), true), + } +} + impl RenderOnce for FindWidget { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); @@ -156,7 +176,7 @@ impl RenderOnce for FindWidget { let options = self.options; let toggle = |key: &'static str, icon: IconName, - words: &'static str, + words: SharedString, on: bool, set: fn(&mut FindOptions, bool)| { let on_options = self.on_options.clone(); @@ -175,16 +195,11 @@ impl RenderOnce for FindWidget { } }) }; - let (count, missing) = match &self.matches { - Ok((_, 0)) => ("No results".to_string(), true), - Ok((Some(current), total)) => (format!("{} of {total}", current + 1), false), - Ok((None, total)) => (format!("{total} found"), false), - Err(message) => (message.to_string(), true), - }; + let (count, missing) = count(&self.matches, cx); let found = matches!(self.matches, Ok((_, total)) if total > 0); let icon_button = |key: &'static str, icon: IconName, - words: &'static str, + words: SharedString, enabled: bool, action: Option, value: bool| { @@ -230,7 +245,7 @@ impl RenderOnce for FindWidget { } else { IconName::ChevronRight }, - "Toggle replace", + i18n::text(cx, "find.fold", &[]), true, Some(fold), !showing, @@ -246,21 +261,21 @@ impl RenderOnce for FindWidget { row.child(toggle( "case", IconName::CaseSensitive, - "Match case", + i18n::text(cx, "find.case", &[]), options.case, |next, on| next.case = on, )) .child(toggle( "word", IconName::WholeWord, - "Whole word", + i18n::text(cx, "find.word", &[]), options.word, |next, on| next.word = on, )) .child(toggle( "regex", IconName::Regex, - "Regular expression", + i18n::text(cx, "find.regex", &[]), options.regex, |next, on| next.regex = on, )) @@ -279,7 +294,7 @@ impl RenderOnce for FindWidget { .child(icon_button( "previous", IconName::ArrowUp, - "Previous match", + i18n::text(cx, "find.previous", &[]), found, self.on_step.clone(), false, @@ -287,7 +302,7 @@ impl RenderOnce for FindWidget { .child(icon_button( "next", IconName::ArrowDown, - "Next match", + i18n::text(cx, "find.next", &[]), found, self.on_step.clone(), true, @@ -313,22 +328,28 @@ impl RenderOnce for FindWidget { .child(Input::new(field).size(ControlSize::Sm)), ) .child( - Button::new((self.id.clone(), "replace"), "Replace") - .variant(ButtonVariant::Ghost) - .size(ControlSize::Sm) - .disabled(!found) - .when_some(one, |button, one| { - button.on_click(move |_, window, cx| one(false, window, cx)) - }), + Button::new( + (self.id.clone(), "replace"), + i18n::text(cx, "find.replace", &[]), + ) + .variant(ButtonVariant::Ghost) + .size(ControlSize::Sm) + .disabled(!found) + .when_some(one, |button, one| { + button.on_click(move |_, window, cx| one(false, window, cx)) + }), ) .child( - Button::new((self.id.clone(), "all"), "Replace all") - .variant(ButtonVariant::Ghost) - .size(ControlSize::Sm) - .disabled(!found) - .when_some(all, |button, all| { - button.on_click(move |_, window, cx| all(true, window, cx)) - }), + Button::new( + (self.id.clone(), "all"), + i18n::text(cx, "find.replace_all", &[]), + ) + .variant(ButtonVariant::Ghost) + .size(ControlSize::Sm) + .disabled(!found) + .when_some(all, |button, all| { + button.on_click(move |_, window, cx| all(true, window, cx)) + }), ) })) } diff --git a/src/editor/tests/chinese.rs b/src/editor/tests/chinese.rs new file mode 100644 index 0000000..a399655 --- /dev/null +++ b/src/editor/tests/chinese.rs @@ -0,0 +1,40 @@ +//! The find box reads in Chinese under zh-CN. + +use gpui::{Entity, IntoElement, Render, TestAppContext, Window, div, prelude::*}; + +use crate::{forms::TextInput, i18n::I18n, theme::Theme}; + +struct Host(Entity, Entity); + +impl Render for Host { + fn render(&mut self, _: &mut Window, _: &mut gpui::Context) -> impl IntoElement { + let find = super::super::FindWidget::new("find", &self.0, Some(0), 2) + .replace(&self.1) + .on_options(|_, _, _| {}) + .on_step(|_, _, _| {}) + .on_toggle_replace(|_, _, _| {}) + .on_close(|_, _| {}); + div().size_full().child(find) + } +} + +#[gpui::test] +fn the_find_box_reads_in_chinese(cx: &mut TestAppContext) { + cx.update(|cx| { + Theme::init(cx); + cx.set_global(I18n::new("zh-CN").catalog("zh-CN", &[])); + }); + cx.update(|cx| { + let count = |matches| super::super::find::count(&matches, cx); + assert_eq!(count(Ok((None, 0))), ("无结果".into(), true)); + assert_eq!(count(Ok((Some(0), 2))), ("第 1 项,共 2 项".into(), false)); + assert_eq!(count(Ok((None, 3))), ("找到 3 项".into(), false)); + }); + let (_, cx) = cx.add_window_view(|window, cx| { + Host( + cx.new(|cx| TextInput::new(window, cx)), + cx.new(|cx| TextInput::new(window, cx)), + ) + }); + cx.run_until_parked(); +} diff --git a/src/editor/tests/mod.rs b/src/editor/tests/mod.rs index 9d0dabe..6f285c0 100644 --- a/src/editor/tests/mod.rs +++ b/src/editor/tests/mod.rs @@ -1,3 +1,4 @@ +mod chinese; mod history; mod stale; diff --git a/src/forms/input.rs b/src/forms/input.rs index c365f2f..de97e49 100644 --- a/src/forms/input.rs +++ b/src/forms/input.rs @@ -199,11 +199,15 @@ impl RenderOnce for PasswordInput { }, ) .size(ControlSize::Sm) - .tooltip(if masked { - "Show password" - } else { - "Hide password" - }) + .tooltip(i18n::text( + cx, + if masked { + "input.show_password" + } else { + "input.hide_password" + }, + &[], + )) .on_click(move |_, _, cx| { toggle.update(cx, |input, cx| { let masked = input.is_masked(); diff --git a/src/i18n/builtin.rs b/src/i18n/builtin.rs index cc8d346..71a2798 100644 --- a/src/i18n/builtin.rs +++ b/src/i18n/builtin.rs @@ -117,9 +117,34 @@ const EN: &[(&str, &str)] = &[ ("theme.system", "System"), ("select.placeholder", "Choose…"), ("input.clear", "Clear"), + ("input.show_password", "Show password"), + ("input.hide_password", "Hide password"), ("completion.empty", "No suggestions"), ("builder.remove", "Remove"), ("tabs.unpin", "Unpin"), + ("find.none", "No results"), + ("find.at", "{at} of {total}"), + ("find.found", "{total} found"), + ("find.fold", "Toggle replace"), + ("find.case", "Match case"), + ("find.word", "Whole word"), + ("find.regex", "Regular expression"), + ("find.previous", "Previous match"), + ("find.next", "Next match"), + ("find.replace", "Replace"), + ("find.replace_all", "Replace all"), + ("builder.add_rule", "Add rule"), + ("builder.all", "Match all"), + ("builder.any", "Match any"), + ("builder.add_sort", "Add sort"), + ("builder.up", "Ascending"), + ("builder.down", "Descending"), + ("builder.test.contains", "contains"), + ("builder.test.equals", "is"), + ("builder.test.not_equals", "is not"), + ("builder.test.above", "is above"), + ("builder.test.below", "is below"), + ("builder.test.empty", "is empty"), ("theme.size.reset", "Back to the default size"), ( "theme.size.sample", @@ -240,9 +265,34 @@ const ZH_CN: &[(&str, &str)] = &[ ("theme.system", "跟随系统"), ("select.placeholder", "请选择…"), ("input.clear", "清除"), + ("input.show_password", "显示密码"), + ("input.hide_password", "隐藏密码"), ("completion.empty", "没有建议"), ("builder.remove", "移除"), ("tabs.unpin", "取消固定"), + ("find.none", "无结果"), + ("find.at", "第 {at} 项,共 {total} 项"), + ("find.found", "找到 {total} 项"), + ("find.fold", "切换替换"), + ("find.case", "区分大小写"), + ("find.word", "全字匹配"), + ("find.regex", "使用正则表达式"), + ("find.previous", "上一个匹配项"), + ("find.next", "下一个匹配项"), + ("find.replace", "替换"), + ("find.replace_all", "全部替换"), + ("builder.add_rule", "添加规则"), + ("builder.all", "满足全部"), + ("builder.any", "满足任一"), + ("builder.add_sort", "添加排序"), + ("builder.up", "升序"), + ("builder.down", "降序"), + ("builder.test.contains", "包含"), + ("builder.test.equals", "等于"), + ("builder.test.not_equals", "不等于"), + ("builder.test.above", "大于"), + ("builder.test.below", "小于"), + ("builder.test.empty", "为空"), ("theme.size.reset", "恢复默认大小"), ( "theme.size.sample", diff --git a/src/i18n/tests/shipped.rs b/src/i18n/tests/shipped.rs index 81d40f0..437014e 100644 --- a/src/i18n/tests/shipped.rs +++ b/src/i18n/tests/shipped.rs @@ -10,6 +10,7 @@ const SHIPPED: &[(&str, &str)] = &[ "tables/builders.rs", include_str!("../../tables/builders.rs"), ), + ("tables/rules.rs", include_str!("../../tables/rules.rs")), ( "navigation/editor_tabs.rs", include_str!("../../navigation/editor_tabs.rs"), @@ -22,17 +23,46 @@ const SHIPPED: &[(&str, &str)] = &[ #[test] fn shipped_components_show_no_fixed_english() { - let shown = [".tooltip(\"", ".child(\"", "SharedString::from(\""]; + let shown = [".tooltip(\"", ".child(\"", "SharedString::from(\"", "=> \""]; + // After an id: words, not a lowercase id. + let labels = ["), \"", "\", \"", "(\""]; let mut found = Vec::new(); for (path, source) in SHIPPED { let code = source.split("#[cfg(test)]").next().unwrap_or(source); for (no, line) in code.lines().enumerate() { - for call in shown { - let Some(at) = line.find(call) else { continue }; - let rest = &line[at + call.len()..]; - if rest.starts_with(|ch: char| ch.is_ascii_alphabetic()) { - found.push(format!("{path}:{}: {}", no + 1, line.trim())); - } + let checks = ["assert", "panic!", "log::", ".expect("]; + if checks.iter().any(|check| line.contains(check)) { + continue; + } + let literals = |calls: &[&str]| -> Vec<&str> { + (calls.iter()) + .flat_map(|call| { + line.match_indices(call) + .map(|(at, _)| &line[at + call.len()..]) + }) + .map(|rest| rest.split('"').next().unwrap_or(rest)) + .filter(|literal| literal.starts_with(|ch: char| ch.is_ascii_alphabetic())) + .collect() + }; + let key = |literal: &str| literal.contains('.') && !literal.contains(' '); + let words = + |literal: &str| literal.contains(' ') || literal.starts_with(char::is_uppercase); + let english = (literals(&shown).into_iter().filter(|literal| !key(literal))) + .chain( + literals(&labels) + .into_iter() + .filter(|literal| words(literal)), + ) + .chain( + line.trim() + .strip_prefix('"') + .and_then(|rest| rest.split('"').next()), + ) + .filter(|literal| { + literal.starts_with(char::is_uppercase) || !line.trim().starts_with('"') + }); + if english.count() > 0 { + found.push(format!("{path}:{}: {}", no + 1, line.trim())); } } } diff --git a/src/tables/builders.rs b/src/tables/builders.rs index 3e7a0d2..e969164 100644 --- a/src/tables/builders.rs +++ b/src/tables/builders.rs @@ -152,7 +152,7 @@ impl RenderOnce for FilterBuilder { let emit = Rc::new(emit); let tests: Vec = Test::ALL .iter() - .map(|test| Choice::new(test.label(), test.label())) + .map(|test| Choice::new(test.key(), i18n::text(cx, test.key(), &[]))) .collect(); let theme = cx.theme(); let (rules, any, id) = (self.rules.clone(), self.any, self.id.clone()); @@ -187,13 +187,13 @@ impl RenderOnce for FilterBuilder { .child( div().w(theme.label_width() * 0.75).child( Select::new((id.clone(), format!("test-{ix}")), tests.clone()) - .selected(rule.test.label()) + .selected(rule.test.key()) .size(ControlSize::Sm) .on_change(move |label, window, cx| { let mut next = for_test.clone(); next[ix].test = *Test::ALL .iter() - .find(|test| test.label() == label.as_ref()) + .find(|test| test.key() == label.as_ref()) .expect("a listed test"); test(next, any, window, cx) }), @@ -224,18 +224,21 @@ impl RenderOnce for FilterBuilder { .items_center() .justify_between() .child( - Button::new((self.id.clone(), "add"), "Add rule") - .icon(IconName::Plus) - .variant(ButtonVariant::Ghost) - .size(ControlSize::Sm) - .on_click(move |_, window, cx| { - let mut next = to_add.clone(); - next.push(FilterRule { - column: first.clone(), - ..FilterRule::default() - }); - add(next, any, window, cx) - }), + Button::new( + (self.id.clone(), "add"), + i18n::text(cx, "builder.add_rule", &[]), + ) + .icon(IconName::Plus) + .variant(ButtonVariant::Ghost) + .size(ControlSize::Sm) + .on_click(move |_, window, cx| { + let mut next = to_add.clone(); + next.push(FilterRule { + column: first.clone(), + ..FilterRule::default() + }); + add(next, any, window, cx) + }), ) .when(self.rules.len() > 1, |footer| { footer.child( @@ -243,8 +246,8 @@ impl RenderOnce for FilterBuilder { (self.id.clone(), "join"), if any { "any" } else { "all" }, ) - .segment("all", "Match all", None) - .segment("any", "Match any", None) + .segment("all", i18n::text(cx, "builder.all", &[]), None) + .segment("any", i18n::text(cx, "builder.any", &[]), None) .size(ControlSize::Sm) .on_change(move |value, window, cx| { join(to_join.clone(), value.as_ref() == "any", window, cx) @@ -365,8 +368,16 @@ impl RenderOnce for SortBuilder { (id.clone(), format!("sort-way-{ix}")), if sort.rising { "up" } else { "down" }, ) - .segment("up", "Ascending", Some(IconName::ArrowUp)) - .segment("down", "Descending", Some(IconName::ArrowDown)) + .segment( + "up", + i18n::text(cx, "builder.up", &[]), + Some(IconName::ArrowUp), + ) + .segment( + "down", + i18n::text(cx, "builder.down", &[]), + Some(IconName::ArrowDown), + ) .size(ControlSize::Sm) .on_change(move |way, window, cx| { let mut next = for_flip.clone(); @@ -395,18 +406,21 @@ impl RenderOnce for SortBuilder { .gap_1() .child(list) .children(unused.map(|column| { - Button::new((self.id.clone(), "add"), "Add sort") - .icon(IconName::Plus) - .variant(ButtonVariant::Ghost) - .size(ControlSize::Sm) - .on_click(move |_, window, cx| { - let mut next = add.1.clone(); - next.push(SortKey { - column: column.clone(), - rising: true, - }); - (add.0)(&next, window, cx) - }) + Button::new( + (self.id.clone(), "add"), + i18n::text(cx, "builder.add_sort", &[]), + ) + .icon(IconName::Plus) + .variant(ButtonVariant::Ghost) + .size(ControlSize::Sm) + .on_click(move |_, window, cx| { + let mut next = add.1.clone(); + next.push(SortKey { + column: column.clone(), + rising: true, + }); + (add.0)(&next, window, cx) + }) })) } } diff --git a/src/tables/rules.rs b/src/tables/rules.rs index 584e7e7..8adea6c 100644 --- a/src/tables/rules.rs +++ b/src/tables/rules.rs @@ -26,14 +26,15 @@ impl Test { Test::Empty, ]; - pub fn label(self) -> &'static str { + /// Its catalog key, which also names it. + pub fn key(self) -> &'static str { match self { - Self::Contains => "contains", - Self::Equals => "is", - Self::NotEquals => "is not", - Self::Above => "is above", - Self::Below => "is below", - Self::Empty => "is empty", + Self::Contains => "builder.test.contains", + Self::Equals => "builder.test.equals", + Self::NotEquals => "builder.test.not_equals", + Self::Above => "builder.test.above", + Self::Below => "builder.test.below", + Self::Empty => "builder.test.empty", } } diff --git a/src/tables/tests/chinese.rs b/src/tables/tests/chinese.rs new file mode 100644 index 0000000..02b6638 --- /dev/null +++ b/src/tables/tests/chinese.rs @@ -0,0 +1,62 @@ +//! The sort and filter builders read in Chinese under zh-CN. + +use gpui::{IntoElement, Render, TestAppContext, Window, div, prelude::*}; + +use crate::{ + i18n::{self, I18n}, + tables::{FilterBuilder, FilterRule, SortBuilder, SortKey, Test}, + theme::Theme, +}; + +struct Builders; + +impl Render for Builders { + fn render(&mut self, _: &mut Window, _: &mut gpui::Context) -> impl IntoElement { + let columns = [("a", "第一列"), ("b", "第二列")]; + let rule = FilterRule { + column: "a".into(), + ..FilterRule::default() + }; + let key = SortKey { + column: "a".into(), + rising: true, + }; + div() + .size_full() + .child(FilterBuilder::new("filter", columns).rules([rule.clone(), rule], true)) + .child(SortBuilder::new("sort", columns).keys([key])) + } +} + +#[gpui::test] +fn the_builders_read_in_chinese(cx: &mut TestAppContext) { + cx.update(|cx| { + Theme::init(cx); + cx.set_global(I18n::new("zh-CN").catalog("zh-CN", &[])); + }); + cx.update(|cx| { + for test in Test::ALL { + let shown = i18n::text(cx, test.key(), &[]); + assert!( + !shown.chars().any(|ch| ch.is_ascii_alphabetic()), + "{test:?}: {shown}" + ); + } + for key in [ + "builder.add_rule", + "builder.all", + "builder.any", + "builder.add_sort", + "builder.up", + "builder.down", + ] { + let shown = i18n::text(cx, key, &[]); + assert!( + !shown.chars().any(|ch| ch.is_ascii_alphabetic()), + "{key}: {shown}" + ); + } + }); + let (_, cx) = cx.add_window_view(|_, _| Builders); + cx.run_until_parked(); +} diff --git a/src/tables/tests/mod.rs b/src/tables/tests/mod.rs index 0fff119..4e8e6b9 100644 --- a/src/tables/tests/mod.rs +++ b/src/tables/tests/mod.rs @@ -9,6 +9,7 @@ use gpui::{ Styled, TestAppContext, VisualTestContext, Window, div, point, px, }; +mod chinese; mod clipboard; mod columns; mod stale;