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},
h_flex,
highlighter::Language,
input::{InputEvent, InputState, TabSize, TextInput},
input::{InputEvent, InputState, Marker, TabSize, TextInput},
v_flex,
};
use story::Assets;
@ -84,6 +84,25 @@ impl Example {
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>) {
if !self.need_update {
return;
@ -103,6 +122,7 @@ impl Example {
impl Render for Example {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
self.update_highlighter(window, cx);
self.set_markers(window, cx);
v_flex()
.size_full()

View file

@ -38,9 +38,10 @@ pub struct SyntaxHighlighter {
/// 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)>,
}
@ -539,42 +540,37 @@ impl SyntaxHighlighter {
) -> Vec<(Range<usize>, HighlightStyle)> {
let mut styles = vec![];
let start_offset = range.start;
let line_len = range.len();
let mut last_range = 0..0;
let mut last_range = start_offset..start_offset;
// 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.
// Ref: https://github.com/longbridge/gpui-component/pull/904/commits/d8f886939d3b472f228c1ce72154a951e98f32c5
if node_range.end > range.end {
break;
}
let range_in_line = node_range.start.saturating_sub(start_offset)
..node_range.end.saturating_sub(start_offset);
// let range_in_line = node_range.start..node_range.end;
// Ensure every range is connected.
if last_range.end < range_in_line.start {
styles.push((
last_range.end..range_in_line.start,
HighlightStyle::default(),
));
if last_range.end < node_range.start {
styles.push((last_range.end..node_range.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));
last_range = range_in_line;
styles.push((node_range.clone(), style));
last_range = node_range.clone();
}
// If the matched styles is empty, return a default range.
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.
if last_range.end < line_len {
styles.push((last_range.end..line_len, HighlightStyle::default()));
if last_range.end < range.end {
styles.push((last_range.end..range.end, HighlightStyle::default()));
}
styles

View file

@ -65,7 +65,7 @@ const DEFAULT_LIGHT: LazyLock<HighlightTheme> = LazyLock::new(|| {
/// Theme for 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 attribute: 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 {
#[serde(rename = "editor.background")]
pub background: Option<Hsla>,
@ -244,6 +320,9 @@ pub struct HighlightThemeStyle {
pub line_number: Option<Hsla>,
#[serde(rename = "editor.active_line_number")]
pub active_line_number: Option<Hsla>,
#[serde(flatten)]
pub status: StatusColors,
#[serde(rename = "syntax")]
pub syntax: SyntaxColors,
}

View file

@ -1,123 +1,165 @@
{
"name": "macOS Classic Dark",
"appearance": "dark",
"style": {
"editor.foreground": "#DDDDDD",
"editor.background": "#131313",
"editor.active_line.background": "#272727",
"editor.line_number": "#8F8F8F",
"editor.active_line_number": "#DDDDDD",
"syntax": {
"attribute": {
"color": "#be9a52",
"font_style": null,
"font_weight": null
},
"boolean": {
"color": "#E1D797",
"font_style": null,
"font_weight": null
},
"comment": {
"color": "#9E9E9E",
"font_style": null,
"font_weight": null
},
"comment.doc": {
"color": "#9E9E9E",
"font_style": null,
"font_weight": null
},
"constant": {
"color": "#E1D797",
"font_style": null,
"font_weight": null
},
"constructor": {
"color": "#b5af9a",
"font_style": null,
"font_weight": null
},
"embedded": {
"color": "#CACCCA",
"font_style": null,
"font_weight": null
},
"function": {
"color": "#E1D797",
"font_style": null,
"font_weight": null
},
"keyword": {
"color": "#E19773",
"font_style": null,
"font_weight": null
},
"link_text": {
"color": "#A86D3B",
"font_style": "normal",
"font_weight": null
},
"link_uri": {
"color": "#6F6D66",
"font_style": "italic",
"font_weight": null
},
"number": {
"color": "#E19773",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#76BA53",
"font_style": null,
"font_weight": null
},
"string.escape": {
"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
}
}
"name": "macOS Classic Dark",
"appearance": "dark",
"style": {
"editor.foreground": "#DDDDDD",
"editor.background": "#131313",
"editor.active_line.background": "#272727",
"editor.line_number": "#8F8F8F",
"editor.active_line_number": "#DDDDDD",
"conflict": "#D2602D",
"conflict.background": null,
"conflict.border": null,
"created": "#3f72e2",
"created.background": "#0C4619",
"created.border": null,
"deleted": null,
"deleted.background": "#46190C",
"deleted.border": null,
"error": null,
"error.background": "#46190C",
"error.border": "#802207",
"hidden": "#9E9E9E",
"hidden.background": null,
"hidden.border": null,
"hint": null,
"hint.background": "#0C194D",
"hint.border": "#082190",
"ignored": null,
"ignored.background": null,
"ignored.border": null,
"info": null,
"info.background": "#0C194D",
"info.border": "#082190",
"modified": "#B0A878",
"modified.background": "#3A310E",
"modified.border": null,
"predictive": "#5D5945",
"predictive.background": null,
"predictive.border": null,
"renamed": null,
"renamed.background": null,
"renamed.border": null,
"success": null,
"success.background": "#0C4619",
"success.border": null,
"unreachable": null,
"unreachable.background": null,
"unreachable.border": null,
"warning": null,
"warning.background": "#3A310E",
"warning.border": "#7B6508",
"syntax": {
"attribute": {
"color": "#be9a52",
"font_style": null,
"font_weight": null
},
"boolean": {
"color": "#E1D797",
"font_style": null,
"font_weight": null
},
"comment": {
"color": "#9E9E9E",
"font_style": null,
"font_weight": null
},
"comment.doc": {
"color": "#9E9E9E",
"font_style": null,
"font_weight": null
},
"constant": {
"color": "#E1D797",
"font_style": null,
"font_weight": null
},
"constructor": {
"color": "#b5af9a",
"font_style": null,
"font_weight": null
},
"embedded": {
"color": "#CACCCA",
"font_style": null,
"font_weight": null
},
"function": {
"color": "#E1D797",
"font_style": null,
"font_weight": null
},
"keyword": {
"color": "#E19773",
"font_style": null,
"font_weight": null
},
"link_text": {
"color": "#A86D3B",
"font_style": "normal",
"font_weight": null
},
"link_uri": {
"color": "#6F6D66",
"font_style": "italic",
"font_weight": null
},
"number": {
"color": "#E19773",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#76BA53",
"font_style": null,
"font_weight": null
},
"string.escape": {
"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",
"appearance": "light",
"style": {
"editor.foreground": "#000000",
"editor.background": "#ffffff",
"editor.active_line.background": "#F0F0F0",
"editor.line_number": "#929292",
"editor.active_line_number": "#000000",
"syntax": {
"attribute": {
"color": "#957931",
"font_style": null,
"font_weight": null
},
"boolean": {
"color": "#C5060B",
"font_style": null,
"font_weight": null
},
"comment": {
"color": "#007fff",
"font_style": null,
"font_weight": null
},
"comment.doc": {
"color": "#007fff",
"font_style": null,
"font_weight": null
},
"constant": {
"color": "#C5060B",
"font_style": null,
"font_weight": null
},
"constructor": {
"color": "#0433ff",
"font_style": null,
"font_weight": null
},
"embedded": {
"color": "#333333",
"font_style": null,
"font_weight": null
},
"function": {
"color": "#0000A2",
"font_style": null,
"font_weight": null
},
"keyword": {
"color": "#0433ff",
"font_style": null,
"font_weight": null
},
"link_text": {
"color": "#0000A2",
"font_style": "normal",
"font_weight": null
},
"link_uri": {
"color": "#6A7293",
"font_style": "italic",
"font_weight": null
},
"number": {
"color": "#0433ff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#036A07",
"font_style": null,
"font_weight": null
},
"string.escape": {
"color": "#036A07",
"font_style": null,
"font_weight": null
},
"string.regex": {
"color": "#036A07",
"font_style": null,
"font_weight": null
},
"string.special": {
"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
}
}
"name": "macOS Classic Light",
"appearance": "light",
"style": {
"editor.foreground": "#000000",
"editor.background": "#ffffff",
"editor.active_line.background": "#F0F0F0",
"editor.line_number": "#929292",
"editor.active_line_number": "#000000",
"conflict": "#C5060B",
"conflict.background": null,
"conflict.border": null,
"created": "#1642FF",
"created.background": "#e5ffe9",
"created.border": null,
"deleted": null,
"deleted.background": "#FBEAE5",
"deleted.border": null,
"error": null,
"error.background": "#FBEAE5",
"error.border": "#EC9F89",
"hidden": "#6D6D6D",
"hidden.background": null,
"hidden.border": null,
"hint": null,
"hint.background": "#E5F2FF",
"hint.border": "#99CCFF",
"ignored": null,
"ignored.background": null,
"ignored.border": null,
"info": null,
"info.background": "#E5EAFF",
"info.border": "#8DA1FF",
"modified": "#9e7008",
"modified.background": "#fff2e5",
"modified.border": null,
"predictive": "#A4ABB6",
"predictive.background": null,
"predictive.border": null,
"renamed": null,
"renamed.background": null,
"renamed.border": null,
"success": null,
"success.background": "#E5FFE5",
"success.border": null,
"unreachable": null,
"unreachable.background": null,
"unreachable.border": null,
"warning": "#C99401",
"warning.background": "#FFFBE5",
"warning.border": "#D9CC89",
"syntax": {
"attribute": {
"color": "#957931",
"font_style": null,
"font_weight": null
},
"boolean": {
"color": "#C5060B",
"font_style": null,
"font_weight": null
},
"comment": {
"color": "#007fff",
"font_style": null,
"font_weight": null
},
"comment.doc": {
"color": "#007fff",
"font_style": null,
"font_weight": null
},
"constant": {
"color": "#C5060B",
"font_style": null,
"font_weight": null
},
"constructor": {
"color": "#0433ff",
"font_style": null,
"font_weight": null
},
"embedded": {
"color": "#333333",
"font_style": null,
"font_weight": null
},
"function": {
"color": "#0000A2",
"font_style": null,
"font_weight": null
},
"keyword": {
"color": "#0433ff",
"font_style": null,
"font_weight": null
},
"link_text": {
"color": "#0000A2",
"font_style": "normal",
"font_weight": null
},
"link_uri": {
"color": "#6A7293",
"font_style": "italic",
"font_weight": null
},
"number": {
"color": "#0433ff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#036A07",
"font_style": null,
"font_weight": null
},
"string.escape": {
"color": "#036A07",
"font_style": null,
"font_weight": null
},
"string.regex": {
"color": "#036A07",
"font_style": null,
"font_weight": null
},
"string.special": {
"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::{
fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler,
Entity, GlobalElementId, IntoElement, LayoutId, MouseButton, MouseMoveEvent, Path, Pixels,
Point, SharedString, Size, Style, TextAlign, TextRun, UnderlineStyle, Window, WrappedLine,
Entity, GlobalElementId, HighlightStyle, IntoElement, LayoutId, MouseButton, MouseMoveEvent,
Path, Pixels, Point, SharedString, Size, Style, TextAlign, TextRun, UnderlineStyle, Window,
WrappedLine,
};
use smallvec::SmallVec;
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 BOTTOM_MARGIN_ROWS: usize = 1;
@ -358,19 +359,25 @@ impl TextElement {
&mut self,
visible_range: &Range<usize>,
cx: &mut App,
) -> Option<(usize, Vec<LineHighlightStyle>)> {
) -> Option<(usize, Vec<(Range<usize>, HighlightStyle)>)> {
let theme = LanguageRegistry::global(cx)
.theme(cx.theme().is_dark())
.clone();
self.input.update(cx, |state, _| match &mut state.mode {
InputMode::CodeEditor { highlighter, .. } => {
self.input.update(cx, |state, _| match &state.mode {
InputMode::CodeEditor {
highlighter,
markers,
..
} => {
let mut offset = 0;
let mut skipped_offset = 0;
let mut lines = vec![];
let mut styles = vec![];
for (ix, line) in state.text.split('\n').enumerate() {
// +1 for last `\n`.
let line_len = line.len() + 1;
if ix < visible_range.start {
offset += line.len() + 1;
offset += line_len;
skipped_offset = offset;
continue;
}
@ -378,16 +385,34 @@ impl TextElement {
break;
}
let range = offset..offset + line.len();
let styles = highlighter.borrow().styles(&range, &theme);
let range = offset..offset + line_len;
let line_styles = highlighter.borrow().styles(&range, &theme);
lines.push(LineHighlightStyle {
offset,
styles: Rc::new(styles),
});
offset += line.len() + 1;
styles = gpui::combine_highlights(styles, line_styles).collect();
offset = range.end;
}
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,
})
@ -502,7 +527,7 @@ impl Element for TextElement {
let line_height = window.line_height();
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 input = self.input.read(cx);
@ -572,7 +597,7 @@ impl Element for TextElement {
};
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![];
if skipped_offset > 0 {
runs.push(TextRun {
@ -581,9 +606,19 @@ impl Element for TextElement {
});
}
for style in highlight_lines {
runs.extend(style.to_run(&text_style, &input.marked_range, &marked_run));
}
runs.extend(highlight_styles.iter().map(|(range, style)| {
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()
} else {
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 change;
mod clear_button;
mod code_highlighter;
mod element;
mod marker;
mod mask_pattern;
mod mode;
mod number_input;
@ -12,6 +12,7 @@ mod text_input;
mod text_wrapper;
pub(crate) use clear_button::*;
pub use marker::*;
pub use mask_pattern::MaskPattern;
pub use mode::TabSize;
pub use number_input::{NumberInput, NumberInputEvent, StepAction};

View file

@ -3,7 +3,7 @@ use std::rc::Rc;
use gpui::{DefiniteLength, SharedString};
use crate::highlighter::SyntaxHighlighter;
use crate::{highlighter::SyntaxHighlighter, input::marker::Marker};
use super::text_wrapper::TextWrapper;
@ -50,6 +50,7 @@ pub enum InputMode {
/// Show line number
line_number: bool,
highlighter: Rc<RefCell<SyntaxHighlighter>>,
markers: Vec<Marker>,
},
AutoGrow {
rows: usize,
@ -160,6 +161,21 @@ impl InputMode {
_ => 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)]

View file

@ -31,6 +31,7 @@ use super::{
text_wrapper::TextWrapper,
};
use crate::highlighter::SyntaxHighlighter;
use crate::input::marker::Marker;
use crate::{history::History, scroll::ScrollbarState, Root};
#[derive(Clone, PartialEq, Eq, Deserialize)]
@ -381,6 +382,7 @@ impl InputState {
highlighter: Rc::new(RefCell::new(SyntaxHighlighter::new(&language))),
line_number: true,
height: Some(relative(1.)),
markers: vec![],
};
self
}
@ -451,6 +453,21 @@ impl InputState {
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
pub fn set_placeholder(
&mut self,
@ -1986,6 +2003,7 @@ impl EntityInputHandler for InputState {
.borrow_mut()
.update(&range, self.text.clone(), &new_text, cx);
}
self.mode.clear_markers();
self.text_wrapper.update(self.text.clone(), false, cx);
self.selected_range = new_pos..new_pos;
self.marked_range.take();
@ -2029,6 +2047,7 @@ impl EntityInputHandler for InputState {
.borrow_mut()
.update(&range, self.text.clone(), &new_text, cx);
}
self.mode.clear_markers();
self.text_wrapper.update(self.text.clone(), false, cx);
if new_text.is_empty() {
// Cancel selection, when cancel IME input.

View file

@ -71,7 +71,7 @@ impl TextWrapper {
.line_wrapper(self.font.clone(), self.font_size);
let mut prev_line_ix = 0;
for line in text.lines() {
for line in text.split('\n') {
let mut line_wraps = vec![];
let mut prev_boundary_ix = 0;
@ -83,7 +83,7 @@ impl TextWrapper {
lines.push(LineWrap {
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);
@ -92,7 +92,7 @@ impl TextWrapper {
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.