input: Refactor diagnostics. (#1240)

<img width="613" height="530" alt="image"
src="https://github.com/user-attachments/assets/be86658d-706c-411c-8ba6-09b495208051"
/>

## 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<usize>, window: &mut Window, cx: &mut Context<Self>)
+ pub fn set_cursor_position(&mut self, position: impl Into<Position>, window: &mut Window, cx: &mut Context<Self>)
```
This commit is contained in:
Jason Lee 2025-09-11 16:54:40 +08:00 committed by GitHub
parent 3fa996c331
commit da85754b96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 665 additions and 385 deletions

23
Cargo.lock generated
View file

@ -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"

View file

@ -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<Self>) {
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<Self>) {
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<Self>) {
// 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<Self>) -> 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))
}),
),

View file

@ -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")

View file

@ -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,

View file

@ -144,7 +144,7 @@ impl Focusable for TextareaStory {
impl Render for TextareaStory {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> 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)),
),
),
)

View file

@ -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 }

View file

@ -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<Position>,
/// 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<SharedString>,
pub code_description: Option<CodeDescription>,
/// A human-readable string describing the source of this
/// diagnostic, e.g. 'typescript' or 'super lint'.
pub source: Option<SharedString>,
/// 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<Vec<DiagnosticRelatedInformation>>,
/// Additional metadata about the diagnostic.
pub tags: Option<Vec<DiagnosticTag>>,
/// A data entry field that is preserved between a `textDocument/publishDiagnostics`
/// notification and `textDocument/codeAction` request.
///
/// @since 3.16.0
pub data: Option<serde_json::Value>,
}
impl From<lsp_types::Diagnostic> 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<lsp_types::DiagnosticSeverity> 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<impl Into<Position>>, message: impl Into<SharedString>) -> Self {
Self {
range: range.start.into()..range.end.into(),
message: message.into(),
..Default::default()
}
}
pub fn with_severity(mut self, severity: impl Into<DiagnosticSeverity>) -> Self {
self.severity = severity.into();
self
}
pub fn with_code(mut self, code: impl Into<SharedString>) -> Self {
self.code = Some(code.into());
self
}
pub fn with_source(mut self, source: impl Into<SharedString>) -> 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<usize>,
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<DiagnosticEntry>,
}
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<I>(&mut self, diagnostics: I)
where
I: IntoIterator<Item = Diagnostic>,
{
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<usize>) -> impl Iterator<Item = &DiagnosticEntry> {
let mut cursor = self.diagnostics.cursor::<DiagnosticSummary>(&());
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<usize>,
cx: &App,
) -> Vec<(Range<usize>, 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<Item = &DiagnosticEntry> {
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::<Vec<_>>();
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::<Vec<_>>();
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);
}
}

View file

@ -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<usize>,
theme: &HighlightTheme,
cx: &App,
) -> Vec<(Range<usize>, HighlightStyle)> {
let theme = &cx.theme().highlight_theme;
let mut styles = vec![];
let start_offset = range.start;

View file

@ -1,7 +1,9 @@
mod diagnostics;
mod highlighter;
mod languages;
mod registry;
pub use diagnostics::*;
pub use highlighter::*;
pub use languages::*;
pub use registry::*;

View file

@ -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]

View file

@ -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<Selection> for Range<usize> {
}
}
/// 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<lsp_types::Position> for Position {
fn from(value: lsp_types::Position) -> Self {
Self {
line: value.line as usize,
character: value.character as usize,
}
}
}
impl From<Position> 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");
}
}

View file

@ -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<usize>,
_visible_top: Pixels,
visible_start_offset: usize,
visible_byte_range: Range<usize>,
cx: &mut App,
) -> Option<Vec<(Range<usize>, 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();

View file

@ -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<InputState>,
pub(super) marker: Rc<Marker>,
pub(super) diagnostic: Rc<DiagnosticEntry>,
bounds: Bounds<Pixels>,
open: bool,
}
impl DiagnosticPopover {
pub fn new(marker: &Marker, state: Entity<InputState>, cx: &mut App) -> Entity<Self> {
let marker = Rc::new(marker.clone());
pub fn new(
diagnostic: &DiagnosticEntry,
state: Entity<InputState>,
cx: &mut App,
) -> Entity<Self> {
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<Point<Pixels>> {
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),

View file

@ -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<Range<usize>>,
/// 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(),
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
}
}

View file

@ -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};

View file

@ -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<RefCell<Option<SyntaxHighlighter>>>,
markers: Rc<Vec<Marker>>,
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<Vec<Marker>>> {
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<Marker>) {
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)]

View file

@ -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<char>;
/// 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::<usize>()
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::<usize>()
}
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)
);
}

View file

@ -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<Marker>, _: &mut Context<Self>) {
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<SharedString>) -> 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<usize>,
position: impl Into<Position>,
window: &mut Window,
cx: &mut Context<Self>,
) {
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);

View file

@ -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();