code-editor: Improve CodeEditor to has default themes. (#900)

This commit is contained in:
Jason Lee 2025-05-26 18:17:15 +08:00 committed by GitHub
parent ebe10e20c7
commit 1cbbf6ef18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 94 additions and 91 deletions

View file

@ -5,7 +5,7 @@ use gpui_component::{
h_flex, h_flex,
highlighter::{HighlightTheme, Highlighter}, highlighter::{HighlightTheme, Highlighter},
input::{InputEvent, InputState, TabSize, TextInput}, input::{InputEvent, InputState, TabSize, TextInput},
v_flex, ActiveTheme as _, v_flex,
}; };
use story::Assets; use story::Assets;
@ -13,7 +13,6 @@ pub struct Example {
input_state: Entity<InputState>, input_state: Entity<InputState>,
language_state: Entity<DropdownState<Vec<SharedString>>>, language_state: Entity<DropdownState<Vec<SharedString>>>,
language: SharedString, language: SharedString,
is_dark: bool,
line_number: bool, line_number: bool,
_subscribes: Vec<Subscription>, _subscribes: Vec<Subscription>,
} }
@ -26,7 +25,7 @@ impl Example {
let default_language: SharedString = LANGUAGES[0].into(); let default_language: SharedString = LANGUAGES[0].into();
let input_state = cx.new(|cx| { let input_state = cx.new(|cx| {
InputState::new(window, cx) InputState::new(window, cx)
.code_editor(Some(&default_language), &HighlightTheme::default_light()) .code_editor(Some(&default_language))
.line_number(true) .line_number(true)
.tab_size(TabSize { .tab_size(TabSize {
tab_size: 4, tab_size: 4,
@ -63,7 +62,6 @@ impl Example {
input_state, input_state,
language_state, language_state,
language: default_language, language: default_language,
is_dark: false,
line_number: true, line_number: true,
_subscribes, _subscribes,
} }
@ -74,26 +72,21 @@ impl Example {
} }
fn update_highlighter(&mut self, new_language: Option<SharedString>, cx: &mut Context<Self>) { fn update_highlighter(&mut self, new_language: Option<SharedString>, cx: &mut Context<Self>) {
let is_dark = cx.theme().mode.is_dark();
let is_language_changed = new_language.is_some(); let is_language_changed = new_language.is_some();
if new_language.is_some() { if new_language.is_some() {
self.language = new_language.unwrap(); self.language = new_language.unwrap();
} }
let language = self.language.as_ref(); let language = self.language.as_ref();
if self.is_dark != is_dark || is_language_changed { if is_language_changed {
self.is_dark = is_dark;
self.input_state.update(cx, |state, cx| { self.input_state.update(cx, |state, cx| {
if is_dark { state.set_highlighter(
state.set_highlighter( Highlighter::new(
Highlighter::new(Some(language), &HighlightTheme::default_dark()), Some(language),
cx, &HighlightTheme::default_light(),
); &HighlightTheme::default_dark(),
} else { ),
state.set_highlighter( cx,
Highlighter::new(Some(language), &HighlightTheme::default_dark()), );
cx,
);
}
}); });
} }
} }

View file

@ -1,17 +1,14 @@
use gpui::*; use gpui::*;
use gpui_component::{ use gpui_component::{
highlighter::{HighlightTheme, Highlighter},
input::{InputState, TabSize, TextInput}, input::{InputState, TabSize, TextInput},
resizable::{h_resizable, resizable_panel, ResizableState}, resizable::{h_resizable, resizable_panel, ResizableState},
text::TextView, text::TextView,
ActiveTheme as _,
}; };
use story::Assets; use story::Assets;
pub struct Example { pub struct Example {
input_state: Entity<InputState>, input_state: Entity<InputState>,
resizable_state: Entity<ResizableState>, resizable_state: Entity<ResizableState>,
is_dark: bool,
_subscribe: Subscription, _subscribe: Subscription,
} }
@ -22,7 +19,7 @@ impl Example {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let input_state = cx.new(|cx| { let input_state = cx.new(|cx| {
InputState::new(window, cx) InputState::new(window, cx)
.code_editor(Some(LANG), &HighlightTheme::default_light()) .code_editor(Some(LANG))
.tab_size(TabSize { .tab_size(TabSize {
tab_size: 4, tab_size: 4,
hard_tabs: false, hard_tabs: false,
@ -43,7 +40,6 @@ impl Example {
Self { Self {
input_state, input_state,
resizable_state, resizable_state,
is_dark: false,
_subscribe, _subscribe,
} }
} }
@ -55,24 +51,6 @@ impl Example {
impl Render for Example { impl Render for Example {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_dark = cx.theme().mode.is_dark();
if self.is_dark != is_dark {
self.is_dark = is_dark;
self.input_state.update(cx, |state, cx| {
if is_dark {
state.set_highlighter(
Highlighter::new(Some(LANG), &HighlightTheme::default_dark()),
cx,
);
} else {
state.set_highlighter(
Highlighter::new(Some(LANG), &HighlightTheme::default_light()),
cx,
);
}
});
}
h_resizable("container", self.resizable_state.clone()) h_resizable("container", self.resizable_state.clone())
.child( .child(
resizable_panel().child( resizable_panel().child(

View file

@ -2,7 +2,7 @@ use std::rc::Rc;
use gpui::*; use gpui::*;
use gpui_component::{ use gpui_component::{
highlighter::{HighlightTheme, Highlighter}, highlighter::HighlightTheme,
input::{InputEvent, InputState, TabSize, TextInput}, input::{InputEvent, InputState, TabSize, TextInput},
resizable::{h_resizable, resizable_panel, ResizableState}, resizable::{h_resizable, resizable_panel, ResizableState},
text::{TextView, TextViewStyle}, text::{TextView, TextViewStyle},
@ -15,7 +15,6 @@ const LANG: &str = "markdown";
pub struct Example { pub struct Example {
input_state: Entity<InputState>, input_state: Entity<InputState>,
resizable_state: Entity<ResizableState>, resizable_state: Entity<ResizableState>,
is_dark: bool,
} }
const EXAMPLE: &str = include_str!("./markdown.md"); const EXAMPLE: &str = include_str!("./markdown.md");
@ -24,7 +23,7 @@ impl Example {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let input_state = cx.new(|cx| { let input_state = cx.new(|cx| {
InputState::new(window, cx) InputState::new(window, cx)
.code_editor(Some(LANG), &HighlightTheme::default_light()) .code_editor(Some(LANG))
.line_number(false) .line_number(false)
.tab_size(TabSize { .tab_size(TabSize {
tab_size: 2, tab_size: 2,
@ -42,7 +41,6 @@ impl Example {
Self { Self {
resizable_state, resizable_state,
input_state, input_state,
is_dark: false,
} }
} }
@ -60,22 +58,6 @@ impl Render for Example {
}; };
let is_dark = cx.theme().mode.is_dark(); let is_dark = cx.theme().mode.is_dark();
if self.is_dark != is_dark {
self.is_dark = is_dark;
self.input_state.update(cx, |state, cx| {
if is_dark {
state.set_highlighter(
Highlighter::new(Some(LANG), &HighlightTheme::default_dark()),
cx,
);
} else {
state.set_highlighter(
Highlighter::new(Some(LANG), &HighlightTheme::default_light()),
cx,
);
}
});
}
h_resizable("container", self.resizable_state.clone()) h_resizable("container", self.resizable_state.clone())
.child( .child(
@ -97,6 +79,7 @@ impl Render for Example {
TextView::markdown("preview", self.input_state.read(cx).value()).style( TextView::markdown("preview", self.input_state.read(cx).value()).style(
TextViewStyle { TextViewStyle {
highlight_theme: Rc::new(theme.clone()), highlight_theme: Rc::new(theme.clone()),
is_dark,
..Default::default() ..Default::default()
}, },
), ),

View file

@ -66,26 +66,51 @@ impl HighlightTheme {
/// https://github.com/iced-rs/iced/blob/master/highlighter/src/lib.rs#L24 /// https://github.com/iced-rs/iced/blob/master/highlighter/src/lib.rs#L24
pub struct Highlighter<'a> { pub struct Highlighter<'a> {
syntax: &'static parsing::SyntaxReference, syntax: &'static parsing::SyntaxReference,
pub(crate) theme: &'a HighlightTheme, pub(crate) light_theme: &'a HighlightTheme,
highlighter: highlighting::Highlighter<'a>, pub(crate) dark_theme: &'a HighlightTheme,
light_highlighter: highlighting::Highlighter<'a>,
dark_highlighter: highlighting::Highlighter<'a>,
} }
impl<'a> Highlighter<'a> { impl<'a> Highlighter<'a> {
pub fn new(lang: Option<&str>, theme: &'a HighlightTheme) -> Self { pub fn new(
lang: Option<&str>,
light_theme: &'a HighlightTheme,
dark_theme: &'a HighlightTheme,
) -> Self {
let syntax = lang let syntax = lang
.and_then(|lang| SYNTAXES.find_syntax_by_token(&lang)) .and_then(|lang| SYNTAXES.find_syntax_by_token(&lang))
.unwrap_or_else(|| SYNTAXES.find_syntax_plain_text()); .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text());
let highlighter = highlighting::Highlighter::new(&theme.inner); let light_highlighter = highlighting::Highlighter::new(&light_theme.inner);
let dark_highlighter = highlighting::Highlighter::new(&dark_theme.inner);
Self { Self {
syntax, syntax,
theme, light_theme,
highlighter, dark_theme,
light_highlighter,
dark_highlighter,
}
}
pub(crate) fn theme(&self, is_dark: bool) -> &HighlightTheme {
if is_dark {
self.dark_theme
} else {
self.light_theme
}
}
fn highlighter(&self, is_dark: bool) -> &highlighting::Highlighter<'a> {
if is_dark {
&self.dark_highlighter
} else {
&self.light_highlighter
} }
} }
/// Highlight a line and returns a vector of ranges and highlight styles /// Highlight a line and returns a vector of ranges and highlight styles
pub fn highlight(&self, line: &str) -> Vec<(Range<usize>, HighlightStyle)> { pub fn highlight(&self, line: &str, is_dark: bool) -> Vec<(Range<usize>, HighlightStyle)> {
let mut parser = parsing::ParseState::new(self.syntax); let mut parser = parsing::ParseState::new(self.syntax);
let mut stack = parsing::ScopeStack::new(); let mut stack = parsing::ScopeStack::new();
@ -102,7 +127,7 @@ impl<'a> Highlighter<'a> {
if range.is_empty() { if range.is_empty() {
return None; return None;
} else { } else {
let style_mod = self.highlighter.style_mod_for_stack(&stack.scopes); let style_mod = self.highlighter(is_dark).style_mod_for_stack(&stack.scopes);
let mut style = HighlightStyle::default(); let mut style = HighlightStyle::default();
style.color = style_mod.foreground.map(color_to_hsla); style.color = style_mod.foreground.map(color_to_hsla);
style.background_color = style_mod.background.map(color_to_hsla); style.background_color = style_mod.background.map(color_to_hsla);

View file

@ -2,7 +2,7 @@ use std::{collections::HashMap, ops::Range, rc::Rc};
use gpui::{App, HighlightStyle, SharedString, TextRun, TextStyle}; use gpui::{App, HighlightStyle, SharedString, TextRun, TextStyle};
use crate::highlighter::Highlighter; use crate::{highlighter::Highlighter, ActiveTheme, ThemeMode};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct LineHighlightStyle { pub(crate) struct LineHighlightStyle {
@ -44,6 +44,7 @@ pub(super) struct CodeHighlighter {
pub(super) text: SharedString, pub(super) text: SharedString,
/// The lines by split \n /// The lines by split \n
pub(super) lines: Vec<LineHighlightStyle>, pub(super) lines: Vec<LineHighlightStyle>,
pub(super) cache_theme_mode: ThemeMode,
pub(super) cache: HashMap<u64, LineHighlightStyle>, pub(super) cache: HashMap<u64, LineHighlightStyle>,
} }
@ -53,6 +54,7 @@ impl CodeHighlighter {
highlighter, highlighter,
text: SharedString::default(), text: SharedString::default(),
lines: vec![], lines: vec![],
cache_theme_mode: ThemeMode::default(),
cache: HashMap::new(), cache: HashMap::new(),
} }
} }
@ -64,11 +66,16 @@ impl CodeHighlighter {
self.update(self.text.clone(), true, cx); self.update(self.text.clone(), true, cx);
} }
pub fn update(&mut self, text: SharedString, force: bool, _: &mut App) { pub fn update(&mut self, text: SharedString, force: bool, cx: &mut App) {
if self.text == text && !force { if self.text == text && self.cache_theme_mode == cx.theme().mode && !force {
return; return;
} }
// Clear if mode is changed
if self.cache_theme_mode != cx.theme().mode {
self.cache.clear();
}
let mut lines = vec![]; let mut lines = vec![];
let mut offset = 0; let mut offset = 0;
let mut new_cache = HashMap::new(); let mut new_cache = HashMap::new();
@ -85,7 +92,7 @@ impl CodeHighlighter {
lines.push(new_style); lines.push(new_style);
} else { } else {
// cache miss // cache miss
let styles = Rc::new(self.highlighter.highlight(line)); let styles = Rc::new(self.highlighter.highlight(line, cx.theme().is_dark()));
let line_style = LineHighlightStyle { offset, styles }; let line_style = LineHighlightStyle { offset, styles };
new_cache.insert(cache_key, line_style.clone()); new_cache.insert(cache_key, line_style.clone());
lines.push(line_style); lines.push(line_style);
@ -96,6 +103,7 @@ impl CodeHighlighter {
} }
// Ensure to recreate cache to remove unused caches. // Ensure to recreate cache to remove unused caches.
self.cache_theme_mode = cx.theme().mode;
self.cache = new_cache; self.cache = new_cache;
self.lines = lines; self.lines = lines;
self.text = text; self.text = text;

View file

@ -709,7 +709,7 @@ impl Element for TextElement {
.read(cx) .read(cx)
.mode .mode
.highlighter() .highlighter()
.and_then(|h| h.theme.settings().line_highlight) .and_then(|h| h.theme(cx.theme().is_dark()).settings().line_highlight)
.map(crate::highlighter::color_to_hsla) .map(crate::highlighter::color_to_hsla)
{ {
window.paint_quad(fill( window.paint_quad(fill(

View file

@ -359,6 +359,8 @@ impl InputState {
/// - hard_tabs: false /// - hard_tabs: false
/// - height: full /// - height: full
/// ///
/// If `highlighter` is None, will use the default highlighter.
///
/// Code Editor aim for help used to simple code editing or display, not a full-featured code editor. /// Code Editor aim for help used to simple code editing or display, not a full-featured code editor.
/// ///
/// ## Features /// ## Features
@ -366,12 +368,15 @@ impl InputState {
/// - Syntax Highlighting /// - Syntax Highlighting
/// - Auto Indent /// - Auto Indent
/// - Line Number /// - Line Number
pub fn code_editor(mut self, language: Option<&str>, theme: &'static HighlightTheme) -> Self { pub fn code_editor(mut self, language: Option<&str>) -> Self {
let highlighter = Rc::new(Highlighter::new(language, theme));
self.mode = InputMode::CodeEditor { self.mode = InputMode::CodeEditor {
rows: 2, rows: 2,
tab: TabSize::default(), tab: TabSize::default(),
highlighter: CodeHighlighter::new(highlighter), highlighter: CodeHighlighter::new(Rc::new(Highlighter::new(
language,
&HighlightTheme::default_light(),
&HighlightTheme::default_dark(),
))),
line_number: true, line_number: true,
height: Some(relative(1.)), height: Some(relative(1.)),
}; };

View file

@ -27,6 +27,7 @@ pub struct TextInput {
cleanable: bool, cleanable: bool,
mask_toggle: bool, mask_toggle: bool,
disabled: bool, disabled: bool,
bordered: bool,
} }
impl Sizable for TextInput { impl Sizable for TextInput {
@ -47,6 +48,7 @@ impl TextInput {
suffix: None, suffix: None,
height: None, height: None,
appearance: true, appearance: true,
bordered: true,
cleanable: false, cleanable: false,
mask_toggle: false, mask_toggle: false,
disabled: false, disabled: false,
@ -81,6 +83,12 @@ impl TextInput {
self self
} }
/// Set the bordered for the input field, default: true
pub fn bordered(mut self, bordered: bool) -> Self {
self.bordered = bordered;
self
}
/// Set true to show the clear button when the input field is not empty. /// Set true to show the clear button when the input field is not empty.
pub fn cleanable(mut self) -> Self { pub fn cleanable(mut self) -> Self {
self.cleanable = true; self.cleanable = true;
@ -234,11 +242,13 @@ impl RenderOnce for TextInput {
}) })
.when(self.appearance, |this| { .when(self.appearance, |this| {
this.bg(bg) this.bg(bg)
.border_color(cx.theme().input)
.border_1()
.rounded(cx.theme().radius) .rounded(cx.theme().radius)
.when(cx.theme().shadow, |this| this.shadow_sm()) .when(self.bordered, |this| {
.when(focused, |this| this.focused_border(cx)) this.border_color(cx.theme().input)
.border_1()
.when(cx.theme().shadow, |this| this.shadow_sm())
.when(focused, |this| this.focused_border(cx))
})
}) })
.when(prefix.is_none(), |this| this.input_pl(self.size)) .when(prefix.is_none(), |this| this.input_pl(self.size))
.input_pr(self.size) .input_pr(self.size)

View file

@ -11,7 +11,6 @@ use crate::{
clipboard::Clipboard, clipboard::Clipboard,
description_list::DescriptionList, description_list::DescriptionList,
h_flex, h_flex,
highlighter::HighlightTheme,
input::{InputState, TextInput}, input::{InputState, TextInput},
link::Link, link::Link,
v_flex, ActiveTheme, IconName, Selectable, Sizable, TITLE_BAR_HEIGHT, v_flex, ActiveTheme, IconName, Selectable, Sizable, TITLE_BAR_HEIGHT,
@ -60,15 +59,9 @@ pub struct DivInspector {
impl DivInspector { impl DivInspector {
pub fn new(window: &mut Window, cx: &mut App) -> Self { pub fn new(window: &mut Window, cx: &mut App) -> Self {
let theme = if cx.theme().is_dark() {
HighlightTheme::default_dark()
} else {
HighlightTheme::default_light()
};
let input_state = cx.new(|cx| { let input_state = cx.new(|cx| {
InputState::new(window, cx) InputState::new(window, cx)
.code_editor(Some("json"), theme) .code_editor(Some("json"))
.line_number(false) .line_number(false)
.disabled(true) .disabled(true)
}); });
@ -134,7 +127,9 @@ impl Render for DivInspector {
.w_full() .w_full()
.font_family("Monaco") .font_family("Monaco")
.text_size(px(12.)) .text_size(px(12.))
.child(TextInput::new(&input_state).h_full()), .border_1()
.border_color(cx.theme().border)
.child(TextInput::new(&input_state).h_full().appearance(false)),
), ),
) )
}, },
@ -150,6 +145,7 @@ fn render_inspector(
let inspector_element_id = inspector.active_element_id(); let inspector_element_id = inspector.active_element_id();
let source_location = let source_location =
inspector_element_id.map(|id| SharedString::new(format!("{}", id.path.source_location))); inspector_element_id.map(|id| SharedString::new(format!("{}", id.path.source_location)));
let element_global_id = inspector_element_id.map(|id| format!("{}", id.path.global_id));
v_flex() v_flex()
.id("inspector") .id("inspector")
@ -202,6 +198,7 @@ fn render_inspector(
.flex_1() .flex_1()
.p_3() .p_3()
.gap_3() .gap_3()
.text_sm()
.when_some(source_location, |this, source_location| { .when_some(source_location, |this, source_location| {
this.child( this.child(
h_flex() h_flex()
@ -215,6 +212,7 @@ fn render_inspector(
.child(Clipboard::new("copy-source-location").value(source_location)), .child(Clipboard::new("copy-source-location").value(source_location)),
) )
}) })
.children(element_global_id)
.children(inspector.render_inspector_states(window, cx)), .children(inspector.render_inspector_states(window, cx)),
) )
.into_any_element() .into_any_element()

View file

@ -215,8 +215,9 @@ impl CodeBlock {
let highlight = Highlighter::new( let highlight = Highlighter::new(
lang.as_ref().map(|v| v.as_ref()), lang.as_ref().map(|v| v.as_ref()),
text_view_style.highlight_theme.as_ref(), text_view_style.highlight_theme.as_ref(),
text_view_style.highlight_theme.as_ref(),
); );
let styles = highlight.highlight(code.as_ref()); let styles = highlight.highlight(code.as_ref(), text_view_style.is_dark);
Self { code, lang, styles } Self { code, lang, styles }
} }
} }

View file

@ -77,6 +77,7 @@ pub struct TextViewStyle {
pub heading_base_font_size: Pixels, pub heading_base_font_size: Pixels,
/// Highlight theme for code blocks. Default: [`HighlightTheme::default_light()`] /// Highlight theme for code blocks. Default: [`HighlightTheme::default_light()`]
pub highlight_theme: Rc<HighlightTheme>, pub highlight_theme: Rc<HighlightTheme>,
pub is_dark: bool,
} }
impl Default for TextViewStyle { impl Default for TextViewStyle {
@ -85,6 +86,7 @@ impl Default for TextViewStyle {
paragraph_gap: rems(1.), paragraph_gap: rems(1.),
heading_base_font_size: px(14.), heading_base_font_size: px(14.),
highlight_theme: Rc::new(HighlightTheme::default_light().clone()), highlight_theme: Rc::new(HighlightTheme::default_light().clone()),
is_dark: false,
} }
} }
} }