code-editor: Show hover popover for diagnostics. (#924)
<img width="719" alt="image" src="https://github.com/user-attachments/assets/75fa9737-09e1-4baa-b7ac-304e847005c8" />
This commit is contained in:
parent
c3ec1deb97
commit
6aab9d3345
11 changed files with 326 additions and 54 deletions
|
|
@ -145,7 +145,7 @@ impl Example {
|
||||||
vec![
|
vec![
|
||||||
Marker::new("warning", (2, 1), (2, 31), "Import but not used."),
|
Marker::new("warning", (2, 1), (2, 31), "Import but not used."),
|
||||||
Marker::new("error", (16, 10), (16, 46), "Syntax error."),
|
Marker::new("error", (16, 10), (16, 46), "Syntax error."),
|
||||||
Marker::new("info", (25, 10), (25, 20), "This is a info message."),
|
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."),
|
Marker::new("hint", (36, 9), (40, 10), "This is a hint message."),
|
||||||
],
|
],
|
||||||
window,
|
window,
|
||||||
|
|
|
||||||
|
|
@ -247,22 +247,32 @@ pub struct StatusColors {
|
||||||
error: Option<Hsla>,
|
error: Option<Hsla>,
|
||||||
#[serde(rename = "error.background")]
|
#[serde(rename = "error.background")]
|
||||||
error_background: Option<Hsla>,
|
error_background: Option<Hsla>,
|
||||||
|
#[serde(rename = "error.border")]
|
||||||
|
error_border: Option<Hsla>,
|
||||||
#[serde(rename = "warning")]
|
#[serde(rename = "warning")]
|
||||||
warning: Option<Hsla>,
|
warning: Option<Hsla>,
|
||||||
#[serde(rename = "warning.background")]
|
#[serde(rename = "warning.background")]
|
||||||
warning_background: Option<Hsla>,
|
warning_background: Option<Hsla>,
|
||||||
|
#[serde(rename = "warning.border")]
|
||||||
|
warning_border: Option<Hsla>,
|
||||||
#[serde(rename = "info")]
|
#[serde(rename = "info")]
|
||||||
info: Option<Hsla>,
|
info: Option<Hsla>,
|
||||||
#[serde(rename = "info.background")]
|
#[serde(rename = "info.background")]
|
||||||
info_background: Option<Hsla>,
|
info_background: Option<Hsla>,
|
||||||
|
#[serde(rename = "info.border")]
|
||||||
|
info_border: Option<Hsla>,
|
||||||
#[serde(rename = "success")]
|
#[serde(rename = "success")]
|
||||||
success: Option<Hsla>,
|
success: Option<Hsla>,
|
||||||
#[serde(rename = "success.background")]
|
#[serde(rename = "success.background")]
|
||||||
success_background: Option<Hsla>,
|
success_background: Option<Hsla>,
|
||||||
|
#[serde(rename = "success.border")]
|
||||||
|
success_border: Option<Hsla>,
|
||||||
#[serde(rename = "hint")]
|
#[serde(rename = "hint")]
|
||||||
hint: Option<Hsla>,
|
hint: Option<Hsla>,
|
||||||
#[serde(rename = "hint.background")]
|
#[serde(rename = "hint.background")]
|
||||||
hint_background: Option<Hsla>,
|
hint_background: Option<Hsla>,
|
||||||
|
#[serde(rename = "hint.border")]
|
||||||
|
hint_border: Option<Hsla>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StatusColors {
|
impl StatusColors {
|
||||||
|
|
@ -273,7 +283,12 @@ impl StatusColors {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn error_background(&self) -> Hsla {
|
pub fn error_background(&self) -> Hsla {
|
||||||
self.error_background.unwrap_or(self.error())
|
self.error_background.unwrap_or(crate::red_200())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn error_border(&self) -> Hsla {
|
||||||
|
self.error_border.unwrap_or(crate::red_500())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
@ -283,7 +298,12 @@ impl StatusColors {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn warning_background(&self) -> Hsla {
|
pub fn warning_background(&self) -> Hsla {
|
||||||
self.warning_background.unwrap_or(self.warning())
|
self.warning_background.unwrap_or(crate::yellow_200())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn warning_border(&self) -> Hsla {
|
||||||
|
self.warning_border.unwrap_or(crate::yellow_500())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
@ -293,7 +313,12 @@ impl StatusColors {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn info_background(&self) -> Hsla {
|
pub fn info_background(&self) -> Hsla {
|
||||||
self.info_background.unwrap_or(self.info())
|
self.info_background.unwrap_or(crate::blue_200())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn info_border(&self) -> Hsla {
|
||||||
|
self.info_border.unwrap_or(crate::blue_500())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
@ -303,17 +328,27 @@ impl StatusColors {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn success_background(&self) -> Hsla {
|
pub fn success_background(&self) -> Hsla {
|
||||||
self.success_background.unwrap_or(self.success())
|
self.success_background.unwrap_or(crate::green_200())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn success_border(&self) -> Hsla {
|
||||||
|
self.success_border.unwrap_or(crate::green_500())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn hint(&self) -> Hsla {
|
pub fn hint(&self) -> Hsla {
|
||||||
self.hint.unwrap_or(crate::cyan_500().opacity(0.5))
|
self.hint.unwrap_or(crate::gray_500())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn hint_background(&self) -> Hsla {
|
pub fn hint_background(&self) -> Hsla {
|
||||||
self.hint_background.unwrap_or(self.hint())
|
self.hint_background.unwrap_or(crate::gray_200())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn hint_border(&self) -> Hsla {
|
||||||
|
self.hint_border.unwrap_or(crate::gray_500())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,9 @@
|
||||||
"hidden": "#9E9E9E",
|
"hidden": "#9E9E9E",
|
||||||
"hidden.background": null,
|
"hidden.background": null,
|
||||||
"hidden.border": null,
|
"hidden.border": null,
|
||||||
"hint": null,
|
"hint": "#b283f8",
|
||||||
"hint.background": "#0C194D",
|
"hint.background": "#250c4b",
|
||||||
"hint.border": "#082190",
|
"hint.border": "#3f0891",
|
||||||
"ignored": null,
|
"ignored": null,
|
||||||
"ignored.background": null,
|
"ignored.background": null,
|
||||||
"ignored.border": null,
|
"ignored.border": null,
|
||||||
|
|
@ -162,4 +162,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -22,9 +22,9 @@
|
||||||
"hidden": "#6D6D6D",
|
"hidden": "#6D6D6D",
|
||||||
"hidden.background": null,
|
"hidden.background": null,
|
||||||
"hidden.border": null,
|
"hidden.border": null,
|
||||||
"hint": null,
|
"hint": "#9e5dff",
|
||||||
"hint.background": "#E5F2FF",
|
"hint.background": "#ece6ff",
|
||||||
"hint.border": "#99CCFF",
|
"hint.border": "#bc8fff",
|
||||||
"ignored": null,
|
"ignored": null,
|
||||||
"ignored.background": null,
|
"ignored.background": null,
|
||||||
"ignored.border": null,
|
"ignored.border": null,
|
||||||
|
|
@ -172,4 +172,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -407,7 +407,7 @@ impl TextElement {
|
||||||
|
|
||||||
let mut marker_styles = vec![];
|
let mut marker_styles = vec![];
|
||||||
for marker in markers.iter() {
|
for marker in markers.iter() {
|
||||||
if let Some(range) = marker.byte_range(&state) {
|
if let Some(range) = &marker.range {
|
||||||
if range.start < skipped_offset {
|
if range.start < skipped_offset {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
133
crates/ui/src/input/hover_popover.rs
Normal file
133
crates/ui/src/input/hover_popover.rs
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
canvas, deferred, div, px, App, AppContext as _, Bounds, Context, Empty, Entity,
|
||||||
|
InteractiveElement, IntoElement, ParentElement as _, Pixels, Point, Render, Styled, Window,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
highlighter::LanguageRegistry,
|
||||||
|
input::{InputState, Marker},
|
||||||
|
text::TextView,
|
||||||
|
ActiveTheme as _,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct DiagnosticPopover {
|
||||||
|
state: Entity<InputState>,
|
||||||
|
pub(super) marker: Rc<Marker>,
|
||||||
|
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());
|
||||||
|
|
||||||
|
cx.new(|_| Self {
|
||||||
|
marker,
|
||||||
|
state,
|
||||||
|
bounds: Bounds::default(),
|
||||||
|
open: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn origin(&self, cx: &App) -> Option<Point<Pixels>> {
|
||||||
|
let Some(range) = self.marker.range.as_ref() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let (_, _, start_pos) = self
|
||||||
|
.state
|
||||||
|
.read(cx)
|
||||||
|
.line_and_position_for_offset(range.start);
|
||||||
|
|
||||||
|
start_pos
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn show(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.open = true;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn hide(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.open = false;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn check_to_hide(&mut self, mouse_position: Point<Pixels>, cx: &mut Context<Self>) {
|
||||||
|
if !self.open {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let padding = px(5.);
|
||||||
|
let bounds = Bounds {
|
||||||
|
origin: self.bounds.origin.map(|v| v - padding),
|
||||||
|
size: self.bounds.size.map(|v| v + padding * 2.),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !bounds.contains(&mouse_position) {
|
||||||
|
self.hide(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for DiagnosticPopover {
|
||||||
|
fn render(&mut self, window: &mut Window, cx: &mut gpui::Context<Self>) -> impl IntoElement {
|
||||||
|
if !self.open {
|
||||||
|
return Empty.into_any_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
let view = cx.entity();
|
||||||
|
let theme = LanguageRegistry::global(cx).theme(cx.theme().is_dark());
|
||||||
|
|
||||||
|
let message = self.marker.message.clone();
|
||||||
|
let Some(pos) = self.origin(cx) else {
|
||||||
|
return Empty.into_any_element();
|
||||||
|
};
|
||||||
|
let (border, bg, fg) = (
|
||||||
|
self.marker.severity.border(theme),
|
||||||
|
self.marker.severity.bg(theme),
|
||||||
|
self.marker.severity.fg(theme),
|
||||||
|
);
|
||||||
|
|
||||||
|
let scroll_origin = self.state.read(cx).scroll_handle.offset();
|
||||||
|
|
||||||
|
let y = pos.y - self.bounds.size.height + scroll_origin.y;
|
||||||
|
let x = pos.x + scroll_origin.x;
|
||||||
|
let max_width = px(500.).min(window.bounds().size.width - x);
|
||||||
|
|
||||||
|
deferred(
|
||||||
|
div()
|
||||||
|
.id("code-editor-diagnostic-popover")
|
||||||
|
.absolute()
|
||||||
|
.left(x)
|
||||||
|
.top(y)
|
||||||
|
.px_1()
|
||||||
|
.py_0p5()
|
||||||
|
.text_xs()
|
||||||
|
.bg(bg)
|
||||||
|
.w(max_width)
|
||||||
|
.text_color(fg)
|
||||||
|
.border_1()
|
||||||
|
.border_color(border)
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.shadow_sm()
|
||||||
|
.child(TextView::markdown("message", message))
|
||||||
|
.child(
|
||||||
|
canvas(
|
||||||
|
move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds),
|
||||||
|
|_, _, _, _| {},
|
||||||
|
)
|
||||||
|
.top_0()
|
||||||
|
.left_0()
|
||||||
|
.absolute()
|
||||||
|
.size_full(),
|
||||||
|
)
|
||||||
|
.on_mouse_down_out(cx.listener(|this, _, _, cx| {
|
||||||
|
this.open = false;
|
||||||
|
cx.notify();
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{highlighter::HighlightTheme, input::InputState};
|
use crate::{highlighter::HighlightTheme, input::InputState};
|
||||||
use gpui::{px, HighlightStyle, SharedString, UnderlineStyle};
|
use gpui::{px, HighlightStyle, Hsla, SharedString, UnderlineStyle};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
|
|
@ -9,6 +9,7 @@ pub struct Marker {
|
||||||
pub severity: MarkerSeverity,
|
pub severity: MarkerSeverity,
|
||||||
pub start: LineColumn,
|
pub start: LineColumn,
|
||||||
pub end: LineColumn,
|
pub end: LineColumn,
|
||||||
|
pub(super) range: Option<Range<usize>>,
|
||||||
/// The message associated with the marker, typically a description of the issue.
|
/// The message associated with the marker, typically a description of the issue.
|
||||||
pub message: SharedString,
|
pub message: SharedString,
|
||||||
}
|
}
|
||||||
|
|
@ -26,22 +27,34 @@ impl Marker {
|
||||||
start: start.into(),
|
start: start.into(),
|
||||||
end: end.into(),
|
end: end.into(),
|
||||||
message: message.into(),
|
message: message.into(),
|
||||||
|
range: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the range (zero-based) of bytes in the source code that this marker covers.
|
/// Prepare the marker to convert line, column to byte offsets.
|
||||||
pub(super) fn byte_range(&self, state: &InputState) -> Option<Range<usize>> {
|
pub(super) fn prepare(&mut self, state: &InputState) {
|
||||||
let start_line = state
|
let Some(start_line) = state
|
||||||
.text_wrapper
|
.text_wrapper
|
||||||
.lines
|
.lines
|
||||||
.get(self.start.line.saturating_sub(1))?;
|
.get(self.start.line.saturating_sub(1))
|
||||||
let start_line_str = state.text.get(start_line.range.clone())?;
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let end_line = state
|
let Some(start_line_str) = state.text.get(start_line.range.clone()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(end_line) = state
|
||||||
.text_wrapper
|
.text_wrapper
|
||||||
.lines
|
.lines
|
||||||
.get(self.end.line.saturating_sub(1))?;
|
.get(self.end.line.saturating_sub(1))
|
||||||
let end_line_str = state.text.get(end_line.range.clone())?;
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(end_line_str) = state.text.get(end_line.range.clone()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let start_byte = start_line.range.start
|
let start_byte = start_line.range.start
|
||||||
+ start_line_str
|
+ start_line_str
|
||||||
|
|
@ -58,7 +71,7 @@ impl Marker {
|
||||||
.values()
|
.values()
|
||||||
.sum::<usize>();
|
.sum::<usize>();
|
||||||
|
|
||||||
Some(start_byte..end_byte)
|
self.range = Some(start_byte..end_byte);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,7 +117,33 @@ impl From<&str> for MarkerSeverity {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MarkerSeverity {
|
impl MarkerSeverity {
|
||||||
/// Returns the [`HighlightStyle`] for the marker severity with the given theme style.
|
pub(super) fn bg(&self, theme: &HighlightTheme) -> Hsla {
|
||||||
|
match self {
|
||||||
|
Self::Error => theme.style.status.error_background(),
|
||||||
|
Self::Warning => theme.style.status.warning_background(),
|
||||||
|
Self::Info => theme.style.status.info_background(),
|
||||||
|
Self::Hint => theme.style.status.hint_background(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn fg(&self, theme: &HighlightTheme) -> Hsla {
|
||||||
|
match self {
|
||||||
|
Self::Error => theme.style.status.error(),
|
||||||
|
Self::Warning => theme.style.status.warning(),
|
||||||
|
Self::Info => theme.style.status.info(),
|
||||||
|
Self::Hint => theme.style.status.hint(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn border(&self, theme: &HighlightTheme) -> Hsla {
|
||||||
|
match self {
|
||||||
|
Self::Error => theme.style.status.error_border(),
|
||||||
|
Self::Warning => theme.style.status.warning_border(),
|
||||||
|
Self::Info => theme.style.status.info_border(),
|
||||||
|
Self::Hint => theme.style.status.hint_border(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn highlight_style(&self, theme: &HighlightTheme) -> HighlightStyle {
|
pub(super) fn highlight_style(&self, theme: &HighlightTheme) -> HighlightStyle {
|
||||||
let color = match self {
|
let color = match self {
|
||||||
Self::Error => Some(theme.style.status.error()),
|
Self::Error => Some(theme.style.status.error()),
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ mod blink_cursor;
|
||||||
mod change;
|
mod change;
|
||||||
mod clear_button;
|
mod clear_button;
|
||||||
mod element;
|
mod element;
|
||||||
|
mod hover_popover;
|
||||||
mod marker;
|
mod marker;
|
||||||
mod mask_pattern;
|
mod mask_pattern;
|
||||||
mod mode;
|
mod mode;
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,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: Vec<Marker>,
|
markers: Rc<Vec<Marker>>,
|
||||||
},
|
},
|
||||||
AutoGrow {
|
AutoGrow {
|
||||||
rows: usize,
|
rows: usize,
|
||||||
|
|
@ -182,20 +182,42 @@ 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<&Vec<Marker>> {
|
pub(super) fn markers(&self) -> Option<&Rc<Vec<Marker>>> {
|
||||||
match &self {
|
match self {
|
||||||
InputMode::CodeEditor { markers, .. } => Some(markers),
|
InputMode::CodeEditor { markers, .. } => Some(markers),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn clear_markers(&mut self) {
|
pub(super) fn set_markers(&mut self, new_markers: Vec<Marker>) {
|
||||||
match self {
|
match self {
|
||||||
InputMode::CodeEditor { markers, .. } => markers.clear(),
|
InputMode::CodeEditor { markers, .. } => *markers = Rc::new(new_markers),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)]
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ use super::{
|
||||||
number_input,
|
number_input,
|
||||||
text_wrapper::TextWrapper,
|
text_wrapper::TextWrapper,
|
||||||
};
|
};
|
||||||
|
use crate::input::hover_popover::DiagnosticPopover;
|
||||||
use crate::input::marker::Marker;
|
use crate::input::marker::Marker;
|
||||||
use crate::{history::History, scroll::ScrollbarState, Root};
|
use crate::{history::History, scroll::ScrollbarState, Root};
|
||||||
|
|
||||||
|
|
@ -258,6 +259,9 @@ pub struct InputState {
|
||||||
pub(crate) mask_pattern: MaskPattern,
|
pub(crate) mask_pattern: MaskPattern,
|
||||||
pub(super) placeholder: SharedString,
|
pub(super) placeholder: SharedString,
|
||||||
|
|
||||||
|
/// Popover
|
||||||
|
diagnostic_popover: Option<Entity<DiagnosticPopover>>,
|
||||||
|
|
||||||
/// To remember the horizontal column (x-coordinate) of the cursor position.
|
/// To remember the horizontal column (x-coordinate) of the cursor position.
|
||||||
preferred_x_offset: Option<Pixels>,
|
preferred_x_offset: Option<Pixels>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
|
|
@ -329,6 +333,7 @@ impl InputState {
|
||||||
line_number_width: px(0.),
|
line_number_width: px(0.),
|
||||||
placeholder: SharedString::default(),
|
placeholder: SharedString::default(),
|
||||||
mask_pattern: MaskPattern::default(),
|
mask_pattern: MaskPattern::default(),
|
||||||
|
diagnostic_popover: None,
|
||||||
_subscriptions,
|
_subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -382,7 +387,7 @@ impl InputState {
|
||||||
highlighter: Rc::new(RefCell::new(None)),
|
highlighter: Rc::new(RefCell::new(None)),
|
||||||
line_number: true,
|
line_number: true,
|
||||||
height: Some(relative(1.)),
|
height: Some(relative(1.)),
|
||||||
markers: vec![],
|
markers: Rc::new(vec![]),
|
||||||
};
|
};
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -465,16 +470,12 @@ impl InputState {
|
||||||
/// Set markers, only for [`InputMode::CodeEditor`] mode.
|
/// Set markers, only for [`InputMode::CodeEditor`] mode.
|
||||||
///
|
///
|
||||||
/// For example to set the diagnostic markers in the code editor.
|
/// For example to set the diagnostic markers in the code editor.
|
||||||
pub fn set_markers(
|
pub fn set_markers(&mut self, markers: Vec<Marker>, _: &mut Window, _: &mut Context<Self>) {
|
||||||
&mut self,
|
let mut markers = markers;
|
||||||
new_markers: Vec<Marker>,
|
for marker in &mut markers {
|
||||||
_: &mut Window,
|
marker.prepare(self);
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if let InputMode::CodeEditor { markers, .. } = &mut self.mode {
|
|
||||||
*markers = new_markers;
|
|
||||||
cx.notify();
|
|
||||||
}
|
}
|
||||||
|
self.mode.set_markers(markers);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set placeholder
|
/// Set placeholder
|
||||||
|
|
@ -490,13 +491,12 @@ impl InputState {
|
||||||
|
|
||||||
/// Called after moving the cursor. Updates preferred_x_offset if we know where the cursor now is.
|
/// Called after moving the cursor. Updates preferred_x_offset if we know where the cursor now is.
|
||||||
fn update_preferred_x_offset(&mut self, _cx: &mut Context<Self>) {
|
fn update_preferred_x_offset(&mut self, _cx: &mut Context<Self>) {
|
||||||
if let (Some(lines), Some(bounds)) = (&self.last_layout, &self.last_bounds) {
|
if let (Some(_), Some(bounds)) = (&self.last_layout, &self.last_bounds) {
|
||||||
let offset = self.cursor_offset();
|
let offset = self.cursor_offset();
|
||||||
let line_height = self.last_line_height;
|
|
||||||
|
|
||||||
// Find which line and sub-line the cursor is on and its position
|
// Find which line and sub-line the cursor is on and its position
|
||||||
let (_line_index, _sub_line_index, cursor_pos) =
|
let (_line_index, _sub_line_index, cursor_pos) =
|
||||||
self.line_and_position_for_offset(offset, lines, line_height);
|
self.line_and_position_for_offset(offset);
|
||||||
|
|
||||||
if let Some(pos) = cursor_pos {
|
if let Some(pos) = cursor_pos {
|
||||||
// Adjust by scroll offset
|
// Adjust by scroll offset
|
||||||
|
|
@ -507,19 +507,29 @@ impl InputState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find which line and sub-line the given offset belongs to, along with the position within that sub-line.
|
/// Find which line and sub-line the given offset belongs to, along with the position within that sub-line.
|
||||||
fn line_and_position_for_offset(
|
///
|
||||||
|
/// Returns:
|
||||||
|
///
|
||||||
|
/// - The index of the line (zero-based) containing the offset.
|
||||||
|
/// - The index of the sub-line (zero-based) within the line containing the offset.
|
||||||
|
/// - The position of the offset.
|
||||||
|
pub(super) fn line_and_position_for_offset(
|
||||||
&self,
|
&self,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
lines: &[WrappedLine],
|
|
||||||
line_height: Pixels,
|
|
||||||
) -> (usize, usize, Option<Point<Pixels>>) {
|
) -> (usize, usize, Option<Point<Pixels>>) {
|
||||||
|
let Some(lines) = &self.last_layout else {
|
||||||
|
return (0, 0, None);
|
||||||
|
};
|
||||||
|
let line_height = self.last_line_height;
|
||||||
|
let line_number_width = self.line_number_width;
|
||||||
|
|
||||||
let mut prev_lines_offset = 0;
|
let mut prev_lines_offset = 0;
|
||||||
let mut y_offset = px(0.);
|
let mut y_offset = px(0.);
|
||||||
for (line_index, line) in lines.iter().enumerate() {
|
for (line_index, line) in lines.iter().enumerate() {
|
||||||
let local_offset = offset.saturating_sub(prev_lines_offset);
|
let local_offset = offset.saturating_sub(prev_lines_offset);
|
||||||
if let Some(pos) = line.position_for_index(local_offset, line_height) {
|
if let Some(pos) = line.position_for_index(local_offset, line_height) {
|
||||||
let sub_line_index = (pos.y.0 / line_height.0) as usize;
|
let sub_line_index = (pos.y.0 / line_height.0) as usize;
|
||||||
let adjusted_pos = point(pos.x, pos.y + y_offset);
|
let adjusted_pos = point(pos.x + line_number_width, pos.y + y_offset);
|
||||||
return (line_index, sub_line_index, Some(adjusted_pos));
|
return (line_index, sub_line_index, Some(adjusted_pos));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -543,7 +553,7 @@ impl InputState {
|
||||||
let offset = self.cursor_offset();
|
let offset = self.cursor_offset();
|
||||||
let line_height = self.last_line_height;
|
let line_height = self.last_line_height;
|
||||||
let (current_line_index, current_sub_line, current_pos) =
|
let (current_line_index, current_sub_line, current_pos) =
|
||||||
self.line_and_position_for_offset(offset, lines, line_height);
|
self.line_and_position_for_offset(offset);
|
||||||
|
|
||||||
let Some(current_pos) = current_pos else {
|
let Some(current_pos) = current_pos else {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1419,6 +1429,35 @@ impl InputState {
|
||||||
self.selected_word_range = None;
|
self.selected_word_range = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn on_mouse_move(
|
||||||
|
&mut self,
|
||||||
|
event: &MouseMoveEvent,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
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_popover) = self.diagnostic_popover.as_ref() {
|
||||||
|
if diagnostic_popover.read(cx).marker.range == marker.range {
|
||||||
|
diagnostic_popover.update(cx, |this, cx| {
|
||||||
|
this.show(cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.diagnostic_popover = Some(DiagnosticPopover::new(marker, cx.entity(), cx));
|
||||||
|
cx.notify();
|
||||||
|
} else {
|
||||||
|
if let Some(diagnostic_popover) = self.diagnostic_popover.as_mut() {
|
||||||
|
diagnostic_popover.update(cx, |this, cx| {
|
||||||
|
this.check_to_hide(event.position, cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn on_scroll_wheel(
|
pub(super) fn on_scroll_wheel(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: &ScrollWheelEvent,
|
event: &ScrollWheelEvent,
|
||||||
|
|
@ -1427,6 +1466,7 @@ impl InputState {
|
||||||
) {
|
) {
|
||||||
let delta = event.delta.pixel_delta(self.last_line_height);
|
let delta = event.delta.pixel_delta(self.last_line_height);
|
||||||
self.update_scroll_offset(Some(self.scroll_handle.offset() + delta), cx);
|
self.update_scroll_offset(Some(self.scroll_handle.offset() + delta), cx);
|
||||||
|
self.diagnostic_popover = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_scroll_offset(&mut self, offset: Option<Point<Pixels>>, cx: &mut Context<Self>) {
|
fn update_scroll_offset(&mut self, offset: Option<Point<Pixels>>, cx: &mut Context<Self>) {
|
||||||
|
|
@ -1751,7 +1791,7 @@ impl InputState {
|
||||||
cx.notify()
|
cx.notify()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn offset_from_utf16(&self, offset: usize) -> usize {
|
pub(super) fn offset_from_utf16(&self, offset: usize) -> usize {
|
||||||
let mut utf8_offset = 0;
|
let mut utf8_offset = 0;
|
||||||
let mut utf16_count = 0;
|
let mut utf16_count = 0;
|
||||||
|
|
||||||
|
|
@ -1766,7 +1806,7 @@ impl InputState {
|
||||||
utf8_offset
|
utf8_offset
|
||||||
}
|
}
|
||||||
|
|
||||||
fn offset_to_utf16(&self, offset: usize) -> usize {
|
pub(super) fn offset_to_utf16(&self, offset: usize) -> usize {
|
||||||
let mut utf16_offset = 0;
|
let mut utf16_offset = 0;
|
||||||
let mut utf8_count = 0;
|
let mut utf8_count = 0;
|
||||||
|
|
||||||
|
|
@ -1781,11 +1821,11 @@ impl InputState {
|
||||||
utf16_offset
|
utf16_offset
|
||||||
}
|
}
|
||||||
|
|
||||||
fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
|
pub(super) fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
|
||||||
self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
|
self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
|
pub(super) fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
|
||||||
self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
|
self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2164,5 +2204,6 @@ impl Render for InputState {
|
||||||
.flex_grow()
|
.flex_grow()
|
||||||
.overflow_x_hidden()
|
.overflow_x_hidden()
|
||||||
.child(TextElement::new(cx.entity().clone()).placeholder(self.placeholder.clone()))
|
.child(TextElement::new(cx.entity().clone()).placeholder(self.placeholder.clone()))
|
||||||
|
.children(self.diagnostic_popover.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,7 @@ impl RenderOnce for TextInput {
|
||||||
MouseButton::Left,
|
MouseButton::Left,
|
||||||
window.listener_for(&self.state, InputState::on_mouse_up),
|
window.listener_for(&self.state, InputState::on_mouse_up),
|
||||||
)
|
)
|
||||||
|
.on_mouse_move(window.listener_for(&self.state, InputState::on_mouse_move))
|
||||||
.on_scroll_wheel(window.listener_for(&self.state, InputState::on_scroll_wheel))
|
.on_scroll_wheel(window.listener_for(&self.state, InputState::on_scroll_wheel))
|
||||||
.size_full()
|
.size_full()
|
||||||
.line_height(LINE_HEIGHT)
|
.line_height(LINE_HEIGHT)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue