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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8"
|
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]]
|
[[package]]
|
||||||
name = "flume"
|
name = "flume"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
|
|
@ -3189,6 +3198,7 @@ dependencies = [
|
||||||
"html5ever 0.27.0",
|
"html5ever 0.27.0",
|
||||||
"indoc",
|
"indoc",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
|
"lsp-types",
|
||||||
"markdown",
|
"markdown",
|
||||||
"markup5ever_rcdom",
|
"markup5ever_rcdom",
|
||||||
"notify",
|
"notify",
|
||||||
|
|
@ -4348,6 +4358,19 @@ version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
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]]
|
[[package]]
|
||||||
name = "lyon"
|
name = "lyon"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ use gpui_component::{
|
||||||
button::{Button, ButtonVariants as _},
|
button::{Button, ButtonVariants as _},
|
||||||
dropdown::{Dropdown, DropdownEvent, DropdownState},
|
dropdown::{Dropdown, DropdownEvent, DropdownState},
|
||||||
h_flex,
|
h_flex,
|
||||||
highlighter::{Language, LanguageConfig, LanguageRegistry},
|
highlighter::{Diagnostic, DiagnosticSeverity, Language, LanguageConfig, LanguageRegistry},
|
||||||
input::{InputEvent, InputState, Marker, TabSize, TextInput},
|
input::{self, InputEvent, InputState, TabSize, TextInput},
|
||||||
v_flex, ActiveTheme, ContextModal, IconName, IndexPath, Selectable, Sizable,
|
v_flex, ActiveTheme, ContextModal, IconName, IndexPath, Selectable, Sizable,
|
||||||
};
|
};
|
||||||
use story::Assets;
|
use story::Assets;
|
||||||
|
|
@ -132,8 +132,8 @@ impl Example {
|
||||||
});
|
});
|
||||||
|
|
||||||
let _subscribes = vec![
|
let _subscribes = vec![
|
||||||
cx.subscribe(&editor, |_, _, _: &InputEvent, cx| {
|
cx.subscribe(&editor, |this, _, _: &InputEvent, cx| {
|
||||||
cx.notify();
|
this.lint_document(cx);
|
||||||
}),
|
}),
|
||||||
cx.subscribe(
|
cx.subscribe(
|
||||||
&language_state,
|
&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>) {
|
fn update_highlighter(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if !self.need_update {
|
if !self.need_update {
|
||||||
return;
|
return;
|
||||||
|
|
@ -203,7 +185,8 @@ impl Example {
|
||||||
|
|
||||||
window.open_modal(cx, move |modal, window, cx| {
|
window.open_modal(cx, move |modal, window, cx| {
|
||||||
input_state.update(cx, |state, 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);
|
state.focus(window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -224,10 +207,12 @@ impl Example {
|
||||||
let Some(line) = parts.next().and_then(|l| l) else {
|
let Some(line) = parts.next().and_then(|l| l) else {
|
||||||
return false;
|
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| {
|
editor.update(cx, |state, cx| {
|
||||||
state.go_to_line(line, column, window, cx);
|
state.set_cursor_position(position, window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
true
|
true
|
||||||
|
|
@ -243,12 +228,40 @@ impl Example {
|
||||||
});
|
});
|
||||||
cx.notify();
|
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 {
|
impl Render for Example {
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
self.update_highlighter(window, cx);
|
self.update_highlighter(window, cx);
|
||||||
self.set_markers(window, cx);
|
|
||||||
|
|
||||||
v_flex().size_full().child(
|
v_flex().size_full().child(
|
||||||
v_flex()
|
v_flex()
|
||||||
|
|
@ -305,13 +318,13 @@ impl Render for Example {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child({
|
.child({
|
||||||
let loc = self.editor.read(cx).line_column();
|
let position = self.editor.read(cx).cursor_position();
|
||||||
let cursor = self.editor.read(cx).cursor();
|
let cursor = self.editor.read(cx).cursor();
|
||||||
|
|
||||||
Button::new("line-column")
|
Button::new("line-column")
|
||||||
.ghost()
|
.ghost()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.label(format!("{} ({} c)", loc, cursor))
|
.label(format!("{} ({} byte)", position, cursor))
|
||||||
.on_click(cx.listener(Self::go_to_line))
|
.on_click(cx.listener(Self::go_to_line))
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ impl Example {
|
||||||
|
|
||||||
window.open_modal(cx, move |modal, window, cx| {
|
window.open_modal(cx, move |modal, window, cx| {
|
||||||
input_state.update(cx, |state, 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);
|
state.focus(window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -76,10 +76,11 @@ impl Example {
|
||||||
let Some(line) = parts.next().and_then(|l| l) else {
|
let Some(line) = parts.next().and_then(|l| l) else {
|
||||||
return false;
|
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| {
|
editor.update(cx, |state, cx| {
|
||||||
state.go_to_line(line, column, window, cx);
|
state.set_cursor_position((line, column), window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
true
|
true
|
||||||
|
|
@ -129,7 +130,7 @@ impl Render for Example {
|
||||||
.on_click(cx.listener(Self::toggle_soft_wrap))
|
.on_click(cx.listener(Self::toggle_soft_wrap))
|
||||||
}))
|
}))
|
||||||
.child({
|
.child({
|
||||||
let loc = self.editor.read(cx).line_column();
|
let loc = self.editor.read(cx).cursor_position();
|
||||||
let cursor = self.editor.read(cx).cursor();
|
let cursor = self.editor.read(cx).cursor();
|
||||||
|
|
||||||
Button::new("line-column")
|
Button::new("line-column")
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use gpui::*;
|
use gpui::*;
|
||||||
use gpui_component::{
|
use gpui_component::{
|
||||||
highlighter::{HighlightTheme, Language},
|
highlighter::{HighlightTheme, Language},
|
||||||
input::{InputEvent, InputState, Marker, MarkerSeverity, TabSize, TextInput},
|
input::{InputEvent, InputState, TabSize, TextInput},
|
||||||
resizable::{h_resizable, resizable_panel, ResizableState},
|
resizable::{h_resizable, resizable_panel, ResizableState},
|
||||||
text::{TextView, TextViewStyle},
|
text::{TextView, TextViewStyle},
|
||||||
ActiveTheme as _,
|
ActiveTheme as _,
|
||||||
|
|
@ -31,32 +31,7 @@ impl Example {
|
||||||
});
|
});
|
||||||
let resizable_state = ResizableState::new(cx);
|
let resizable_state = ResizableState::new(cx);
|
||||||
|
|
||||||
let _subscriptions = vec![cx.subscribe(&input_state, |_, input, _: &InputEvent, cx| {
|
let _subscriptions = vec![cx.subscribe(&input_state, |_, _, _: &InputEvent, _| {})];
|
||||||
// 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();
|
|
||||||
})];
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
resizable_state,
|
resizable_state,
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ impl Focusable for TextareaStory {
|
||||||
|
|
||||||
impl Render for TextareaStory {
|
impl Render for TextareaStory {
|
||||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
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()
|
v_flex()
|
||||||
.key_context(CONTEXT)
|
.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"
|
chrono = "0.4.38"
|
||||||
|
|
||||||
# Code Editor
|
# Code Editor
|
||||||
|
lsp-types = "0.97.0"
|
||||||
tree-sitter = "0.25.4"
|
tree-sitter = "0.25.4"
|
||||||
tree-sitter-json = "0.24.8"
|
tree-sitter-json = "0.24.8"
|
||||||
tree-sitter-bash = { version = "0.23.3", optional = true }
|
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 _, ActiveTheme};
|
||||||
use crate::{highlighter::LanguageRegistry, input::RopeExt as _};
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use gpui::{App, HighlightStyle, SharedString};
|
use gpui::{App, HighlightStyle, SharedString};
|
||||||
|
|
@ -551,9 +550,10 @@ impl SyntaxHighlighter {
|
||||||
pub(crate) fn styles(
|
pub(crate) fn styles(
|
||||||
&self,
|
&self,
|
||||||
range: &Range<usize>,
|
range: &Range<usize>,
|
||||||
theme: &HighlightTheme,
|
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> Vec<(Range<usize>, HighlightStyle)> {
|
) -> Vec<(Range<usize>, HighlightStyle)> {
|
||||||
|
let theme = &cx.theme().highlight_theme;
|
||||||
|
|
||||||
let mut styles = vec![];
|
let mut styles = vec![];
|
||||||
let start_offset = range.start;
|
let start_offset = range.start;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
|
mod diagnostics;
|
||||||
mod highlighter;
|
mod highlighter;
|
||||||
mod languages;
|
mod languages;
|
||||||
mod registry;
|
mod registry;
|
||||||
|
|
||||||
|
pub use diagnostics::*;
|
||||||
pub use highlighter::*;
|
pub use highlighter::*;
|
||||||
pub use languages::*;
|
pub use languages::*;
|
||||||
pub use registry::*;
|
pub use registry::*;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use std::{collections::HashMap, ops::Deref, sync::Arc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
highlighter::{languages, Language},
|
highlighter::{languages, Language},
|
||||||
ActiveTheme, Colorize, ThemeMode, DEFAULT_THEME_COLORS,
|
ActiveTheme, ThemeMode, DEFAULT_THEME_COLORS,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(super) fn init(cx: &mut App) {
|
pub(super) fn init(cx: &mut App) {
|
||||||
|
|
@ -334,7 +334,7 @@ impl StatusColors {
|
||||||
pub fn error_background(&self, cx: &App) -> Hsla {
|
pub fn error_background(&self, cx: &App) -> Hsla {
|
||||||
let bg = cx.theme().background;
|
let bg = cx.theme().background;
|
||||||
self.error_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]
|
#[inline]
|
||||||
|
|
@ -351,7 +351,7 @@ impl StatusColors {
|
||||||
pub fn warning_background(&self, cx: &App) -> Hsla {
|
pub fn warning_background(&self, cx: &App) -> Hsla {
|
||||||
let bg = cx.theme().background;
|
let bg = cx.theme().background;
|
||||||
self.warning_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]
|
#[inline]
|
||||||
|
|
@ -368,7 +368,7 @@ impl StatusColors {
|
||||||
pub fn info_background(&self, cx: &App) -> Hsla {
|
pub fn info_background(&self, cx: &App) -> Hsla {
|
||||||
let bg = cx.theme().background;
|
let bg = cx.theme().background;
|
||||||
self.info_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]
|
#[inline]
|
||||||
|
|
@ -385,7 +385,7 @@ impl StatusColors {
|
||||||
pub fn success_background(&self, cx: &App) -> Hsla {
|
pub fn success_background(&self, cx: &App) -> Hsla {
|
||||||
let bg = cx.theme().background;
|
let bg = cx.theme().background;
|
||||||
self.success_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]
|
#[inline]
|
||||||
|
|
@ -402,7 +402,7 @@ impl StatusColors {
|
||||||
pub fn hint_background(&self, cx: &App) -> Hsla {
|
pub fn hint_background(&self, cx: &App) -> Hsla {
|
||||||
let bg = cx.theme().background;
|
let bg = cx.theme().background;
|
||||||
self.hint_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]
|
#[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)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
|
||||||
pub struct Selection {
|
pub struct Selection {
|
||||||
pub start: usize,
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
pub struct LineColumn {
|
pub struct Position {
|
||||||
/// Line number (1-based)
|
/// Line number (0-based)
|
||||||
pub line: usize,
|
pub line: usize,
|
||||||
/// Column number (1-based)
|
/// The character offset (0-based) in the line
|
||||||
pub column: usize,
|
pub character: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LineColumn {
|
impl Position {
|
||||||
pub fn new(line: usize, column: usize) -> Self {
|
pub fn new(line: usize, column: usize) -> Self {
|
||||||
(line, column).into()
|
(line, column).into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<(usize, usize)> for LineColumn {
|
impl From<(usize, usize)> for Position {
|
||||||
fn from(value: (usize, usize)) -> Self {
|
fn from(value: (usize, usize)) -> Self {
|
||||||
Self {
|
Self {
|
||||||
line: value.0.max(1),
|
line: value.0,
|
||||||
column: value.1.max(1),
|
character: value.1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for LineColumn {
|
impl From<lsp_types::Position> for Position {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn from(value: lsp_types::Position) -> Self {
|
||||||
write!(f, "{}:{}", self.line, self.column)
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::input::LineColumn;
|
use crate::input::Position;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_line_column_from_to() {
|
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!(
|
assert_eq!(
|
||||||
LineColumn::from((10, 10)),
|
Position::new(1, 2),
|
||||||
LineColumn {
|
Position {
|
||||||
line: 10,
|
line: 1,
|
||||||
column: 10
|
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]
|
#[test]
|
||||||
fn test_line_column_display() {
|
fn test_position_display() {
|
||||||
assert_eq!(LineColumn::from((1, 2)).to_string(), "1:2");
|
let pos = Position::new(0, 0);
|
||||||
assert_eq!(LineColumn::from((10, 10)).to_string(), "10:10");
|
assert_eq!(pos.to_string(), "1:1");
|
||||||
assert_eq!(LineColumn::from((0, 0)).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 smallvec::SmallVec;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
highlighter::SyntaxHighlighter,
|
|
||||||
input::{blink_cursor::CURSOR_WIDTH, RopeExt as _},
|
input::{blink_cursor::CURSOR_WIDTH, RopeExt as _},
|
||||||
ActiveTheme as _, Root,
|
ActiveTheme as _, Root,
|
||||||
};
|
};
|
||||||
|
|
@ -414,31 +413,26 @@ impl TextElement {
|
||||||
&mut self,
|
&mut self,
|
||||||
visible_range: &Range<usize>,
|
visible_range: &Range<usize>,
|
||||||
_visible_top: Pixels,
|
_visible_top: Pixels,
|
||||||
visible_start_offset: usize,
|
visible_byte_range: Range<usize>,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Option<Vec<(Range<usize>, HighlightStyle)>> {
|
) -> Option<Vec<(Range<usize>, HighlightStyle)>> {
|
||||||
let theme = cx.theme().highlight_theme.clone();
|
let state = self.state.read(cx);
|
||||||
self.state.update(cx, |state, cx| match &state.mode {
|
let text = &state.text;
|
||||||
InputMode::CodeEditor {
|
|
||||||
language,
|
|
||||||
highlighter,
|
|
||||||
markers,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
// 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut offset = visible_start_offset;
|
let (highlighter, diagnostics) = match &state.mode {
|
||||||
|
InputMode::CodeEditor {
|
||||||
|
highlighter,
|
||||||
|
diagnostics,
|
||||||
|
..
|
||||||
|
} => (highlighter.borrow(), diagnostics),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let highlighter = highlighter.as_ref()?;
|
||||||
|
|
||||||
|
let mut offset = visible_byte_range.start;
|
||||||
let mut styles = vec![];
|
let mut styles = vec![];
|
||||||
|
|
||||||
for line in state
|
for line in text
|
||||||
.text
|
|
||||||
.lines()
|
.lines()
|
||||||
.skip(visible_range.start)
|
.skip(visible_range.start)
|
||||||
.take(visible_range.len())
|
.take(visible_range.len())
|
||||||
|
|
@ -446,34 +440,19 @@ impl TextElement {
|
||||||
// +1 for `\n`
|
// +1 for `\n`
|
||||||
let line_len = line.len() + 1;
|
let line_len = line.len() + 1;
|
||||||
let range = offset..offset + line_len;
|
let range = offset..offset + line_len;
|
||||||
let line_styles = highlighter.styles(&range, &theme, cx);
|
let line_styles = highlighter.styles(&range, cx);
|
||||||
styles = gpui::combine_highlights(styles, line_styles).collect();
|
styles = gpui::combine_highlights(styles, line_styles).collect();
|
||||||
|
|
||||||
offset = range.end;
|
offset = range.end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let diagnostic_styles = diagnostics.styles_for_range(&visible_byte_range, cx);
|
||||||
|
|
||||||
// Combine marker styles
|
// Combine marker styles
|
||||||
if !markers.is_empty() {
|
styles = gpui::combine_highlights(diagnostic_styles, styles).collect();
|
||||||
let mut marker_styles = vec![];
|
|
||||||
for marker in markers.iter() {
|
|
||||||
if let Some(range) = &marker.range {
|
|
||||||
if range.start < visible_start_offset {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
marker_styles
|
|
||||||
.push((range.clone(), marker.severity.highlight_style(&theme, cx)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
styles = gpui::combine_highlights(marker_styles, styles).collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(styles)
|
Some(styles)
|
||||||
}
|
}
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) struct PrepaintState {
|
pub(super) struct PrepaintState {
|
||||||
|
|
@ -584,9 +563,16 @@ impl Element for TextElement {
|
||||||
let (visible_range, visible_top) =
|
let (visible_range, visible_top) =
|
||||||
self.calculate_visible_range(&state, line_height, bounds.size.height);
|
self.calculate_visible_range(&state, line_height, bounds.size.height);
|
||||||
let visible_start_offset = state.text.line_start_offset(visible_range.start);
|
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 =
|
let highlight_styles = self.highlight_lines(
|
||||||
self.highlight_lines(&visible_range, visible_top, visible_start_offset, cx);
|
&visible_range,
|
||||||
|
visible_top,
|
||||||
|
visible_start_offset..visible_end_offset,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
let state = self.state.read(cx);
|
let state = self.state.read(cx);
|
||||||
let multi_line = state.mode.is_multi_line();
|
let multi_line = state.mode.is_multi_line();
|
||||||
|
|
|
||||||
|
|
@ -5,25 +5,25 @@ use gpui::{
|
||||||
InteractiveElement, IntoElement, ParentElement as _, Pixels, Point, Render, Styled, Window,
|
InteractiveElement, IntoElement, ParentElement as _, Pixels, Point, Render, Styled, Window,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{highlighter::DiagnosticEntry, input::InputState, text::TextView, ActiveTheme as _};
|
||||||
input::{InputState, Marker},
|
|
||||||
text::TextView,
|
|
||||||
ActiveTheme as _,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct DiagnosticPopover {
|
pub struct DiagnosticPopover {
|
||||||
state: Entity<InputState>,
|
state: Entity<InputState>,
|
||||||
pub(super) marker: Rc<Marker>,
|
pub(super) diagnostic: Rc<DiagnosticEntry>,
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
open: bool,
|
open: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiagnosticPopover {
|
impl DiagnosticPopover {
|
||||||
pub fn new(marker: &Marker, state: Entity<InputState>, cx: &mut App) -> Entity<Self> {
|
pub fn new(
|
||||||
let marker = Rc::new(marker.clone());
|
diagnostic: &DiagnosticEntry,
|
||||||
|
state: Entity<InputState>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Entity<Self> {
|
||||||
|
let diagnostic = Rc::new(diagnostic.clone());
|
||||||
|
|
||||||
cx.new(|_| Self {
|
cx.new(|_| Self {
|
||||||
marker,
|
diagnostic,
|
||||||
state,
|
state,
|
||||||
bounds: Bounds::default(),
|
bounds: Bounds::default(),
|
||||||
open: true,
|
open: true,
|
||||||
|
|
@ -31,19 +31,13 @@ impl DiagnosticPopover {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn origin(&self, cx: &App) -> Option<Point<Pixels>> {
|
fn origin(&self, cx: &App) -> Option<Point<Pixels>> {
|
||||||
let Some(range) = self.marker.range.as_ref() else {
|
let state = self.state.read(cx);
|
||||||
return None;
|
let Some(last_layout) = state.last_layout.as_ref() else {
|
||||||
};
|
|
||||||
let Some(last_layout) = self.state.read(cx).last_layout.as_ref() else {
|
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
let line_number_width = last_layout.line_number_width;
|
let line_number_width = last_layout.line_number_width;
|
||||||
|
let (_, _, start_pos) = state.line_and_position_for_offset(self.diagnostic.range.start);
|
||||||
let (_, _, start_pos) = self
|
|
||||||
.state
|
|
||||||
.read(cx)
|
|
||||||
.line_and_position_for_offset(range.start);
|
|
||||||
|
|
||||||
start_pos.map(|pos| pos + Point::new(line_number_width, px(0.)))
|
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 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 {
|
let Some(pos) = self.origin(cx) else {
|
||||||
return Empty.into_any_element();
|
return Empty.into_any_element();
|
||||||
};
|
};
|
||||||
let (border, bg, fg) = (
|
let (border, bg, fg) = (
|
||||||
self.marker.severity.border(theme, cx),
|
self.diagnostic.severity.border(cx),
|
||||||
self.marker.severity.bg(theme, cx),
|
self.diagnostic.severity.bg(cx),
|
||||||
self.marker.severity.fg(theme, cx),
|
self.diagnostic.severity.fg(cx),
|
||||||
);
|
);
|
||||||
|
|
||||||
let scroll_origin = self.state.read(cx).scroll_handle.offset();
|
let scroll_origin = self.state.read(cx).scroll_handle.offset();
|
||||||
|
|
@ -109,14 +102,14 @@ impl Render for DiagnosticPopover {
|
||||||
.px_1()
|
.px_1()
|
||||||
.py_0p5()
|
.py_0p5()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
|
.max_w(max_width)
|
||||||
.bg(bg)
|
.bg(bg)
|
||||||
.w(max_width)
|
|
||||||
.text_color(fg)
|
.text_color(fg)
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(border)
|
.border_color(border)
|
||||||
.rounded(cx.theme().radius)
|
.rounded(cx.theme().radius)
|
||||||
.shadow_xs()
|
.shadow_md()
|
||||||
.child(TextView::markdown("message", message, window, cx))
|
.child(TextView::markdown("message", message, window, cx).selectable())
|
||||||
.child(
|
.child(
|
||||||
canvas(
|
canvas(
|
||||||
move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds),
|
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 cursor;
|
||||||
mod element;
|
mod element;
|
||||||
mod hover_popover;
|
mod hover_popover;
|
||||||
mod marker;
|
|
||||||
mod mask_pattern;
|
mod mask_pattern;
|
||||||
mod mode;
|
mod mode;
|
||||||
mod number_input;
|
mod number_input;
|
||||||
|
|
@ -16,7 +15,6 @@ mod text_wrapper;
|
||||||
|
|
||||||
pub(crate) use clear_button::*;
|
pub(crate) use clear_button::*;
|
||||||
pub use cursor::*;
|
pub use cursor::*;
|
||||||
pub use marker::*;
|
|
||||||
pub use mask_pattern::MaskPattern;
|
pub use mask_pattern::MaskPattern;
|
||||||
pub use mode::TabSize;
|
pub use mode::TabSize;
|
||||||
pub use number_input::{NumberInput, NumberInputEvent, StepAction};
|
pub use number_input::{NumberInput, NumberInputEvent, StepAction};
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ use gpui::{App, SharedString};
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
use tree_sitter::{InputEdit, Point};
|
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;
|
use super::text_wrapper::TextWrapper;
|
||||||
|
|
||||||
|
|
@ -56,7 +57,7 @@ pub enum InputMode {
|
||||||
line_number: bool,
|
line_number: bool,
|
||||||
language: SharedString,
|
language: SharedString,
|
||||||
highlighter: Rc<RefCell<Option<SyntaxHighlighter>>>,
|
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)]
|
#[allow(unused)]
|
||||||
pub(super) fn markers(&self) -> Option<&Rc<Vec<Marker>>> {
|
pub(super) fn diagnostics(&self) -> Option<&DiagnosticSet> {
|
||||||
match self {
|
match self {
|
||||||
InputMode::CodeEditor { markers, .. } => Some(markers),
|
InputMode::CodeEditor { diagnostics, .. } => Some(diagnostics),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn set_markers(&mut self, new_markers: Vec<Marker>) {
|
pub(super) fn diagnostics_mut(&mut self) -> Option<&mut DiagnosticSet> {
|
||||||
match self {
|
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)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use rope::{Point, Rope};
|
use rope::{Point, Rope};
|
||||||
|
|
||||||
use crate::input::LineColumn;
|
use crate::input::Position;
|
||||||
|
|
||||||
/// An extension trait for `Rope` to provide additional utility methods.
|
/// An extension trait for `Rope` to provide additional utility methods.
|
||||||
pub trait RopeExt {
|
pub trait RopeExt {
|
||||||
|
|
@ -38,11 +38,11 @@ pub trait RopeExt {
|
||||||
/// If the offset is out of bounds, return None.
|
/// If the offset is out of bounds, return None.
|
||||||
fn char_at(&self, offset: usize) -> Option<char>;
|
fn char_at(&self, offset: usize) -> Option<char>;
|
||||||
|
|
||||||
/// Get the byte offset from the given `LineColumn` (1-based).
|
/// Get the byte offset from the given line, column [`Position`] (0-based).
|
||||||
fn line_column_to_offset(&self, line_col: &LineColumn) -> usize;
|
fn position_to_offset(&self, line_col: &Position) -> usize;
|
||||||
|
|
||||||
/// Get the `LineColumn` (1-based) from the given byte offset.
|
/// Get the line, column [`Position`] (0-based) from the given byte offset.
|
||||||
fn offset_to_line_column(&self, offset: usize) -> LineColumn;
|
fn offset_to_position(&self, offset: usize) -> Position;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An iterator over the lines of a `Rope`.
|
/// An iterator over the lines of a `Rope`.
|
||||||
|
|
@ -106,22 +106,21 @@ impl RopeExt for Rope {
|
||||||
self.point_to_offset(Point::new(row, 0))
|
self.point_to_offset(Point::new(row, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn line_column_to_offset(&self, line_col: &LineColumn) -> usize {
|
fn position_to_offset(&self, pos: &Position) -> usize {
|
||||||
let row = line_col.line.saturating_sub(1);
|
let line = self.line(pos.line);
|
||||||
let col = line_col.column.saturating_sub(1);
|
self.line_start_offset(pos.line)
|
||||||
|
+ line
|
||||||
let line = self.line(row);
|
.chars()
|
||||||
self.line_start_offset(row) + line.chars().take(col).map(|c| c.len_utf8()).sum::<usize>()
|
.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 point = self.offset_to_point(offset);
|
||||||
let line = self.line(point.row as usize);
|
let line = self.line(point.row as usize);
|
||||||
let column = line.slice(0..point.column as usize).chars().count();
|
let character = line.slice(0..point.column as usize).chars().count();
|
||||||
LineColumn {
|
Position::new(point.row as usize, character)
|
||||||
line: point.row as usize + 1,
|
|
||||||
column: column + 1,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn line_end_offset(&self, row: usize) -> usize {
|
fn line_end_offset(&self, row: usize) -> usize {
|
||||||
|
|
@ -161,7 +160,7 @@ impl RopeExt for Rope {
|
||||||
mod tests {
|
mod tests {
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
|
|
||||||
use crate::input::{LineColumn, RopeExt as _};
|
use crate::input::{Position, RopeExt as _};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_line() {
|
fn test_line() {
|
||||||
|
|
@ -237,26 +236,23 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_line_column() {
|
fn test_line_column() {
|
||||||
let rope = Rope::from("a 中文🎉 test\nRope");
|
let rope = Rope::from("a 中文🎉 test\nRope");
|
||||||
|
assert_eq!(rope.position_to_offset(&Position::new(0, 3)), "a 中".len());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rope.line_column_to_offset(&LineColumn::new(1, 4)),
|
rope.position_to_offset(&Position::new(0, 5)),
|
||||||
"a 中".len()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
rope.line_column_to_offset(&LineColumn::new(1, 6)),
|
|
||||||
"a 中文🎉".len()
|
"a 中文🎉".len()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rope.line_column_to_offset(&LineColumn::new(2, 2)),
|
rope.position_to_offset(&Position::new(1, 1)),
|
||||||
"a 中文🎉 test\nR".len()
|
"a 中文🎉 test\nR".len()
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rope.offset_to_line_column("a 中文🎉 test\nR".len()),
|
rope.offset_to_position("a 中文🎉 test\nR".len()),
|
||||||
LineColumn::new(2, 2)
|
Position::new(1, 1)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rope.offset_to_line_column("a 中文🎉".len()),
|
rope.offset_to_position("a 中文🎉".len()),
|
||||||
LineColumn::new(1, 6)
|
Position::new(0, 5)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,9 @@ use super::{
|
||||||
number_input,
|
number_input,
|
||||||
text_wrapper::TextWrapper,
|
text_wrapper::TextWrapper,
|
||||||
};
|
};
|
||||||
use crate::input::hover_popover::DiagnosticPopover;
|
use crate::input::{hover_popover::DiagnosticPopover, Position};
|
||||||
use crate::input::marker::Marker;
|
use crate::input::{RopeExt as _, Selection};
|
||||||
use crate::input::text_wrapper::LineItem;
|
use crate::{highlighter::DiagnosticSet, input::text_wrapper::LineItem};
|
||||||
use crate::input::{LineColumn, RopeExt as _, Selection};
|
|
||||||
use crate::{history::History, scroll::ScrollbarState, Root};
|
use crate::{history::History, scroll::ScrollbarState, Root};
|
||||||
|
|
||||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||||
|
|
@ -400,7 +399,7 @@ impl InputState {
|
||||||
language,
|
language,
|
||||||
highlighter: Rc::new(RefCell::new(None)),
|
highlighter: Rc::new(RefCell::new(None)),
|
||||||
line_number: true,
|
line_number: true,
|
||||||
markers: Rc::new(vec![]),
|
diagnostics: DiagnosticSet::default(),
|
||||||
};
|
};
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -490,15 +489,14 @@ impl InputState {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set markers, only for [`InputMode::CodeEditor`] mode.
|
#[inline]
|
||||||
///
|
pub fn diagnostics(&self) -> Option<&DiagnosticSet> {
|
||||||
/// For example to set the diagnostic markers in the code editor.
|
self.mode.diagnostics()
|
||||||
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_mut(&mut self) -> Option<&mut DiagnosticSet> {
|
||||||
|
self.mode.diagnostics_mut()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set placeholder
|
/// Set placeholder
|
||||||
|
|
@ -730,6 +728,9 @@ impl InputState {
|
||||||
pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
|
pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
|
||||||
let text: SharedString = value.into();
|
let text: SharedString = value.into();
|
||||||
self.text = Rope::from(text.as_str());
|
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.text_wrapper.set_default_text(&self.text);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -744,34 +745,26 @@ impl InputState {
|
||||||
self.mask_pattern.unmask(&self.text.to_string()).into()
|
self.mask_pattern.unmask(&self.text.to_string()).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the (1-based) line and column of the cursor.
|
/// Return the (0-based) [`Position`] of the cursor.
|
||||||
pub fn line_column(&self) -> LineColumn {
|
pub fn cursor_position(&self) -> Position {
|
||||||
let offset = self.cursor();
|
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.
|
/// This will move the cursor to the specified line and column, and update the selection range.
|
||||||
///
|
pub fn set_cursor_position(
|
||||||
/// - 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(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
line: usize,
|
position: impl Into<Position>,
|
||||||
column: Option<usize>,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
let position: Position = position.into();
|
||||||
let max_point = self.text.max_point();
|
let max_point = self.text.max_point();
|
||||||
let row = line.saturating_sub(1).min(max_point.row as usize);
|
let row = position.line.min(max_point.row as usize);
|
||||||
let col = column
|
let col = position
|
||||||
.unwrap_or(1)
|
.character
|
||||||
.saturating_sub(1)
|
|
||||||
.min(self.text.line_len(row as u32) as usize);
|
.min(self.text.line_len(row as u32) as usize);
|
||||||
|
|
||||||
let offset = self
|
let offset = self
|
||||||
|
|
@ -1469,9 +1462,13 @@ impl InputState {
|
||||||
if self.mode.is_code_editor() {
|
if self.mode.is_code_editor() {
|
||||||
// Show diagnostic popover on mouse move
|
// Show diagnostic popover on mouse move
|
||||||
let offset = self.index_for_mouse_position(event.position, window, cx);
|
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 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| {
|
diagnostic_popover.update(cx, |this, cx| {
|
||||||
this.show(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();
|
cx.notify();
|
||||||
} else {
|
} else {
|
||||||
if let Some(diagnostic_popover) = self.diagnostic_popover.as_mut() {
|
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.push_history(&old_text, &range, &new_text);
|
||||||
|
if let Some(diagnostics) = self.mode.diagnostics_mut() {
|
||||||
self.mode.clear_markers();
|
diagnostics.reset(&self.text)
|
||||||
|
}
|
||||||
self.text_wrapper.update(&self.text, false, cx);
|
self.text_wrapper.update(&self.text, false, cx);
|
||||||
self.mode
|
self.mode
|
||||||
.update_highlighter(&range, &self.text, &new_text, true, cx);
|
.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.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.text_wrapper.update(&self.text, false, cx);
|
||||||
self.mode
|
self.mode
|
||||||
.update_highlighter(&range, &self.text, &new_text, true, cx);
|
.update_highlighter(&range, &self.text, &new_text, true, cx);
|
||||||
|
|
|
||||||
|
|
@ -288,12 +288,11 @@ impl CodeBlock {
|
||||||
_: &TextViewStyle,
|
_: &TextViewStyle,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let theme = cx.theme().highlight_theme.clone();
|
|
||||||
let mut styles = vec![];
|
let mut styles = vec![];
|
||||||
if let Some(lang) = &lang {
|
if let Some(lang) = &lang {
|
||||||
let mut highlighter = SyntaxHighlighter::new(&lang, cx);
|
let mut highlighter = SyntaxHighlighter::new(&lang, cx);
|
||||||
highlighter.update(None, &Rope::from(code.as_str()));
|
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();
|
let state = InlineState::default();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue