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:
parent
3fa996c331
commit
da85754b96
19 changed files with 665 additions and 385 deletions
23
Cargo.lock
generated
23
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
388
crates/ui/src/highlighter/diagnostics.rs
Normal file
388
crates/ui/src/highlighter/diagnostics.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
mod diagnostics;
|
||||
mod highlighter;
|
||||
mod languages;
|
||||
mod registry;
|
||||
|
||||
pub use diagnostics::*;
|
||||
pub use highlighter::*;
|
||||
pub use languages::*;
|
||||
pub use registry::*;
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue