From da85754b965f4c9f0aedc27813102a1077743642 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 11 Sep 2025 16:54:40 +0800 Subject: [PATCH] input: Refactor diagnostics. (#1240) image ## Break Changes - The `Markers` has been renamed to use `Diagnostics`. - The `input::LineNumber` has renamed to `input::Position` and changed from 1-based to use 0-based. - Renamed `go_to_line` to `set_cursor_position`, `line_column` to `cursor_position`. ```diff - pub fn line_column(&self) -> LineColumn + pub fn cursor_position(&self) -> Position - pub fn go_to_line(&mut self, line: usize, column: Option, window: &mut Window, cx: &mut Context) + pub fn set_cursor_position(&mut self, position: impl Into, window: &mut Window, cx: &mut Context) ``` --- Cargo.lock | 23 ++ crates/story/examples/code-editor.rs | 69 ++-- crates/story/examples/large-text.rs | 9 +- crates/story/examples/markdown.rs | 29 +- crates/story/src/textarea_story.rs | 4 +- crates/ui/Cargo.toml | 1 + crates/ui/src/highlighter/diagnostics.rs | 388 +++++++++++++++++++++++ crates/ui/src/highlighter/highlighter.rs | 6 +- crates/ui/src/highlighter/mod.rs | 2 + crates/ui/src/highlighter/registry.rs | 12 +- crates/ui/src/input/cursor.rs | 91 ++++-- crates/ui/src/input/element.rs | 88 +++-- crates/ui/src/input/hover_popover.rs | 45 ++- crates/ui/src/input/marker.rs | 113 ------- crates/ui/src/input/mod.rs | 2 - crates/ui/src/input/mode.rs | 37 +-- crates/ui/src/input/rope_ext.rs | 52 ++- crates/ui/src/input/state.rs | 76 ++--- crates/ui/src/text/node.rs | 3 +- 19 files changed, 665 insertions(+), 385 deletions(-) create mode 100644 crates/ui/src/highlighter/diagnostics.rs delete mode 100644 crates/ui/src/input/marker.rs diff --git a/Cargo.lock b/Cargo.lock index f7c7df06..affb4a30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2448,6 +2448,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" +[[package]] +name = "fluent-uri" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "flume" version = "0.11.1" @@ -3189,6 +3198,7 @@ dependencies = [ "html5ever 0.27.0", "indoc", "itertools 0.13.0", + "lsp-types", "markdown", "markup5ever_rcdom", "notify", @@ -4348,6 +4358,19 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "lsp-types" +version = "0.97.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071" +dependencies = [ + "bitflags 1.3.2", + "fluent-uri", + "serde", + "serde_json", + "serde_repr", +] + [[package]] name = "lyon" version = "1.0.1" diff --git a/crates/story/examples/code-editor.rs b/crates/story/examples/code-editor.rs index a39c64c7..f040b4a6 100644 --- a/crates/story/examples/code-editor.rs +++ b/crates/story/examples/code-editor.rs @@ -3,8 +3,8 @@ use gpui_component::{ button::{Button, ButtonVariants as _}, dropdown::{Dropdown, DropdownEvent, DropdownState}, h_flex, - highlighter::{Language, LanguageConfig, LanguageRegistry}, - input::{InputEvent, InputState, Marker, TabSize, TextInput}, + highlighter::{Diagnostic, DiagnosticSeverity, Language, LanguageConfig, LanguageRegistry}, + input::{self, InputEvent, InputState, TabSize, TextInput}, v_flex, ActiveTheme, ContextModal, IconName, IndexPath, Selectable, Sizable, }; use story::Assets; @@ -132,8 +132,8 @@ impl Example { }); let _subscribes = vec![ - cx.subscribe(&editor, |_, _, _: &InputEvent, cx| { - cx.notify(); + cx.subscribe(&editor, |this, _, _: &InputEvent, cx| { + this.lint_document(cx); }), cx.subscribe( &language_state, @@ -164,24 +164,6 @@ impl Example { } } - fn set_markers(&mut self, _: &mut Window, cx: &mut Context) { - if self.language.name() != "rust" { - return; - } - - self.editor.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, this is a very long message, with **Markdown** support."), - Marker::new("hint", (36, 9), (40, 10), "This is a hint message."), - ], - cx, - ); - }); - } - fn update_highlighter(&mut self, window: &mut Window, cx: &mut Context) { if !self.need_update { return; @@ -203,7 +185,8 @@ impl Example { window.open_modal(cx, move |modal, window, cx| { input_state.update(cx, |state, cx| { - state.set_placeholder(format!("{}", editor.read(cx).line_column()), window, cx); + let cursor_pos = editor.read(cx).cursor_position(); + state.set_placeholder(format!("{}", cursor_pos), window, cx); state.focus(window, cx); }); @@ -224,10 +207,12 @@ impl Example { let Some(line) = parts.next().and_then(|l| l) else { return false; }; - let column = parts.next().and_then(|c| c); + let column = parts.next().and_then(|c| c).unwrap_or(1); + let position = + input::Position::new(line.saturating_sub(1), column.saturating_sub(1)); editor.update(cx, |state, cx| { - state.go_to_line(line, column, window, cx); + state.set_cursor_position(position, window, cx); }); true @@ -243,12 +228,40 @@ impl Example { }); cx.notify(); } + + fn lint_document(&self, cx: &mut Context) { + // Subscribe to input changes and perform linting with AutoCorrect for markers example. + let value = self.editor.read(cx).value().clone(); + let result = autocorrect::lint_for(value.as_str(), self.language.name()); + + self.editor.update(cx, |state, cx| { + state.diagnostics_mut().map(|diagnostics| { + diagnostics.clear(); + for item in result.lines.iter() { + let severity = match item.severity { + autocorrect::Severity::Error => DiagnosticSeverity::Warning, + autocorrect::Severity::Warning => DiagnosticSeverity::Hint, + autocorrect::Severity::Pass => DiagnosticSeverity::Info, + }; + + let line = item.line.saturating_sub(1); // Convert to 0-based index + let col = item.col.saturating_sub(1); // Convert to 0-based index + + let start = (line, col); + let end = (line, col + item.old.chars().count()); + let message = format!("AutoCorrect: {}", item.new); + diagnostics.push(Diagnostic::new(start..end, message).with_severity(severity)); + } + }); + + cx.notify(); + }); + } } impl Render for Example { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { self.update_highlighter(window, cx); - self.set_markers(window, cx); v_flex().size_full().child( v_flex() @@ -305,13 +318,13 @@ impl Render for Example { }), ) .child({ - let loc = self.editor.read(cx).line_column(); + let position = self.editor.read(cx).cursor_position(); let cursor = self.editor.read(cx).cursor(); Button::new("line-column") .ghost() .xsmall() - .label(format!("{} ({} c)", loc, cursor)) + .label(format!("{} ({} byte)", position, cursor)) .on_click(cx.listener(Self::go_to_line)) }), ), diff --git a/crates/story/examples/large-text.rs b/crates/story/examples/large-text.rs index b68265df..a3d5f401 100644 --- a/crates/story/examples/large-text.rs +++ b/crates/story/examples/large-text.rs @@ -55,7 +55,7 @@ impl Example { window.open_modal(cx, move |modal, window, cx| { input_state.update(cx, |state, cx| { - state.set_placeholder(format!("{}", editor.read(cx).line_column()), window, cx); + state.set_placeholder(format!("{}", editor.read(cx).cursor_position()), window, cx); state.focus(window, cx); }); @@ -76,10 +76,11 @@ impl Example { let Some(line) = parts.next().and_then(|l| l) else { return false; }; - let column = parts.next().and_then(|c| c); + let line = line.saturating_sub(1); + let column = parts.next().and_then(|c| c).unwrap_or(1).saturating_sub(1); editor.update(cx, |state, cx| { - state.go_to_line(line, column, window, cx); + state.set_cursor_position((line, column), window, cx); }); true @@ -129,7 +130,7 @@ impl Render for Example { .on_click(cx.listener(Self::toggle_soft_wrap)) })) .child({ - let loc = self.editor.read(cx).line_column(); + let loc = self.editor.read(cx).cursor_position(); let cursor = self.editor.read(cx).cursor(); Button::new("line-column") diff --git a/crates/story/examples/markdown.rs b/crates/story/examples/markdown.rs index d785d27f..d95608a9 100644 --- a/crates/story/examples/markdown.rs +++ b/crates/story/examples/markdown.rs @@ -1,7 +1,7 @@ use gpui::*; use gpui_component::{ highlighter::{HighlightTheme, Language}, - input::{InputEvent, InputState, Marker, MarkerSeverity, TabSize, TextInput}, + input::{InputEvent, InputState, TabSize, TextInput}, resizable::{h_resizable, resizable_panel, ResizableState}, text::{TextView, TextViewStyle}, ActiveTheme as _, @@ -31,32 +31,7 @@ impl Example { }); let resizable_state = ResizableState::new(cx); - let _subscriptions = vec![cx.subscribe(&input_state, |_, input, _: &InputEvent, cx| { - // Subscribe to input changes and perform linting with AutoCorrect for markers example. - let value = input.read(cx).value().clone(); - let result = autocorrect::lint_for(value.as_str(), "md"); - - let mut markets = vec![]; - for item in result.lines.iter() { - let severity = match item.severity { - autocorrect::Severity::Error => MarkerSeverity::Warning, - autocorrect::Severity::Warning => MarkerSeverity::Hint, - autocorrect::Severity::Pass => MarkerSeverity::Info, - }; - - let start = (item.line, item.col); - let end = (item.line, item.col + item.old.chars().count()); - let message = format!("AutoCorrect: {}", item.new); - let market = Marker::new(severity, start, end, message); - markets.push(market); - } - - input.update(cx, |state, cx| { - state.set_markers(markets, cx); - }); - - cx.notify(); - })]; + let _subscriptions = vec![cx.subscribe(&input_state, |_, _, _: &InputEvent, _| {})]; Self { resizable_state, diff --git a/crates/story/src/textarea_story.rs b/crates/story/src/textarea_story.rs index 54126cd3..ea2dcfe7 100644 --- a/crates/story/src/textarea_story.rs +++ b/crates/story/src/textarea_story.rs @@ -144,7 +144,7 @@ impl Focusable for TextareaStory { impl Render for TextareaStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let loc = self.textarea.read(cx).line_column(); + let loc = self.textarea.read(cx).cursor_position(); v_flex() .key_context(CONTEXT) @@ -183,7 +183,7 @@ impl Render for TextareaStory { ), ), ) - .child(format!("{}:{}", loc.line, loc.column)), + .child(format!("{}:{}", loc.line, loc.character)), ), ), ) diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 4e3f98f0..d5ad5667 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -90,6 +90,7 @@ markup5ever_rcdom = "0.3.0" chrono = "0.4.38" # Code Editor +lsp-types = "0.97.0" tree-sitter = "0.25.4" tree-sitter-json = "0.24.8" tree-sitter-bash = { version = "0.23.3", optional = true } diff --git a/crates/ui/src/highlighter/diagnostics.rs b/crates/ui/src/highlighter/diagnostics.rs new file mode 100644 index 00000000..b53648c5 --- /dev/null +++ b/crates/ui/src/highlighter/diagnostics.rs @@ -0,0 +1,388 @@ +use std::{ + cmp::Ordering, + ops::{Deref, Range}, + usize, +}; + +use gpui::{px, App, HighlightStyle, Hsla, SharedString, UnderlineStyle}; +use rope::Rope; +use sum_tree::{Bias, SeekTarget, SumTree}; + +use crate::{ + input::{Position, RopeExt as _}, + ActiveTheme, +}; + +pub type DiagnosticRelatedInformation = lsp_types::DiagnosticRelatedInformation; +pub type CodeDescription = lsp_types::CodeDescription; +pub type RelatedInformation = lsp_types::DiagnosticRelatedInformation; +pub type DiagnosticTag = lsp_types::DiagnosticTag; + +#[derive(Debug, Eq, PartialEq, Clone, Default)] +pub struct Diagnostic { + /// The range [`Position`] at which the message applies. + /// + /// This is the column, character range within a single line. + pub range: Range, + + /// The diagnostic's severity. Can be omitted. If omitted it is up to the + /// client to interpret diagnostics as error, warning, info or hint. + pub severity: DiagnosticSeverity, + + /// The diagnostic's code. Can be omitted. + pub code: Option, + + pub code_description: Option, + + /// A human-readable string describing the source of this + /// diagnostic, e.g. 'typescript' or 'super lint'. + pub source: Option, + + /// The diagnostic's message. + pub message: SharedString, + + /// An array of related diagnostic information, e.g. when symbol-names within + /// a scope collide all definitions can be marked via this property. + pub related_information: Option>, + + /// Additional metadata about the diagnostic. + pub tags: Option>, + + /// A data entry field that is preserved between a `textDocument/publishDiagnostics` + /// notification and `textDocument/codeAction` request. + /// + /// @since 3.16.0 + pub data: Option, +} + +impl From for Diagnostic { + fn from(value: lsp_types::Diagnostic) -> Self { + Self { + range: Position::from(value.range.start)..Position::from(value.range.end), + severity: value + .severity + .map(Into::into) + .unwrap_or(DiagnosticSeverity::Info), + code: value.code.map(|c| match c { + lsp_types::NumberOrString::Number(n) => SharedString::from(n.to_string()), + lsp_types::NumberOrString::String(s) => SharedString::from(s), + }), + code_description: value.code_description, + source: value.source.map(|s| s.into()), + message: value.message.into(), + related_information: value.related_information, + tags: value.tags, + data: value.data, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum DiagnosticSeverity { + #[default] + Hint, + Error, + Warning, + Info, +} + +impl From for DiagnosticSeverity { + fn from(value: lsp_types::DiagnosticSeverity) -> Self { + match value { + lsp_types::DiagnosticSeverity::ERROR => Self::Error, + lsp_types::DiagnosticSeverity::WARNING => Self::Warning, + lsp_types::DiagnosticSeverity::INFORMATION => Self::Info, + lsp_types::DiagnosticSeverity::HINT => Self::Hint, + _ => Self::Info, // Default to Info if unknown + } + } +} + +impl DiagnosticSeverity { + pub(crate) fn bg(&self, cx: &App) -> Hsla { + let theme = &cx.theme().highlight_theme; + + match self { + Self::Error => theme.style.status.error_background(cx), + Self::Warning => theme.style.status.warning_background(cx), + Self::Info => theme.style.status.info_background(cx), + Self::Hint => theme.style.status.hint_background(cx), + } + } + + pub(crate) fn fg(&self, cx: &App) -> Hsla { + let theme = &cx.theme().highlight_theme; + + match self { + Self::Error => theme.style.status.error(cx), + Self::Warning => theme.style.status.warning(cx), + Self::Info => theme.style.status.info(cx), + Self::Hint => theme.style.status.hint(cx), + } + } + + pub(crate) fn border(&self, cx: &App) -> Hsla { + let theme = &cx.theme().highlight_theme; + match self { + Self::Error => theme.style.status.error_border(cx), + Self::Warning => theme.style.status.warning_border(cx), + Self::Info => theme.style.status.info_border(cx), + Self::Hint => theme.style.status.hint_border(cx), + } + } + + pub(crate) fn highlight_style(&self, cx: &App) -> HighlightStyle { + let theme = &cx.theme().highlight_theme; + + let color = match self { + Self::Error => Some(theme.style.status.error(cx)), + Self::Warning => Some(theme.style.status.warning(cx)), + Self::Info => Some(theme.style.status.info(cx)), + Self::Hint => Some(theme.style.status.hint(cx)), + }; + + let mut style = HighlightStyle::default(); + style.underline = Some(UnderlineStyle { + color: color, + thickness: px(1.), + wavy: true, + }); + + style + } +} + +impl Diagnostic { + pub fn new(range: Range>, message: impl Into) -> Self { + Self { + range: range.start.into()..range.end.into(), + message: message.into(), + ..Default::default() + } + } + + pub fn with_severity(mut self, severity: impl Into) -> Self { + self.severity = severity.into(); + self + } + + pub fn with_code(mut self, code: impl Into) -> Self { + self.code = Some(code.into()); + self + } + + pub fn with_source(mut self, source: impl Into) -> Self { + self.source = Some(source.into()); + self + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub(crate) struct DiagnosticEntry { + /// The byte range of the diagnostic in the rope. + pub range: Range, + pub diagnostic: Diagnostic, +} + +impl Deref for DiagnosticEntry { + type Target = Diagnostic; + + fn deref(&self) -> &Self::Target { + &self.diagnostic + } +} + +#[derive(Debug, Default, Clone)] +pub struct DiagnosticSummary { + count: usize, + start: usize, + end: usize, +} + +impl sum_tree::Item for DiagnosticEntry { + type Summary = DiagnosticSummary; + fn summary(&self, _cx: &()) -> Self::Summary { + DiagnosticSummary { + count: 1, + start: self.range.start, + end: self.range.end, + } + } +} + +impl sum_tree::Summary for DiagnosticSummary { + type Context = (); + fn zero(_: &Self::Context) -> Self { + DiagnosticSummary { + count: 0, + start: usize::MIN, + end: usize::MIN, + } + } + + fn add_summary(&mut self, other: &Self, _: &Self::Context) { + self.start = other.start; + self.end = other.end; + self.count += other.count; + } +} + +/// For seeking by byte range. +impl SeekTarget<'_, DiagnosticSummary, DiagnosticSummary> for usize { + fn cmp(&self, other: &DiagnosticSummary, _: &()) -> Ordering { + if *self < other.start { + Ordering::Less + } else if *self > other.end { + Ordering::Greater + } else { + Ordering::Equal + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct DiagnosticSet { + text: Rope, + diagnostics: SumTree, +} + +impl DiagnosticSet { + pub fn new(text: &Rope) -> Self { + Self { + text: text.clone(), + diagnostics: SumTree::new(&()), + } + } + + pub fn reset(&mut self, text: &Rope) { + self.text = text.clone(); + self.clear(); + } + + pub fn push(&mut self, diagnostic: Diagnostic) { + let start = self.text.position_to_offset(&diagnostic.range.start); + let end = self.text.position_to_offset(&diagnostic.range.end); + + self.diagnostics.push( + DiagnosticEntry { + range: start..end, + diagnostic, + }, + &(), + ); + } + + pub fn extend(&mut self, diagnostics: I) + where + I: IntoIterator, + { + for diagnostic in diagnostics { + self.push(diagnostic); + } + } + + pub fn len(&self) -> usize { + self.diagnostics.summary().count + } + + pub fn clear(&mut self) { + self.diagnostics = SumTree::new(&()); + } + + pub fn is_empty(&self) -> bool { + self.diagnostics.is_empty() + } + + pub(crate) fn range(&self, range: Range) -> impl Iterator { + let mut cursor = self.diagnostics.cursor::(&()); + cursor.seek(&range.start, Bias::Left); + std::iter::from_fn(move || { + if let Some(entry) = cursor.item() { + if entry.range.start < range.end { + cursor.next(); + return Some(entry); + } + } + None + }) + } + + pub(crate) fn for_offset(&self, offset: usize) -> Option<&DiagnosticEntry> { + self.range(offset..offset + 1).next() + } + + pub(crate) fn styles_for_range( + &self, + range: &Range, + cx: &App, + ) -> Vec<(Range, HighlightStyle)> { + if self.diagnostics.is_empty() { + return vec![]; + } + + let mut styles = vec![]; + for entry in self.range(range.clone()) { + let range = entry.range.clone(); + styles.push((range, entry.diagnostic.severity.highlight_style(cx))); + } + + styles + } + + #[allow(unused)] + pub(crate) fn iter(&self) -> impl Iterator { + self.diagnostics.iter() + } +} + +#[cfg(test)] +mod tests { + + #[test] + fn test_diagnostic() { + use rope::Rope; + + use super::{Diagnostic, DiagnosticSet, DiagnosticSeverity}; + + let text = Rope::from("Hello, 你好warld!\nThis is a test.\nGoodbye, world!"); + let mut diagnostics = DiagnosticSet::new(&text); + + diagnostics.push( + Diagnostic::new((0, 7)..(0, 17), "Spelling mistake") + .with_severity(DiagnosticSeverity::Warning), + ); + diagnostics.push( + Diagnostic::new((2, 9)..(2, 14), "Syntax error") + .with_severity(DiagnosticSeverity::Error), + ); + + assert_eq!(diagnostics.len(), 2); + let items = diagnostics.iter().collect::>(); + + assert_eq!(items[0].message.as_str(), "Spelling mistake"); + assert_eq!(items[0].range, 7..19); + + assert_eq!(items[1].message.as_str(), "Syntax error"); + assert_eq!(items[1].range, 45..50); + + let items = diagnostics.range(6..48).collect::>(); + assert_eq!(items.len(), 2); + + let item = diagnostics.for_offset(10).unwrap(); + assert_eq!(item.message.as_str(), "Spelling mistake"); + + let item = diagnostics.for_offset(30); + assert!(item.is_none()); + + let item = diagnostics.for_offset(46).unwrap(); + assert_eq!(item.message.as_str(), "Syntax error"); + + diagnostics.push( + Diagnostic::new((1, 5)..(1, 7), "Info message").with_severity(DiagnosticSeverity::Info), + ); + assert_eq!(diagnostics.len(), 3); + + diagnostics.clear(); + assert_eq!(diagnostics.len(), 0); + } +} diff --git a/crates/ui/src/highlighter/highlighter.rs b/crates/ui/src/highlighter/highlighter.rs index d72325c9..21a71b06 100644 --- a/crates/ui/src/highlighter/highlighter.rs +++ b/crates/ui/src/highlighter/highlighter.rs @@ -1,5 +1,4 @@ -use super::HighlightTheme; -use crate::{highlighter::LanguageRegistry, input::RopeExt as _}; +use crate::{highlighter::LanguageRegistry, input::RopeExt as _, ActiveTheme}; use anyhow::{anyhow, Context, Result}; use gpui::{App, HighlightStyle, SharedString}; @@ -551,9 +550,10 @@ impl SyntaxHighlighter { pub(crate) fn styles( &self, range: &Range, - theme: &HighlightTheme, cx: &App, ) -> Vec<(Range, HighlightStyle)> { + let theme = &cx.theme().highlight_theme; + let mut styles = vec![]; let start_offset = range.start; diff --git a/crates/ui/src/highlighter/mod.rs b/crates/ui/src/highlighter/mod.rs index 0d2aa89f..5e4fa9a0 100644 --- a/crates/ui/src/highlighter/mod.rs +++ b/crates/ui/src/highlighter/mod.rs @@ -1,7 +1,9 @@ +mod diagnostics; mod highlighter; mod languages; mod registry; +pub use diagnostics::*; pub use highlighter::*; pub use languages::*; pub use registry::*; diff --git a/crates/ui/src/highlighter/registry.rs b/crates/ui/src/highlighter/registry.rs index fe716de5..34012f03 100644 --- a/crates/ui/src/highlighter/registry.rs +++ b/crates/ui/src/highlighter/registry.rs @@ -6,7 +6,7 @@ use std::{collections::HashMap, ops::Deref, sync::Arc}; use crate::{ highlighter::{languages, Language}, - ActiveTheme, Colorize, ThemeMode, DEFAULT_THEME_COLORS, + ActiveTheme, ThemeMode, DEFAULT_THEME_COLORS, }; pub(super) fn init(cx: &mut App) { @@ -334,7 +334,7 @@ impl StatusColors { pub fn error_background(&self, cx: &App) -> Hsla { let bg = cx.theme().background; self.error_background - .unwrap_or(self.error(cx).lightness(bg.l).saturation(bg.s)) + .unwrap_or(bg.blend(self.error(cx).alpha(0.2))) } #[inline] @@ -351,7 +351,7 @@ impl StatusColors { pub fn warning_background(&self, cx: &App) -> Hsla { let bg = cx.theme().background; self.warning_background - .unwrap_or(self.warning(cx).lightness(bg.l).saturation(bg.s)) + .unwrap_or(bg.blend(self.warning(cx).alpha(0.2))) } #[inline] @@ -368,7 +368,7 @@ impl StatusColors { pub fn info_background(&self, cx: &App) -> Hsla { let bg = cx.theme().background; self.info_background - .unwrap_or(self.info(cx).lightness(bg.l).saturation(bg.s)) + .unwrap_or(bg.blend(self.info(cx).alpha(0.2))) } #[inline] @@ -385,7 +385,7 @@ impl StatusColors { pub fn success_background(&self, cx: &App) -> Hsla { let bg = cx.theme().background; self.success_background - .unwrap_or(self.success(cx).lightness(bg.l).saturation(bg.s)) + .unwrap_or(bg.blend(self.success(cx).alpha(0.2))) } #[inline] @@ -402,7 +402,7 @@ impl StatusColors { pub fn hint_background(&self, cx: &App) -> Hsla { let bg = cx.theme().background; self.hint_background - .unwrap_or(self.hint(cx).lightness(bg.l).saturation(bg.s)) + .unwrap_or(bg.blend(self.hint(cx).alpha(0.2))) } #[inline] diff --git a/crates/ui/src/input/cursor.rs b/crates/ui/src/input/cursor.rs index 6ff9596e..48298baf 100644 --- a/crates/ui/src/input/cursor.rs +++ b/crates/ui/src/input/cursor.rs @@ -1,5 +1,6 @@ -use std::{fmt, ops::Range}; +use std::ops::Range; +/// A selection in the text, represented by start and end byte indices. #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] pub struct Selection { pub start: usize, @@ -37,59 +38,97 @@ impl From for Range { } } -/// Line and column position (1-based) in the source code. +/// Line and column position (0-based) in the source code. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct LineColumn { - /// Line number (1-based) +pub struct Position { + /// Line number (0-based) pub line: usize, - /// Column number (1-based) - pub column: usize, + /// The character offset (0-based) in the line + pub character: usize, } -impl LineColumn { +impl Position { pub fn new(line: usize, column: usize) -> Self { (line, column).into() } } -impl From<(usize, usize)> for LineColumn { +impl From<(usize, usize)> for Position { fn from(value: (usize, usize)) -> Self { Self { - line: value.0.max(1), - column: value.1.max(1), + line: value.0, + character: value.1, } } } -impl fmt::Display for LineColumn { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}:{}", self.line, self.column) +impl From for Position { + fn from(value: lsp_types::Position) -> Self { + Self { + line: value.line as usize, + character: value.character as usize, + } + } +} + +impl From for lsp_types::Position { + fn from(value: Position) -> Self { + Self { + line: value.line as u32, + character: value.character as u32, + } + } +} + +impl std::fmt::Display for Position { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.line + 1, self.character + 1) } } #[cfg(test)] mod tests { - use crate::input::LineColumn; + use crate::input::Position; #[test] fn test_line_column_from_to() { - assert_eq!(LineColumn::new(1, 2), LineColumn { line: 1, column: 2 }); - - assert_eq!(LineColumn::from((1, 2)), LineColumn { line: 1, column: 2 }); assert_eq!( - LineColumn::from((10, 10)), - LineColumn { - line: 10, - column: 10 + Position::new(1, 2), + Position { + line: 1, + character: 2 + } + ); + + assert_eq!( + Position::from((1, 2)), + Position { + line: 1, + character: 2 + } + ); + assert_eq!( + Position::from((10, 10)), + Position { + line: 10, + character: 10 + } + ); + assert_eq!( + Position::from((0, 0)), + Position { + line: 0, + character: 0 } ); - assert_eq!(LineColumn::from((0, 0)), LineColumn { line: 1, column: 1 }); } #[test] - fn test_line_column_display() { - assert_eq!(LineColumn::from((1, 2)).to_string(), "1:2"); - assert_eq!(LineColumn::from((10, 10)).to_string(), "10:10"); - assert_eq!(LineColumn::from((0, 0)).to_string(), "1:1"); + fn test_position_display() { + let pos = Position::new(0, 0); + assert_eq!(pos.to_string(), "1:1"); + + let pos = Position::new(4, 10); + assert_eq!(pos.to_string(), "5:11"); } } diff --git a/crates/ui/src/input/element.rs b/crates/ui/src/input/element.rs index 997da743..d1687d4a 100644 --- a/crates/ui/src/input/element.rs +++ b/crates/ui/src/input/element.rs @@ -10,7 +10,6 @@ use rope::Rope; use smallvec::SmallVec; use crate::{ - highlighter::SyntaxHighlighter, input::{blink_cursor::CURSOR_WIDTH, RopeExt as _}, ActiveTheme as _, Root, }; @@ -414,65 +413,45 @@ impl TextElement { &mut self, visible_range: &Range, _visible_top: Pixels, - visible_start_offset: usize, + visible_byte_range: Range, cx: &mut App, ) -> Option, HighlightStyle)>> { - let theme = cx.theme().highlight_theme.clone(); - self.state.update(cx, |state, cx| match &state.mode { + let state = self.state.read(cx); + let text = &state.text; + + let (highlighter, diagnostics) = match &state.mode { InputMode::CodeEditor { - language, highlighter, - markers, + diagnostics, .. - } => { - // Init highlighter if not initialized - let mut highlighter = highlighter.borrow_mut(); - if highlighter.is_none() { - highlighter.replace(SyntaxHighlighter::new(language, cx)); - }; - let Some(highlighter) = highlighter.as_ref() else { - return None; - }; + } => (highlighter.borrow(), diagnostics), + _ => return None, + }; + let highlighter = highlighter.as_ref()?; - let mut offset = visible_start_offset; - let mut styles = vec![]; + let mut offset = visible_byte_range.start; + let mut styles = vec![]; - for line in state - .text - .lines() - .skip(visible_range.start) - .take(visible_range.len()) - { - // +1 for `\n` - let line_len = line.len() + 1; - let range = offset..offset + line_len; - let line_styles = highlighter.styles(&range, &theme, cx); - styles = gpui::combine_highlights(styles, line_styles).collect(); + for line in text + .lines() + .skip(visible_range.start) + .take(visible_range.len()) + { + // +1 for `\n` + let line_len = line.len() + 1; + let range = offset..offset + line_len; + let line_styles = highlighter.styles(&range, cx); + styles = gpui::combine_highlights(styles, line_styles).collect(); - offset = range.end; - } + offset = range.end; + } - // Combine marker styles - if !markers.is_empty() { - let mut marker_styles = vec![]; - for marker in markers.iter() { - if let Some(range) = &marker.range { - if range.start < visible_start_offset { - continue; - } + let diagnostic_styles = diagnostics.styles_for_range(&visible_byte_range, cx); - marker_styles - .push((range.clone(), marker.severity.highlight_style(&theme, cx))); - } - } + // Combine marker styles + styles = gpui::combine_highlights(diagnostic_styles, styles).collect(); - styles = gpui::combine_highlights(marker_styles, styles).collect(); - } - - Some(styles) - } - _ => None, - }) + Some(styles) } } @@ -584,9 +563,16 @@ impl Element for TextElement { let (visible_range, visible_top) = self.calculate_visible_range(&state, line_height, bounds.size.height); let visible_start_offset = state.text.line_start_offset(visible_range.start); + let visible_end_offset = state + .text + .line_end_offset(visible_range.end.saturating_sub(1)); - let highlight_styles = - self.highlight_lines(&visible_range, visible_top, visible_start_offset, cx); + let highlight_styles = self.highlight_lines( + &visible_range, + visible_top, + visible_start_offset..visible_end_offset, + cx, + ); let state = self.state.read(cx); let multi_line = state.mode.is_multi_line(); diff --git a/crates/ui/src/input/hover_popover.rs b/crates/ui/src/input/hover_popover.rs index d7a4c763..2305d027 100644 --- a/crates/ui/src/input/hover_popover.rs +++ b/crates/ui/src/input/hover_popover.rs @@ -5,25 +5,25 @@ use gpui::{ InteractiveElement, IntoElement, ParentElement as _, Pixels, Point, Render, Styled, Window, }; -use crate::{ - input::{InputState, Marker}, - text::TextView, - ActiveTheme as _, -}; +use crate::{highlighter::DiagnosticEntry, input::InputState, text::TextView, ActiveTheme as _}; pub struct DiagnosticPopover { state: Entity, - pub(super) marker: Rc, + pub(super) diagnostic: Rc, bounds: Bounds, open: bool, } impl DiagnosticPopover { - pub fn new(marker: &Marker, state: Entity, cx: &mut App) -> Entity { - let marker = Rc::new(marker.clone()); + pub fn new( + diagnostic: &DiagnosticEntry, + state: Entity, + cx: &mut App, + ) -> Entity { + let diagnostic = Rc::new(diagnostic.clone()); cx.new(|_| Self { - marker, + diagnostic, state, bounds: Bounds::default(), open: true, @@ -31,19 +31,13 @@ impl DiagnosticPopover { } fn origin(&self, cx: &App) -> Option> { - let Some(range) = self.marker.range.as_ref() else { - return None; - }; - let Some(last_layout) = self.state.read(cx).last_layout.as_ref() else { + let state = self.state.read(cx); + let Some(last_layout) = state.last_layout.as_ref() else { return None; }; let line_number_width = last_layout.line_number_width; - - let (_, _, start_pos) = self - .state - .read(cx) - .line_and_position_for_offset(range.start); + let (_, _, start_pos) = state.line_and_position_for_offset(self.diagnostic.range.start); start_pos.map(|pos| pos + Point::new(line_number_width, px(0.))) } @@ -82,16 +76,15 @@ impl Render for DiagnosticPopover { } let view = cx.entity(); - let theme = &cx.theme().highlight_theme; - let message = self.marker.message.clone(); + let message = self.diagnostic.message.clone(); let Some(pos) = self.origin(cx) else { return Empty.into_any_element(); }; let (border, bg, fg) = ( - self.marker.severity.border(theme, cx), - self.marker.severity.bg(theme, cx), - self.marker.severity.fg(theme, cx), + self.diagnostic.severity.border(cx), + self.diagnostic.severity.bg(cx), + self.diagnostic.severity.fg(cx), ); let scroll_origin = self.state.read(cx).scroll_handle.offset(); @@ -109,14 +102,14 @@ impl Render for DiagnosticPopover { .px_1() .py_0p5() .text_xs() + .max_w(max_width) .bg(bg) - .w(max_width) .text_color(fg) .border_1() .border_color(border) .rounded(cx.theme().radius) - .shadow_xs() - .child(TextView::markdown("message", message, window, cx)) + .shadow_md() + .child(TextView::markdown("message", message, window, cx).selectable()) .child( canvas( move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds), diff --git a/crates/ui/src/input/marker.rs b/crates/ui/src/input/marker.rs deleted file mode 100644 index 7e6c441f..00000000 --- a/crates/ui/src/input/marker.rs +++ /dev/null @@ -1,113 +0,0 @@ -use crate::{ - highlighter::HighlightTheme, - input::{InputState, LineColumn, RopeExt}, -}; -use gpui::{px, App, HighlightStyle, Hsla, SharedString, UnderlineStyle}; -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, - pub(super) range: Option>, - /// 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, - start: impl Into, - end: impl Into, - message: impl Into, - ) -> Self { - Self { - severity: severity.into(), - start: start.into(), - end: end.into(), - message: message.into(), - range: None, - } - } - - /// Prepare the marker to convert line, column to byte offsets. - pub(super) fn prepare(&mut self, state: &InputState) { - let start = state.text.line_column_to_offset(&self.start); - let end = state.text.line_column_to_offset(&self.end); - - self.range = Some(start..end); - } -} - -/// 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 { - pub(super) fn bg(&self, theme: &HighlightTheme, cx: &App) -> Hsla { - match self { - Self::Error => theme.style.status.error_background(cx), - Self::Warning => theme.style.status.warning_background(cx), - Self::Info => theme.style.status.info_background(cx), - Self::Hint => theme.style.status.hint_background(cx), - } - } - - pub(super) fn fg(&self, theme: &HighlightTheme, cx: &App) -> Hsla { - match self { - Self::Error => theme.style.status.error(cx), - Self::Warning => theme.style.status.warning(cx), - Self::Info => theme.style.status.info(cx), - Self::Hint => theme.style.status.hint(cx), - } - } - - pub(super) fn border(&self, theme: &HighlightTheme, cx: &App) -> Hsla { - match self { - Self::Error => theme.style.status.error_border(cx), - Self::Warning => theme.style.status.warning_border(cx), - Self::Info => theme.style.status.info_border(cx), - Self::Hint => theme.style.status.hint_border(cx), - } - } - - pub(super) fn highlight_style(&self, theme: &HighlightTheme, cx: &App) -> HighlightStyle { - let color = match self { - Self::Error => Some(theme.style.status.error(cx)), - Self::Warning => Some(theme.style.status.warning(cx)), - Self::Info => Some(theme.style.status.info(cx)), - Self::Hint => Some(theme.style.status.hint(cx)), - }; - - let mut style = HighlightStyle::default(); - style.underline = Some(UnderlineStyle { - color: color, - thickness: px(1.), - wavy: true, - }); - - style - } -} diff --git a/crates/ui/src/input/mod.rs b/crates/ui/src/input/mod.rs index 65e92fdf..ef037f77 100644 --- a/crates/ui/src/input/mod.rs +++ b/crates/ui/src/input/mod.rs @@ -4,7 +4,6 @@ mod clear_button; mod cursor; mod element; mod hover_popover; -mod marker; mod mask_pattern; mod mode; mod number_input; @@ -16,7 +15,6 @@ mod text_wrapper; pub(crate) use clear_button::*; pub use cursor::*; -pub use marker::*; pub use mask_pattern::MaskPattern; pub use mode::TabSize; pub use number_input::{NumberInput, NumberInputEvent, StepAction}; diff --git a/crates/ui/src/input/mode.rs b/crates/ui/src/input/mode.rs index 0699609c..ce3c6f54 100644 --- a/crates/ui/src/input/mode.rs +++ b/crates/ui/src/input/mode.rs @@ -5,7 +5,8 @@ use gpui::{App, SharedString}; use rope::Rope; use tree_sitter::{InputEdit, Point}; -use crate::{highlighter::SyntaxHighlighter, input::marker::Marker}; +use crate::highlighter::DiagnosticSet; +use crate::highlighter::SyntaxHighlighter; use super::text_wrapper::TextWrapper; @@ -56,7 +57,7 @@ pub enum InputMode { line_number: bool, language: SharedString, highlighter: Rc>>, - markers: Rc>, + diagnostics: DiagnosticSet, }, } @@ -223,42 +224,20 @@ impl InputMode { } } - pub(super) fn clear_markers(&mut self) { - match self { - InputMode::CodeEditor { markers, .. } => *markers = Rc::new(vec![]), - _ => {} - } - } - #[allow(unused)] - pub(super) fn markers(&self) -> Option<&Rc>> { + pub(super) fn diagnostics(&self) -> Option<&DiagnosticSet> { match self { - InputMode::CodeEditor { markers, .. } => Some(markers), + InputMode::CodeEditor { diagnostics, .. } => Some(diagnostics), _ => None, } } - pub(super) fn set_markers(&mut self, new_markers: Vec) { + pub(super) fn diagnostics_mut(&mut self) -> Option<&mut DiagnosticSet> { match self { - InputMode::CodeEditor { markers, .. } => *markers = Rc::new(new_markers), - _ => {} + InputMode::CodeEditor { diagnostics, .. } => Some(diagnostics), + _ => None, } } - - pub(super) fn marker_for_offset(&self, offset: usize) -> Option<&Marker> { - let Some(markers) = self.markers() else { - return None; - }; - - for marker in markers.iter() { - if let Some(range) = marker.range.as_ref() { - if range.contains(&offset) { - return Some(marker); - } - } - } - None - } } #[cfg(test)] diff --git a/crates/ui/src/input/rope_ext.rs b/crates/ui/src/input/rope_ext.rs index 202c4cb7..0807cdca 100644 --- a/crates/ui/src/input/rope_ext.rs +++ b/crates/ui/src/input/rope_ext.rs @@ -1,6 +1,6 @@ use rope::{Point, Rope}; -use crate::input::LineColumn; +use crate::input::Position; /// An extension trait for `Rope` to provide additional utility methods. pub trait RopeExt { @@ -38,11 +38,11 @@ pub trait RopeExt { /// If the offset is out of bounds, return None. fn char_at(&self, offset: usize) -> Option; - /// Get the byte offset from the given `LineColumn` (1-based). - fn line_column_to_offset(&self, line_col: &LineColumn) -> usize; + /// Get the byte offset from the given line, column [`Position`] (0-based). + fn position_to_offset(&self, line_col: &Position) -> usize; - /// Get the `LineColumn` (1-based) from the given byte offset. - fn offset_to_line_column(&self, offset: usize) -> LineColumn; + /// Get the line, column [`Position`] (0-based) from the given byte offset. + fn offset_to_position(&self, offset: usize) -> Position; } /// An iterator over the lines of a `Rope`. @@ -106,22 +106,21 @@ impl RopeExt for Rope { self.point_to_offset(Point::new(row, 0)) } - fn line_column_to_offset(&self, line_col: &LineColumn) -> usize { - let row = line_col.line.saturating_sub(1); - let col = line_col.column.saturating_sub(1); - - let line = self.line(row); - self.line_start_offset(row) + line.chars().take(col).map(|c| c.len_utf8()).sum::() + fn position_to_offset(&self, pos: &Position) -> usize { + let line = self.line(pos.line); + self.line_start_offset(pos.line) + + line + .chars() + .take(pos.character) + .map(|c| c.len_utf8()) + .sum::() } - fn offset_to_line_column(&self, offset: usize) -> LineColumn { + fn offset_to_position(&self, offset: usize) -> Position { let point = self.offset_to_point(offset); let line = self.line(point.row as usize); - let column = line.slice(0..point.column as usize).chars().count(); - LineColumn { - line: point.row as usize + 1, - column: column + 1, - } + let character = line.slice(0..point.column as usize).chars().count(); + Position::new(point.row as usize, character) } fn line_end_offset(&self, row: usize) -> usize { @@ -161,7 +160,7 @@ impl RopeExt for Rope { mod tests { use rope::Rope; - use crate::input::{LineColumn, RopeExt as _}; + use crate::input::{Position, RopeExt as _}; #[test] fn test_line() { @@ -237,26 +236,23 @@ mod tests { #[test] fn test_line_column() { let rope = Rope::from("a 中文🎉 test\nRope"); + assert_eq!(rope.position_to_offset(&Position::new(0, 3)), "a 中".len()); assert_eq!( - rope.line_column_to_offset(&LineColumn::new(1, 4)), - "a 中".len() - ); - assert_eq!( - rope.line_column_to_offset(&LineColumn::new(1, 6)), + rope.position_to_offset(&Position::new(0, 5)), "a 中文🎉".len() ); assert_eq!( - rope.line_column_to_offset(&LineColumn::new(2, 2)), + rope.position_to_offset(&Position::new(1, 1)), "a 中文🎉 test\nR".len() ); assert_eq!( - rope.offset_to_line_column("a 中文🎉 test\nR".len()), - LineColumn::new(2, 2) + rope.offset_to_position("a 中文🎉 test\nR".len()), + Position::new(1, 1) ); assert_eq!( - rope.offset_to_line_column("a 中文🎉".len()), - LineColumn::new(1, 6) + rope.offset_to_position("a 中文🎉".len()), + Position::new(0, 5) ); } diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index 728176d1..b66817f7 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -29,10 +29,9 @@ use super::{ number_input, text_wrapper::TextWrapper, }; -use crate::input::hover_popover::DiagnosticPopover; -use crate::input::marker::Marker; -use crate::input::text_wrapper::LineItem; -use crate::input::{LineColumn, RopeExt as _, Selection}; +use crate::input::{hover_popover::DiagnosticPopover, Position}; +use crate::input::{RopeExt as _, Selection}; +use crate::{highlighter::DiagnosticSet, input::text_wrapper::LineItem}; use crate::{history::History, scroll::ScrollbarState, Root}; #[derive(Action, Clone, PartialEq, Eq, Deserialize)] @@ -400,7 +399,7 @@ impl InputState { language, highlighter: Rc::new(RefCell::new(None)), line_number: true, - markers: Rc::new(vec![]), + diagnostics: DiagnosticSet::default(), }; self } @@ -490,15 +489,14 @@ 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, markers: Vec, _: &mut Context) { - let mut markers = markers; - for marker in &mut markers { - marker.prepare(self); - } - self.mode.set_markers(markers); + #[inline] + pub fn diagnostics(&self) -> Option<&DiagnosticSet> { + self.mode.diagnostics() + } + + #[inline] + pub fn diagnostics_mut(&mut self) -> Option<&mut DiagnosticSet> { + self.mode.diagnostics_mut() } /// Set placeholder @@ -730,6 +728,9 @@ impl InputState { pub fn default_value(mut self, value: impl Into) -> Self { let text: SharedString = value.into(); self.text = Rope::from(text.as_str()); + if let Some(diagnostics) = self.mode.diagnostics_mut() { + diagnostics.reset(&self.text) + } self.text_wrapper.set_default_text(&self.text); self } @@ -744,34 +745,26 @@ impl InputState { self.mask_pattern.unmask(&self.text.to_string()).into() } - /// Return the (1-based) line and column of the cursor. - pub fn line_column(&self) -> LineColumn { + /// Return the (0-based) [`Position`] of the cursor. + pub fn cursor_position(&self) -> Position { let offset = self.cursor(); - self.text.offset_to_line_column(offset) + self.text.offset_to_position(offset) } - /// Set (1-based) line and column of the cursor. + /// Set (0-based) [`Position`] of the cursor. /// /// This will move the cursor to the specified line and column, and update the selection range. - /// - /// - The `column` is optional, if it is `None`, it will return the start of the line. - /// - If the `line` is 0, it will return 0. - /// - If the `line` is greater than the number of lines, it will return - /// the length of the text. - /// - /// Ignore, if the line, column is invalid. - pub fn go_to_line( + pub fn set_cursor_position( &mut self, - line: usize, - column: Option, + position: impl Into, window: &mut Window, cx: &mut Context, ) { + let position: Position = position.into(); let max_point = self.text.max_point(); - let row = line.saturating_sub(1).min(max_point.row as usize); - let col = column - .unwrap_or(1) - .saturating_sub(1) + let row = position.line.min(max_point.row as usize); + let col = position + .character .min(self.text.line_len(row as u32) as usize); let offset = self @@ -1469,9 +1462,13 @@ impl InputState { if self.mode.is_code_editor() { // Show diagnostic popover on mouse move let offset = self.index_for_mouse_position(event.position, window, cx); - if let Some(marker) = self.mode.marker_for_offset(offset) { + if let Some(diagnostic) = self + .mode + .diagnostics() + .and_then(|set| set.for_offset(offset)) + { if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() { - if diagnostic_popover.read(cx).marker.range == marker.range { + if diagnostic_popover.read(cx).diagnostic.range == diagnostic.range { diagnostic_popover.update(cx, |this, cx| { this.show(cx); }); @@ -1480,7 +1477,7 @@ impl InputState { } } - self.diagnostic_popover = Some(DiagnosticPopover::new(marker, cx.entity(), cx)); + self.diagnostic_popover = Some(DiagnosticPopover::new(diagnostic, cx.entity(), cx)); cx.notify(); } else { if let Some(diagnostic_popover) = self.diagnostic_popover.as_mut() { @@ -2109,8 +2106,9 @@ impl EntityInputHandler for InputState { } self.push_history(&old_text, &range, &new_text); - - self.mode.clear_markers(); + if let Some(diagnostics) = self.mode.diagnostics_mut() { + diagnostics.reset(&self.text) + } self.text_wrapper.update(&self.text, false, cx); self.mode .update_highlighter(&range, &self.text, &new_text, true, cx); @@ -2152,7 +2150,9 @@ impl EntityInputHandler for InputState { } self.push_history(&old_text, &range, new_text); - self.mode.clear_markers(); + if let Some(diagnostics) = self.mode.diagnostics_mut() { + diagnostics.reset(&self.text) + } self.text_wrapper.update(&self.text, false, cx); self.mode .update_highlighter(&range, &self.text, &new_text, true, cx); diff --git a/crates/ui/src/text/node.rs b/crates/ui/src/text/node.rs index 23c85c9f..c2e3de8f 100644 --- a/crates/ui/src/text/node.rs +++ b/crates/ui/src/text/node.rs @@ -288,12 +288,11 @@ impl CodeBlock { _: &TextViewStyle, cx: &App, ) -> Self { - let theme = cx.theme().highlight_theme.clone(); let mut styles = vec![]; if let Some(lang) = &lang { let mut highlighter = SyntaxHighlighter::new(&lang, cx); highlighter.update(None, &Rope::from(code.as_str())); - styles = highlighter.styles(&(0..code.len()), &theme, cx); + styles = highlighter.styles(&(0..code.len()), cx); }; let state = InlineState::default();