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

View file

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

View file

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

View file

@ -66,26 +66,51 @@ impl HighlightTheme {
/// https://github.com/iced-rs/iced/blob/master/highlighter/src/lib.rs#L24
pub struct Highlighter<'a> {
syntax: &'static parsing::SyntaxReference,
pub(crate) theme: &'a HighlightTheme,
highlighter: highlighting::Highlighter<'a>,
pub(crate) light_theme: &'a HighlightTheme,
pub(crate) dark_theme: &'a HighlightTheme,
light_highlighter: highlighting::Highlighter<'a>,
dark_highlighter: highlighting::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
.and_then(|lang| SYNTAXES.find_syntax_by_token(&lang))
.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 {
syntax,
theme,
highlighter,
light_theme,
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
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 stack = parsing::ScopeStack::new();
@ -102,7 +127,7 @@ impl<'a> Highlighter<'a> {
if range.is_empty() {
return None;
} 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();
style.color = style_mod.foreground.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 crate::highlighter::Highlighter;
use crate::{highlighter::Highlighter, ActiveTheme, ThemeMode};
#[derive(Debug, Clone)]
pub(crate) struct LineHighlightStyle {
@ -44,6 +44,7 @@ pub(super) struct CodeHighlighter {
pub(super) text: SharedString,
/// The lines by split \n
pub(super) lines: Vec<LineHighlightStyle>,
pub(super) cache_theme_mode: ThemeMode,
pub(super) cache: HashMap<u64, LineHighlightStyle>,
}
@ -53,6 +54,7 @@ impl CodeHighlighter {
highlighter,
text: SharedString::default(),
lines: vec![],
cache_theme_mode: ThemeMode::default(),
cache: HashMap::new(),
}
}
@ -64,11 +66,16 @@ impl CodeHighlighter {
self.update(self.text.clone(), true, cx);
}
pub fn update(&mut self, text: SharedString, force: bool, _: &mut App) {
if self.text == text && !force {
pub fn update(&mut self, text: SharedString, force: bool, cx: &mut App) {
if self.text == text && self.cache_theme_mode == cx.theme().mode && !force {
return;
}
// Clear if mode is changed
if self.cache_theme_mode != cx.theme().mode {
self.cache.clear();
}
let mut lines = vec![];
let mut offset = 0;
let mut new_cache = HashMap::new();
@ -85,7 +92,7 @@ impl CodeHighlighter {
lines.push(new_style);
} else {
// 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 };
new_cache.insert(cache_key, line_style.clone());
lines.push(line_style);
@ -96,6 +103,7 @@ impl CodeHighlighter {
}
// Ensure to recreate cache to remove unused caches.
self.cache_theme_mode = cx.theme().mode;
self.cache = new_cache;
self.lines = lines;
self.text = text;

View file

@ -709,7 +709,7 @@ impl Element for TextElement {
.read(cx)
.mode
.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)
{
window.paint_quad(fill(

View file

@ -359,6 +359,8 @@ impl InputState {
/// - hard_tabs: false
/// - 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.
///
/// ## Features
@ -366,12 +368,15 @@ impl InputState {
/// - Syntax Highlighting
/// - Auto Indent
/// - Line Number
pub fn code_editor(mut self, language: Option<&str>, theme: &'static HighlightTheme) -> Self {
let highlighter = Rc::new(Highlighter::new(language, theme));
pub fn code_editor(mut self, language: Option<&str>) -> Self {
self.mode = InputMode::CodeEditor {
rows: 2,
tab: TabSize::default(),
highlighter: CodeHighlighter::new(highlighter),
highlighter: CodeHighlighter::new(Rc::new(Highlighter::new(
language,
&HighlightTheme::default_light(),
&HighlightTheme::default_dark(),
))),
line_number: true,
height: Some(relative(1.)),
};

View file

@ -27,6 +27,7 @@ pub struct TextInput {
cleanable: bool,
mask_toggle: bool,
disabled: bool,
bordered: bool,
}
impl Sizable for TextInput {
@ -47,6 +48,7 @@ impl TextInput {
suffix: None,
height: None,
appearance: true,
bordered: true,
cleanable: false,
mask_toggle: false,
disabled: false,
@ -81,6 +83,12 @@ impl TextInput {
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.
pub fn cleanable(mut self) -> Self {
self.cleanable = true;
@ -234,11 +242,13 @@ impl RenderOnce for TextInput {
})
.when(self.appearance, |this| {
this.bg(bg)
.border_color(cx.theme().input)
.border_1()
.rounded(cx.theme().radius)
.when(cx.theme().shadow, |this| this.shadow_sm())
.when(focused, |this| this.focused_border(cx))
.when(self.bordered, |this| {
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))
.input_pr(self.size)

View file

@ -11,7 +11,6 @@ use crate::{
clipboard::Clipboard,
description_list::DescriptionList,
h_flex,
highlighter::HighlightTheme,
input::{InputState, TextInput},
link::Link,
v_flex, ActiveTheme, IconName, Selectable, Sizable, TITLE_BAR_HEIGHT,
@ -60,15 +59,9 @@ pub struct DivInspector {
impl DivInspector {
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| {
InputState::new(window, cx)
.code_editor(Some("json"), theme)
.code_editor(Some("json"))
.line_number(false)
.disabled(true)
});
@ -134,7 +127,9 @@ impl Render for DivInspector {
.w_full()
.font_family("Monaco")
.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 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()
.id("inspector")
@ -202,6 +198,7 @@ fn render_inspector(
.flex_1()
.p_3()
.gap_3()
.text_sm()
.when_some(source_location, |this, source_location| {
this.child(
h_flex()
@ -215,6 +212,7 @@ fn render_inspector(
.child(Clipboard::new("copy-source-location").value(source_location)),
)
})
.children(element_global_id)
.children(inspector.render_inspector_states(window, cx)),
)
.into_any_element()

View file

@ -215,8 +215,9 @@ impl CodeBlock {
let highlight = Highlighter::new(
lang.as_ref().map(|v| v.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 }
}
}

View file

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