Jason Lee 2025-06-05 23:04:01 +08:00 committed by GitHub
parent 9afe0063a4
commit a8f5ced4e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 674 additions and 335 deletions

View file

@ -4,7 +4,7 @@ use gpui_component::{
dropdown::{Dropdown, DropdownEvent, DropdownState}, dropdown::{Dropdown, DropdownEvent, DropdownState},
h_flex, h_flex,
highlighter::Language, highlighter::Language,
input::{InputEvent, InputState, TabSize, TextInput}, input::{InputEvent, InputState, Marker, TabSize, TextInput},
v_flex, v_flex,
}; };
use story::Assets; use story::Assets;
@ -84,6 +84,25 @@ impl Example {
cx.new(|cx| Self::new(window, cx)) cx.new(|cx| Self::new(window, cx))
} }
fn set_markers(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.language.name() != "rust" {
return;
}
self.input_state.update(cx, |state, cx| {
state.set_markers(
vec![
Marker::new("warning", (2, 1), (2, 31), "Import but not used."),
Marker::new("error", (16, 10), (16, 46), "Syntax error."),
Marker::new("info", (25, 10), (25, 20), "This is a info message."),
Marker::new("hint", (36, 9), (40, 10), "This is a hint message."),
],
window,
cx,
);
});
}
fn update_highlighter(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn update_highlighter(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if !self.need_update { if !self.need_update {
return; return;
@ -103,6 +122,7 @@ impl Example {
impl Render for Example { impl Render for Example {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
self.update_highlighter(window, cx); self.update_highlighter(window, cx);
self.set_markers(window, cx);
v_flex() v_flex()
.size_full() .size_full()

View file

@ -38,9 +38,10 @@ pub struct SyntaxHighlighter {
/// Cache of highlight, the range is offset of the token in the tree. /// Cache of highlight, the range is offset of the token in the tree.
/// ///
/// The BTreeMap is ordered by the range from 0 to the end of the line. /// The BTreeMap is ordered by the range in the entire text.
/// ///
/// The `key` is the `start` of the range. /// - The `key` is the `start` of the range.
/// -The `value` is a tuple of the range (in the entire text) and the highlight name.
cache: BTreeMap<usize, (Range<usize>, String)>, cache: BTreeMap<usize, (Range<usize>, String)>,
} }
@ -539,42 +540,37 @@ impl SyntaxHighlighter {
) -> Vec<(Range<usize>, HighlightStyle)> { ) -> Vec<(Range<usize>, HighlightStyle)> {
let mut styles = vec![]; let mut styles = vec![];
let start_offset = range.start; let start_offset = range.start;
let line_len = range.len(); let mut last_range = start_offset..start_offset;
let mut last_range = 0..0;
// NOTE: the ranges in the cache may have duplicates, so we need to merge them. // NOTE: the ranges in the cache may have duplicates, so we need to merge them.
for (_, (node_range, highlight_name)) in self.cache.range(range.start..) { for (_, (node_range, name)) in self.cache.range(range.start..) {
// TODO: If break, the `comment.doc` will not work. // TODO: If break, the `comment.doc` will not work.
// Ref: https://github.com/longbridge/gpui-component/pull/904/commits/d8f886939d3b472f228c1ce72154a951e98f32c5 // Ref: https://github.com/longbridge/gpui-component/pull/904/commits/d8f886939d3b472f228c1ce72154a951e98f32c5
if node_range.end > range.end { if node_range.end > range.end {
break; break;
} }
let range_in_line = node_range.start.saturating_sub(start_offset) // let range_in_line = node_range.start..node_range.end;
..node_range.end.saturating_sub(start_offset);
// Ensure every range is connected. // Ensure every range is connected.
if last_range.end < range_in_line.start { if last_range.end < node_range.start {
styles.push(( styles.push((last_range.end..node_range.start, HighlightStyle::default()));
last_range.end..range_in_line.start,
HighlightStyle::default(),
));
} }
let style = theme.style(&highlight_name).unwrap_or_default(); let style = theme.style(&name).unwrap_or_default();
styles.push((range_in_line.clone(), style)); styles.push((node_range.clone(), style));
last_range = range_in_line; last_range = node_range.clone();
} }
// If the matched styles is empty, return a default range. // If the matched styles is empty, return a default range.
if styles.len() == 0 { if styles.len() == 0 {
return vec![(0..line_len, HighlightStyle::default())]; return vec![(start_offset..range.end, HighlightStyle::default())];
} }
// Ensure the last range is connected to the end of the line. // Ensure the last range is connected to the end of the line.
if last_range.end < line_len { if last_range.end < range.end {
styles.push((last_range.end..line_len, HighlightStyle::default())); styles.push((last_range.end..range.end, HighlightStyle::default()));
} }
styles styles

View file

@ -65,7 +65,7 @@ const DEFAULT_LIGHT: LazyLock<HighlightTheme> = LazyLock::new(|| {
/// Theme for Tree-sitter Highlight /// Theme for Tree-sitter Highlight
/// ///
/// https://docs.rs/tree-sitter-highlight/0.25.4/tree_sitter_highlight/ /// https://docs.rs/tree-sitter-highlight/0.25.4/tree_sitter_highlight/
#[derive(Debug, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
pub struct SyntaxColors { pub struct SyntaxColors {
pub attribute: Option<ThemeStyle>, pub attribute: Option<ThemeStyle>,
pub boolean: Option<ThemeStyle>, pub boolean: Option<ThemeStyle>,
@ -232,7 +232,83 @@ impl SyntaxColors {
} }
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
pub struct StatusColors {
#[serde(rename = "error")]
error: Option<Hsla>,
#[serde(rename = "error.background")]
error_background: Option<Hsla>,
#[serde(rename = "warning")]
warning: Option<Hsla>,
#[serde(rename = "warning.background")]
warning_background: Option<Hsla>,
#[serde(rename = "info")]
info: Option<Hsla>,
#[serde(rename = "info.background")]
info_background: Option<Hsla>,
#[serde(rename = "success")]
success: Option<Hsla>,
#[serde(rename = "success.background")]
success_background: Option<Hsla>,
#[serde(rename = "hint")]
hint: Option<Hsla>,
#[serde(rename = "hint.background")]
hint_background: Option<Hsla>,
}
impl StatusColors {
#[inline]
pub fn error(&self) -> Hsla {
self.error.unwrap_or(crate::red_500())
}
#[inline]
pub fn error_background(&self) -> Hsla {
self.error_background.unwrap_or(self.error())
}
#[inline]
pub fn warning(&self) -> Hsla {
self.warning.unwrap_or(crate::yellow_500())
}
#[inline]
pub fn warning_background(&self) -> Hsla {
self.warning_background.unwrap_or(self.warning())
}
#[inline]
pub fn info(&self) -> Hsla {
self.info.unwrap_or(crate::blue_500())
}
#[inline]
pub fn info_background(&self) -> Hsla {
self.info_background.unwrap_or(self.info())
}
#[inline]
pub fn success(&self) -> Hsla {
self.success.unwrap_or(crate::green_500())
}
#[inline]
pub fn success_background(&self) -> Hsla {
self.success_background.unwrap_or(self.success())
}
#[inline]
pub fn hint(&self) -> Hsla {
self.hint.unwrap_or(crate::cyan_500().opacity(0.5))
}
#[inline]
pub fn hint_background(&self) -> Hsla {
self.hint_background.unwrap_or(self.hint())
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
pub struct HighlightThemeStyle { pub struct HighlightThemeStyle {
#[serde(rename = "editor.background")] #[serde(rename = "editor.background")]
pub background: Option<Hsla>, pub background: Option<Hsla>,
@ -244,6 +320,9 @@ pub struct HighlightThemeStyle {
pub line_number: Option<Hsla>, pub line_number: Option<Hsla>,
#[serde(rename = "editor.active_line_number")] #[serde(rename = "editor.active_line_number")]
pub active_line_number: Option<Hsla>, pub active_line_number: Option<Hsla>,
#[serde(flatten)]
pub status: StatusColors,
#[serde(rename = "syntax")]
pub syntax: SyntaxColors, pub syntax: SyntaxColors,
} }

View file

@ -1,123 +1,165 @@
{ {
"name": "macOS Classic Dark", "name": "macOS Classic Dark",
"appearance": "dark", "appearance": "dark",
"style": { "style": {
"editor.foreground": "#DDDDDD", "editor.foreground": "#DDDDDD",
"editor.background": "#131313", "editor.background": "#131313",
"editor.active_line.background": "#272727", "editor.active_line.background": "#272727",
"editor.line_number": "#8F8F8F", "editor.line_number": "#8F8F8F",
"editor.active_line_number": "#DDDDDD", "editor.active_line_number": "#DDDDDD",
"syntax": { "conflict": "#D2602D",
"attribute": { "conflict.background": null,
"color": "#be9a52", "conflict.border": null,
"font_style": null, "created": "#3f72e2",
"font_weight": null "created.background": "#0C4619",
}, "created.border": null,
"boolean": { "deleted": null,
"color": "#E1D797", "deleted.background": "#46190C",
"font_style": null, "deleted.border": null,
"font_weight": null "error": null,
}, "error.background": "#46190C",
"comment": { "error.border": "#802207",
"color": "#9E9E9E", "hidden": "#9E9E9E",
"font_style": null, "hidden.background": null,
"font_weight": null "hidden.border": null,
}, "hint": null,
"comment.doc": { "hint.background": "#0C194D",
"color": "#9E9E9E", "hint.border": "#082190",
"font_style": null, "ignored": null,
"font_weight": null "ignored.background": null,
}, "ignored.border": null,
"constant": { "info": null,
"color": "#E1D797", "info.background": "#0C194D",
"font_style": null, "info.border": "#082190",
"font_weight": null "modified": "#B0A878",
}, "modified.background": "#3A310E",
"constructor": { "modified.border": null,
"color": "#b5af9a", "predictive": "#5D5945",
"font_style": null, "predictive.background": null,
"font_weight": null "predictive.border": null,
}, "renamed": null,
"embedded": { "renamed.background": null,
"color": "#CACCCA", "renamed.border": null,
"font_style": null, "success": null,
"font_weight": null "success.background": "#0C4619",
}, "success.border": null,
"function": { "unreachable": null,
"color": "#E1D797", "unreachable.background": null,
"font_style": null, "unreachable.border": null,
"font_weight": null "warning": null,
}, "warning.background": "#3A310E",
"keyword": { "warning.border": "#7B6508",
"color": "#E19773", "syntax": {
"font_style": null, "attribute": {
"font_weight": null "color": "#be9a52",
}, "font_style": null,
"link_text": { "font_weight": null
"color": "#A86D3B", },
"font_style": "normal", "boolean": {
"font_weight": null "color": "#E1D797",
}, "font_style": null,
"link_uri": { "font_weight": null
"color": "#6F6D66", },
"font_style": "italic", "comment": {
"font_weight": null "color": "#9E9E9E",
}, "font_style": null,
"number": { "font_weight": null
"color": "#E19773", },
"font_style": null, "comment.doc": {
"font_weight": null "color": "#9E9E9E",
}, "font_style": null,
"string": { "font_weight": null
"color": "#76BA53", },
"font_style": null, "constant": {
"font_weight": null "color": "#E1D797",
}, "font_style": null,
"string.escape": { "font_weight": null
"color": "#76BA53", },
"font_style": null, "constructor": {
"font_weight": null "color": "#b5af9a",
}, "font_style": null,
"string.regex": { "font_weight": null
"color": "#76BA53", },
"font_style": null, "embedded": {
"font_weight": null "color": "#CACCCA",
}, "font_style": null,
"string.special": { "font_weight": null
"color": "#E1D797", },
"font_style": null, "function": {
"font_weight": null "color": "#E1D797",
}, "font_style": null,
"string.special.symbol": { "font_weight": null
"color": "#E1D797", },
"font_style": null, "keyword": {
"font_weight": null "color": "#E19773",
}, "font_style": null,
"tag": { "font_weight": null
"color": "#b5af9a", },
"font_style": null, "link_text": {
"font_weight": null "color": "#A86D3B",
}, "font_style": "normal",
"text.literal": { "font_weight": null
"color": "#E1D797", },
"font_style": null, "link_uri": {
"font_weight": null "color": "#6F6D66",
}, "font_style": "italic",
"title": { "font_weight": null
"color": "#A76D3B", },
"font_style": null, "number": {
"font_weight": 600 "color": "#E19773",
}, "font_style": null,
"type": { "font_weight": null
"color": "#A86D3B", },
"font_style": null, "string": {
"font_weight": null "color": "#76BA53",
}, "font_style": null,
"variable.special": { "font_weight": null
"color": "#E19773", },
"font_style": null, "string.escape": {
"font_weight": null "color": "#76BA53",
} "font_style": null,
} "font_weight": null
},
"string.regex": {
"color": "#76BA53",
"font_style": null,
"font_weight": null
},
"string.special": {
"color": "#E1D797",
"font_style": null,
"font_weight": null
},
"string.special.symbol": {
"color": "#E1D797",
"font_style": null,
"font_weight": null
},
"tag": {
"color": "#b5af9a",
"font_style": null,
"font_weight": null
},
"text.literal": {
"color": "#E1D797",
"font_style": null,
"font_weight": null
},
"title": {
"color": "#A76D3B",
"font_style": null,
"font_weight": 600
},
"type": {
"color": "#A86D3B",
"font_style": null,
"font_weight": null
},
"variable.special": {
"color": "#E19773",
"font_style": null,
"font_weight": null
}
} }
}
} }

View file

@ -1,133 +1,175 @@
{ {
"name": "macOS Classic Light", "name": "macOS Classic Light",
"appearance": "light", "appearance": "light",
"style": { "style": {
"editor.foreground": "#000000", "editor.foreground": "#000000",
"editor.background": "#ffffff", "editor.background": "#ffffff",
"editor.active_line.background": "#F0F0F0", "editor.active_line.background": "#F0F0F0",
"editor.line_number": "#929292", "editor.line_number": "#929292",
"editor.active_line_number": "#000000", "editor.active_line_number": "#000000",
"syntax": { "conflict": "#C5060B",
"attribute": { "conflict.background": null,
"color": "#957931", "conflict.border": null,
"font_style": null, "created": "#1642FF",
"font_weight": null "created.background": "#e5ffe9",
}, "created.border": null,
"boolean": { "deleted": null,
"color": "#C5060B", "deleted.background": "#FBEAE5",
"font_style": null, "deleted.border": null,
"font_weight": null "error": null,
}, "error.background": "#FBEAE5",
"comment": { "error.border": "#EC9F89",
"color": "#007fff", "hidden": "#6D6D6D",
"font_style": null, "hidden.background": null,
"font_weight": null "hidden.border": null,
}, "hint": null,
"comment.doc": { "hint.background": "#E5F2FF",
"color": "#007fff", "hint.border": "#99CCFF",
"font_style": null, "ignored": null,
"font_weight": null "ignored.background": null,
}, "ignored.border": null,
"constant": { "info": null,
"color": "#C5060B", "info.background": "#E5EAFF",
"font_style": null, "info.border": "#8DA1FF",
"font_weight": null "modified": "#9e7008",
}, "modified.background": "#fff2e5",
"constructor": { "modified.border": null,
"color": "#0433ff", "predictive": "#A4ABB6",
"font_style": null, "predictive.background": null,
"font_weight": null "predictive.border": null,
}, "renamed": null,
"embedded": { "renamed.background": null,
"color": "#333333", "renamed.border": null,
"font_style": null, "success": null,
"font_weight": null "success.background": "#E5FFE5",
}, "success.border": null,
"function": { "unreachable": null,
"color": "#0000A2", "unreachable.background": null,
"font_style": null, "unreachable.border": null,
"font_weight": null "warning": "#C99401",
}, "warning.background": "#FFFBE5",
"keyword": { "warning.border": "#D9CC89",
"color": "#0433ff", "syntax": {
"font_style": null, "attribute": {
"font_weight": null "color": "#957931",
}, "font_style": null,
"link_text": { "font_weight": null
"color": "#0000A2", },
"font_style": "normal", "boolean": {
"font_weight": null "color": "#C5060B",
}, "font_style": null,
"link_uri": { "font_weight": null
"color": "#6A7293", },
"font_style": "italic", "comment": {
"font_weight": null "color": "#007fff",
}, "font_style": null,
"number": { "font_weight": null
"color": "#0433ff", },
"font_style": null, "comment.doc": {
"font_weight": null "color": "#007fff",
}, "font_style": null,
"string": { "font_weight": null
"color": "#036A07", },
"font_style": null, "constant": {
"font_weight": null "color": "#C5060B",
}, "font_style": null,
"string.escape": { "font_weight": null
"color": "#036A07", },
"font_style": null, "constructor": {
"font_weight": null "color": "#0433ff",
}, "font_style": null,
"string.regex": { "font_weight": null
"color": "#036A07", },
"font_style": null, "embedded": {
"font_weight": null "color": "#333333",
}, "font_style": null,
"string.special": { "font_weight": null
"color": "#d21f07", },
"font_style": null, "function": {
"font_weight": null "color": "#0000A2",
}, "font_style": null,
"string.special.symbol": { "font_weight": null
"color": "#d21f07", },
"font_style": null, "keyword": {
"font_weight": null "color": "#0433ff",
}, "font_style": null,
"tag": { "font_weight": null
"color": "#0433ff", },
"font_style": null, "link_text": {
"font_weight": null "color": "#0000A2",
}, "font_style": "normal",
"text.literal": { "font_weight": null
"color": "#6F42C1", },
"font_style": null, "link_uri": {
"font_weight": null "color": "#6A7293",
}, "font_style": "italic",
"title": { "font_weight": null
"color": "#0433FF", },
"font_style": null, "number": {
"font_weight": null "color": "#0433ff",
}, "font_style": null,
"type": { "font_weight": null
"color": "#6f42c1", },
"font_style": null, "string": {
"font_weight": null "color": "#036A07",
}, "font_style": null,
"property": { "font_weight": null
"color": "#333333", },
"font_style": null, "string.escape": {
"font_weight": null "color": "#036A07",
}, "font_style": null,
"variable": { "font_weight": null
"color": "#333333", },
"font_style": null, "string.regex": {
"font_weight": null "color": "#036A07",
}, "font_style": null,
"variable.special": { "font_weight": null
"color": "#C5060B", },
"font_style": null, "string.special": {
"font_weight": null "color": "#d21f07",
} "font_style": null,
} "font_weight": null
},
"string.special.symbol": {
"color": "#d21f07",
"font_style": null,
"font_weight": null
},
"tag": {
"color": "#0433ff",
"font_style": null,
"font_weight": null
},
"text.literal": {
"color": "#6F42C1",
"font_style": null,
"font_weight": null
},
"title": {
"color": "#0433FF",
"font_style": null,
"font_weight": null
},
"type": {
"color": "#6f42c1",
"font_style": null,
"font_weight": null
},
"property": {
"color": "#333333",
"font_style": null,
"font_weight": null
},
"variable": {
"color": "#333333",
"font_style": null,
"font_weight": null
},
"variable.special": {
"color": "#C5060B",
"font_style": null,
"font_weight": null
}
} }
}
} }

View file

@ -1,36 +0,0 @@
use gpui::{HighlightStyle, TextRun, TextStyle};
use std::{ops::Range, rc::Rc};
#[derive(Debug, Clone)]
pub(crate) struct LineHighlightStyle {
pub(crate) offset: usize,
pub(crate) styles: Rc<Vec<(Range<usize>, HighlightStyle)>>,
}
impl LineHighlightStyle {
pub(super) fn to_run(
&self,
text_style: &TextStyle,
marked_range: &Option<Range<usize>>,
marked_run: &TextRun,
) -> Vec<TextRun> {
self.styles
.iter()
.map(|(range, style)| {
let mut run = text_style.clone().highlight(*style).to_run(range.len());
if let Some(marked_range) = marked_range {
if self.offset + range.start >= marked_range.start
&& self.offset + range.end <= marked_range.end
{
run.color = marked_run.color;
run.strikethrough = marked_run.strikethrough;
run.underline = marked_run.underline;
}
}
run
})
// Add last `\n` Run with len 1
.chain(std::iter::once(text_style.clone().to_run(1)))
.collect()
}
}

View file

@ -2,14 +2,15 @@ use std::{ops::Range, rc::Rc};
use gpui::{ use gpui::{
fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler, fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler,
Entity, GlobalElementId, IntoElement, LayoutId, MouseButton, MouseMoveEvent, Path, Pixels, Entity, GlobalElementId, HighlightStyle, IntoElement, LayoutId, MouseButton, MouseMoveEvent,
Point, SharedString, Size, Style, TextAlign, TextRun, UnderlineStyle, Window, WrappedLine, Path, Pixels, Point, SharedString, Size, Style, TextAlign, TextRun, UnderlineStyle, Window,
WrappedLine,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
use crate::{highlighter::LanguageRegistry, ActiveTheme as _, Root}; use crate::{highlighter::LanguageRegistry, ActiveTheme as _, Root};
use super::{code_highlighter::LineHighlightStyle, mode::InputMode, InputState, LastLayout}; use super::{mode::InputMode, InputState, LastLayout};
const RIGHT_MARGIN: Pixels = px(5.); const RIGHT_MARGIN: Pixels = px(5.);
const BOTTOM_MARGIN_ROWS: usize = 1; const BOTTOM_MARGIN_ROWS: usize = 1;
@ -358,19 +359,25 @@ impl TextElement {
&mut self, &mut self,
visible_range: &Range<usize>, visible_range: &Range<usize>,
cx: &mut App, cx: &mut App,
) -> Option<(usize, Vec<LineHighlightStyle>)> { ) -> Option<(usize, Vec<(Range<usize>, HighlightStyle)>)> {
let theme = LanguageRegistry::global(cx) let theme = LanguageRegistry::global(cx)
.theme(cx.theme().is_dark()) .theme(cx.theme().is_dark())
.clone(); .clone();
self.input.update(cx, |state, _| match &mut state.mode { self.input.update(cx, |state, _| match &state.mode {
InputMode::CodeEditor { highlighter, .. } => { InputMode::CodeEditor {
highlighter,
markers,
..
} => {
let mut offset = 0; let mut offset = 0;
let mut skipped_offset = 0; let mut skipped_offset = 0;
let mut lines = vec![]; let mut styles = vec![];
for (ix, line) in state.text.split('\n').enumerate() { for (ix, line) in state.text.split('\n').enumerate() {
// +1 for last `\n`.
let line_len = line.len() + 1;
if ix < visible_range.start { if ix < visible_range.start {
offset += line.len() + 1; offset += line_len;
skipped_offset = offset; skipped_offset = offset;
continue; continue;
} }
@ -378,16 +385,34 @@ impl TextElement {
break; break;
} }
let range = offset..offset + line.len(); let range = offset..offset + line_len;
let styles = highlighter.borrow().styles(&range, &theme); let line_styles = highlighter.borrow().styles(&range, &theme);
lines.push(LineHighlightStyle { styles = gpui::combine_highlights(styles, line_styles).collect();
offset,
styles: Rc::new(styles), offset = range.end;
});
offset += line.len() + 1;
} }
Some((skipped_offset, lines))
let mut marker_styles = vec![];
for marker in markers.iter() {
if let Some(range) = marker.byte_range(&state) {
if range.start < skipped_offset {
continue;
}
let node_range = range.start..range.end;
if node_range.start >= visible_range.start
|| node_range.end <= visible_range.end
{
marker_styles
.push((node_range, marker.severity.highlight_style(&theme)));
}
}
}
styles = gpui::combine_highlights(marker_styles, styles).collect();
Some((skipped_offset, styles))
} }
_ => None, _ => None,
}) })
@ -502,7 +527,7 @@ impl Element for TextElement {
let line_height = window.line_height(); let line_height = window.line_height();
let visible_range = self.calculate_visible_range(&state, line_height, &bounds); let visible_range = self.calculate_visible_range(&state, line_height, &bounds);
let highlight_lines = self.highlight_lines(&visible_range, cx); let highlight_styles = self.highlight_lines(&visible_range, cx);
let multi_line = self.input.read(cx).is_multi_line(); let multi_line = self.input.read(cx).is_multi_line();
let input = self.input.read(cx); let input = self.input.read(cx);
@ -572,7 +597,7 @@ impl Element for TextElement {
}; };
let runs = if !is_empty { let runs = if !is_empty {
if let Some((skipped_offset, highlight_lines)) = highlight_lines { if let Some((skipped_offset, highlight_styles)) = highlight_styles {
let mut runs = vec![]; let mut runs = vec![];
if skipped_offset > 0 { if skipped_offset > 0 {
runs.push(TextRun { runs.push(TextRun {
@ -581,9 +606,19 @@ impl Element for TextElement {
}); });
} }
for style in highlight_lines { runs.extend(highlight_styles.iter().map(|(range, style)| {
runs.extend(style.to_run(&text_style, &input.marked_range, &marked_run)); let mut run = text_style.clone().highlight(*style).to_run(range.len());
} if let Some(marked_range) = &input.marked_range {
if range.start >= marked_range.start && range.end <= marked_range.end {
run.color = marked_run.color;
run.strikethrough = marked_run.strikethrough;
run.underline = marked_run.underline;
}
}
run
}));
runs.into_iter().filter(|run| run.len > 0).collect() runs.into_iter().filter(|run| run.len > 0).collect()
} else { } else {
vec![run] vec![run]

View file

@ -0,0 +1,125 @@
use crate::{highlighter::HighlightTheme, input::InputState};
use gpui::{px, HighlightStyle, SharedString, UnderlineStyle};
use itertools::Itertools;
use std::ops::Range;
/// Marker represents a diagnostic message, such as an error or warning, in the code editor.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Marker {
pub severity: MarkerSeverity,
pub start: LineColumn,
pub end: LineColumn,
/// The message associated with the marker, typically a description of the issue.
pub message: SharedString,
}
impl Marker {
/// Creates a new marker with the specified severity, start and end positions, and message.
pub fn new(
severity: impl Into<MarkerSeverity>,
start: impl Into<LineColumn>,
end: impl Into<LineColumn>,
message: impl Into<SharedString>,
) -> Self {
Self {
severity: severity.into(),
start: start.into(),
end: end.into(),
message: message.into(),
}
}
/// Returns the range (zero-based) of bytes in the source code that this marker covers.
pub(super) fn byte_range(&self, state: &InputState) -> Option<Range<usize>> {
let start_line = state
.text_wrapper
.lines
.get(self.start.line.saturating_sub(1))?;
let start_line_str = state.text.get(start_line.range.clone())?;
let end_line = state
.text_wrapper
.lines
.get(self.end.line.saturating_sub(1))?;
let end_line_str = state.text.get(end_line.range.clone())?;
let start_byte = start_line.range.start
+ start_line_str
.chars()
.take(self.start.column.saturating_sub(1))
.counts_by(|c| c.len_utf8())
.values()
.sum::<usize>();
let end_byte = end_line.range.start
+ end_line_str
.chars()
.take(self.end.column.saturating_sub(1))
.counts_by(|c| c.len_utf8())
.values()
.sum::<usize>();
Some(start_byte..end_byte)
}
}
/// Line and column position (1-based) in the source code.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct LineColumn {
/// Line number (1-based)
pub line: usize,
/// Column number (1-based)
pub column: usize,
}
impl From<(usize, usize)> for LineColumn {
fn from(value: (usize, usize)) -> Self {
Self {
line: value.0.max(1),
column: value.1.max(1),
}
}
}
/// Severity of the marker.
#[allow(unused)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MarkerSeverity {
#[default]
Hint,
Error,
Warning,
Info,
}
impl From<&str> for MarkerSeverity {
fn from(value: &str) -> Self {
match value {
"error" => Self::Error,
"warning" => Self::Warning,
"info" => Self::Info,
"hint" => Self::Hint,
_ => Self::Info, // Default to Info if unknown
}
}
}
impl MarkerSeverity {
/// Returns the [`HighlightStyle`] for the marker severity with the given theme style.
pub(super) fn highlight_style(&self, theme: &HighlightTheme) -> HighlightStyle {
let color = match self {
Self::Error => Some(theme.style.status.error()),
Self::Warning => Some(theme.style.status.warning()),
Self::Info => Some(theme.style.status.info()),
Self::Hint => Some(theme.style.status.hint()),
};
let mut style = HighlightStyle::default();
style.underline = Some(UnderlineStyle {
color: color,
thickness: px(1.),
wavy: true,
});
style
}
}

View file

@ -1,8 +1,8 @@
mod blink_cursor; mod blink_cursor;
mod change; mod change;
mod clear_button; mod clear_button;
mod code_highlighter;
mod element; mod element;
mod marker;
mod mask_pattern; mod mask_pattern;
mod mode; mod mode;
mod number_input; mod number_input;
@ -12,6 +12,7 @@ mod text_input;
mod text_wrapper; mod text_wrapper;
pub(crate) use clear_button::*; pub(crate) use clear_button::*;
pub use marker::*;
pub use mask_pattern::MaskPattern; pub use mask_pattern::MaskPattern;
pub use mode::TabSize; pub use mode::TabSize;
pub use number_input::{NumberInput, NumberInputEvent, StepAction}; pub use number_input::{NumberInput, NumberInputEvent, StepAction};

View file

@ -3,7 +3,7 @@ use std::rc::Rc;
use gpui::{DefiniteLength, SharedString}; use gpui::{DefiniteLength, SharedString};
use crate::highlighter::SyntaxHighlighter; use crate::{highlighter::SyntaxHighlighter, input::marker::Marker};
use super::text_wrapper::TextWrapper; use super::text_wrapper::TextWrapper;
@ -50,6 +50,7 @@ pub enum InputMode {
/// Show line number /// Show line number
line_number: bool, line_number: bool,
highlighter: Rc<RefCell<SyntaxHighlighter>>, highlighter: Rc<RefCell<SyntaxHighlighter>>,
markers: Vec<Marker>,
}, },
AutoGrow { AutoGrow {
rows: usize, rows: usize,
@ -160,6 +161,21 @@ impl InputMode {
_ => None, _ => None,
} }
} }
#[allow(unused)]
pub(super) fn markers(&self) -> Option<&Vec<Marker>> {
match &self {
InputMode::CodeEditor { markers, .. } => Some(markers),
_ => None,
}
}
pub(super) fn clear_markers(&mut self) {
match self {
InputMode::CodeEditor { markers, .. } => markers.clear(),
_ => {}
}
}
} }
#[cfg(test)] #[cfg(test)]

View file

@ -31,6 +31,7 @@ use super::{
text_wrapper::TextWrapper, text_wrapper::TextWrapper,
}; };
use crate::highlighter::SyntaxHighlighter; use crate::highlighter::SyntaxHighlighter;
use crate::input::marker::Marker;
use crate::{history::History, scroll::ScrollbarState, Root}; use crate::{history::History, scroll::ScrollbarState, Root};
#[derive(Clone, PartialEq, Eq, Deserialize)] #[derive(Clone, PartialEq, Eq, Deserialize)]
@ -381,6 +382,7 @@ impl InputState {
highlighter: Rc::new(RefCell::new(SyntaxHighlighter::new(&language))), highlighter: Rc::new(RefCell::new(SyntaxHighlighter::new(&language))),
line_number: true, line_number: true,
height: Some(relative(1.)), height: Some(relative(1.)),
markers: vec![],
}; };
self self
} }
@ -451,6 +453,21 @@ impl InputState {
cx.notify(); cx.notify();
} }
/// Set markers, only for [`InputMode::CodeEditor`] mode.
///
/// For example to set the diagnostic markers in the code editor.
pub fn set_markers(
&mut self,
new_markers: Vec<Marker>,
_: &mut Window,
cx: &mut Context<Self>,
) {
if let InputMode::CodeEditor { markers, .. } = &mut self.mode {
*markers = new_markers;
cx.notify();
}
}
/// Set placeholder /// Set placeholder
pub fn set_placeholder( pub fn set_placeholder(
&mut self, &mut self,
@ -1986,6 +2003,7 @@ impl EntityInputHandler for InputState {
.borrow_mut() .borrow_mut()
.update(&range, self.text.clone(), &new_text, cx); .update(&range, self.text.clone(), &new_text, cx);
} }
self.mode.clear_markers();
self.text_wrapper.update(self.text.clone(), false, cx); self.text_wrapper.update(self.text.clone(), false, cx);
self.selected_range = new_pos..new_pos; self.selected_range = new_pos..new_pos;
self.marked_range.take(); self.marked_range.take();
@ -2029,6 +2047,7 @@ impl EntityInputHandler for InputState {
.borrow_mut() .borrow_mut()
.update(&range, self.text.clone(), &new_text, cx); .update(&range, self.text.clone(), &new_text, cx);
} }
self.mode.clear_markers();
self.text_wrapper.update(self.text.clone(), false, cx); self.text_wrapper.update(self.text.clone(), false, cx);
if new_text.is_empty() { if new_text.is_empty() {
// Cancel selection, when cancel IME input. // Cancel selection, when cancel IME input.

View file

@ -71,7 +71,7 @@ impl TextWrapper {
.line_wrapper(self.font.clone(), self.font_size); .line_wrapper(self.font.clone(), self.font_size);
let mut prev_line_ix = 0; let mut prev_line_ix = 0;
for line in text.lines() { for line in text.split('\n') {
let mut line_wraps = vec![]; let mut line_wraps = vec![];
let mut prev_boundary_ix = 0; let mut prev_boundary_ix = 0;
@ -83,7 +83,7 @@ impl TextWrapper {
lines.push(LineWrap { lines.push(LineWrap {
wrap_lines: line_wraps.len(), wrap_lines: line_wraps.len(),
range: prev_line_ix..line.len(), range: prev_line_ix..prev_line_ix + line.len(),
}); });
wrapped_lines.extend(line_wraps); wrapped_lines.extend(line_wraps);
@ -92,7 +92,7 @@ impl TextWrapper {
wrapped_lines.push(prev_line_ix + prev_boundary_ix..prev_line_ix + line.len()); wrapped_lines.push(prev_line_ix + prev_boundary_ix..prev_line_ix + line.len());
} }
prev_line_ix += line.len(); prev_line_ix += line.len() + 1;
} }
// Add last empty line. // Add last empty line.