feat: add selection, active line, and incremental highlighting to code
view
This commit is contained in:
+452
-35
@@ -4,15 +4,47 @@
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Context, ElementId, HighlightStyle, Hsla, InteractiveElement as _, IntoElement,
|
App, Bounds, ClipboardItem, Context, ElementId, HighlightStyle, Hsla, InteractiveElement as _,
|
||||||
ParentElement as _, Pixels, Render, ScrollStrategy, SharedString,
|
IntoElement, KeyBinding, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
||||||
StatefulInteractiveElement as _, Styled as _, TextStyle, UniformListScrollHandle, Window, div,
|
ParentElement as _, Pixels, Point, Render, ScrollStrategy, SharedString,
|
||||||
uniform_list,
|
StatefulInteractiveElement as _, Styled as _, TextStyle, UniformListScrollHandle, Window,
|
||||||
|
actions, canvas, div, uniform_list,
|
||||||
};
|
};
|
||||||
use gpui_native_core::scrollbar::{Scrollbar, ScrollbarVisibility};
|
use gpui_native_core::scrollbar::{Scrollbar, ScrollbarHandle, ScrollbarVisibility};
|
||||||
use gpui_native_theme::{ActiveTheme as _, ColorTokens, Ramps};
|
use gpui_native_theme::{ActiveTheme as _, ColorTokens, Ramps};
|
||||||
|
|
||||||
use crate::highlight::{HighlightedCode, Language, Role, highlight};
|
use crate::highlight::{HighlightedCode, Language, Role, highlight_with_tree};
|
||||||
|
|
||||||
|
actions!(code_view, [Copy]);
|
||||||
|
|
||||||
|
/// The key context the code view declares while it holds focus.
|
||||||
|
pub const CODE_VIEW_CONTEXT: &str = "CodeView";
|
||||||
|
|
||||||
|
/// Binds the code view's copy chord: cmd-c on macOS, ctrl-c elsewhere.
|
||||||
|
///
|
||||||
|
/// Call once at startup. Without it, selection copy has no key.
|
||||||
|
pub fn bind_code_view_keys(cx: &mut App) {
|
||||||
|
let context = Some(CODE_VIEW_CONTEXT);
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let copy = "cmd-c";
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
let copy = "ctrl-c";
|
||||||
|
cx.bind_keys([KeyBinding::new(copy, Copy, context)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A point in the source, in lines and characters (not bytes).
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub struct SelectionPoint {
|
||||||
|
pub row: usize,
|
||||||
|
pub col: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SelectionPoint {
|
||||||
|
/// The earlier of two points, in document order.
|
||||||
|
fn earlier_than(&self, other: &Self) -> bool {
|
||||||
|
(self.row, self.col) <= (other.row, other.col)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Retained state for a code view.
|
/// Retained state for a code view.
|
||||||
pub struct CodeViewState {
|
pub struct CodeViewState {
|
||||||
@@ -20,26 +52,40 @@ pub struct CodeViewState {
|
|||||||
source: SharedString,
|
source: SharedString,
|
||||||
language: Language,
|
language: Language,
|
||||||
code: HighlightedCode,
|
code: HighlightedCode,
|
||||||
|
/// The parse tree of `source`, so updates reparse incrementally instead of from scratch.
|
||||||
|
tree: Option<tree_sitter::Tree>,
|
||||||
handle: UniformListScrollHandle,
|
handle: UniformListScrollHandle,
|
||||||
|
/// The hovered line, driving the active-line wash and number emphasis.
|
||||||
|
active_line: Option<usize>,
|
||||||
|
/// `(anchor, head)` in the order they were set.
|
||||||
|
selection: Option<(SelectionPoint, SelectionPoint)>,
|
||||||
|
/// Whether a drag that started on a row is still extending the selection.
|
||||||
|
selecting: bool,
|
||||||
|
focus_handle: gpui::FocusHandle,
|
||||||
a11y_label: Option<SharedString>,
|
a11y_label: Option<SharedString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CodeViewState {
|
impl CodeViewState {
|
||||||
/// Builds the state, highlighting `source` with `language`.
|
/// Builds the state, highlighting `source` with `language`.
|
||||||
pub fn new<I, S>(id: I, source: S, language: Language) -> Self
|
pub fn new<I, S>(id: I, source: S, language: Language, cx: &mut App) -> Self
|
||||||
where
|
where
|
||||||
I: Into<ElementId>,
|
I: Into<ElementId>,
|
||||||
S: Into<SharedString>,
|
S: Into<SharedString>,
|
||||||
{
|
{
|
||||||
let source = source.into();
|
let source = source.into();
|
||||||
let code = highlight(&source, language);
|
let (code, tree) = highlight_with_tree(&source, language, None);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
source,
|
source,
|
||||||
language,
|
language,
|
||||||
code,
|
code,
|
||||||
|
tree,
|
||||||
handle: UniformListScrollHandle::new(),
|
handle: UniformListScrollHandle::new(),
|
||||||
|
active_line: None,
|
||||||
|
selection: None,
|
||||||
|
selecting: false,
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
a11y_label: None,
|
a11y_label: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,10 +95,48 @@ impl CodeViewState {
|
|||||||
where
|
where
|
||||||
S: Into<SharedString>,
|
S: Into<SharedString>,
|
||||||
{
|
{
|
||||||
self.source = source.into();
|
let source = source.into();
|
||||||
|
|
||||||
|
// The old tree is only reusable when the grammar stays the same.
|
||||||
|
let old_tree = if language == self.language {
|
||||||
|
self.tree.as_ref()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let (code, tree) = highlight_with_tree(&source, language, old_tree);
|
||||||
|
self.source = source;
|
||||||
self.language = language;
|
self.language = language;
|
||||||
self.code = highlight(&self.source, self.language);
|
self.code = code;
|
||||||
|
self.tree = tree;
|
||||||
|
self.selection = None;
|
||||||
self.handle.scroll_to_item(0, ScrollStrategy::Top);
|
self.handle.scroll_to_item(0, ScrollStrategy::Top);
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaces one line's text and re-highlights the file.
|
||||||
|
pub fn update_line<T>(&mut self, line_ix: usize, text: T, cx: &mut Context<Self>)
|
||||||
|
where
|
||||||
|
T: AsRef<str>,
|
||||||
|
{
|
||||||
|
let trailing_newline = self.source.ends_with('\n');
|
||||||
|
let mut lines: Vec<String> = self.source.lines().map(str::to_string).collect();
|
||||||
|
let line_ix = line_ix.min(lines.len().saturating_sub(1));
|
||||||
|
|
||||||
|
let replacement: Vec<String> = if text.as_ref().is_empty() {
|
||||||
|
vec![String::new()]
|
||||||
|
} else {
|
||||||
|
text.as_ref().lines().map(str::to_string).collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
lines.splice(line_ix..line_ix + 1, replacement);
|
||||||
|
self.source = (lines.join("\n") + if trailing_newline { "\n" } else { "" }).into();
|
||||||
|
|
||||||
|
let (code, tree) = highlight_with_tree(&self.source, self.language, self.tree.as_ref());
|
||||||
|
self.code = code;
|
||||||
|
self.tree = tree;
|
||||||
|
self.selection = None;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +163,71 @@ impl CodeViewState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The line the pointer last hovered, if any.
|
||||||
|
pub fn active_line(&self) -> Option<usize> {
|
||||||
|
self.active_line
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selects a range, endpoints in lines and characters.
|
||||||
|
pub fn select_range(
|
||||||
|
&mut self,
|
||||||
|
start: SelectionPoint,
|
||||||
|
end: SelectionPoint,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.selection = if start.earlier_than(&end) {
|
||||||
|
Some((start, end))
|
||||||
|
} else {
|
||||||
|
Some((end, start))
|
||||||
|
};
|
||||||
|
self.selecting = false;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The selected range, endpoints in document order, when there is one.
|
||||||
|
pub fn selection(&self) -> Option<(SelectionPoint, SelectionPoint)> {
|
||||||
|
self.selection.map(|(anchor, head)| {
|
||||||
|
if anchor.earlier_than(&head) {
|
||||||
|
(anchor, head)
|
||||||
|
} else {
|
||||||
|
(head, anchor)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The selected text, or `None` when nothing is selected.
|
||||||
|
pub fn selected_text(&self) -> Option<String> {
|
||||||
|
let (start, end) = self.selection()?;
|
||||||
|
|
||||||
|
let lines: Vec<&str> = self.source.split('\n').collect();
|
||||||
|
let mut selected = Vec::new();
|
||||||
|
|
||||||
|
for row in start.row..=end.row.min(lines.len().saturating_sub(1)) {
|
||||||
|
let line_chars: Vec<char> = lines.get(row)?.chars().collect();
|
||||||
|
let start_col = if row == start.row {
|
||||||
|
start.col.min(line_chars.len())
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
let end_col = if row == end.row {
|
||||||
|
end.col.min(line_chars.len())
|
||||||
|
} else {
|
||||||
|
line_chars.len()
|
||||||
|
};
|
||||||
|
|
||||||
|
selected.push(line_chars[start_col..end_col].iter().collect::<String>());
|
||||||
|
}
|
||||||
|
|
||||||
|
(!selected.is_empty()).then(|| selected.join("\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drops the selection.
|
||||||
|
pub fn clear_selection(&mut self) {
|
||||||
|
self.selection = None;
|
||||||
|
self.selecting = false;
|
||||||
|
}
|
||||||
|
|
||||||
/// Overrides the accessibility label of the view.
|
/// Overrides the accessibility label of the view.
|
||||||
pub fn accessibility_label<L>(mut self, label: L) -> Self
|
pub fn accessibility_label<L>(mut self, label: L) -> Self
|
||||||
where
|
where
|
||||||
@@ -87,6 +236,39 @@ impl CodeViewState {
|
|||||||
self.a11y_label = Some(label.into());
|
self.a11y_label = Some(label.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Maps a window position to a point in the source.
|
||||||
|
fn position_at(
|
||||||
|
&self,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
position: Point<Pixels>,
|
||||||
|
scroll_y: Pixels,
|
||||||
|
line_height: Pixels,
|
||||||
|
char_width: Pixels,
|
||||||
|
text_x: Pixels,
|
||||||
|
) -> SelectionPoint {
|
||||||
|
let row = (((position.y - bounds.origin.y - scroll_y) / line_height).floor() as isize)
|
||||||
|
.clamp(0, self.code.lines.len() as isize - 1)
|
||||||
|
.max(0) as usize;
|
||||||
|
|
||||||
|
let chars = self.code.lines[row]
|
||||||
|
.spans
|
||||||
|
.iter()
|
||||||
|
.map(|span| span.text.chars().count())
|
||||||
|
.sum::<usize>();
|
||||||
|
|
||||||
|
let col = (((position.x - text_x) / char_width).round() as isize)
|
||||||
|
.clamp(0, chars as isize)
|
||||||
|
.max(0) as usize;
|
||||||
|
|
||||||
|
SelectionPoint { row, col }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl gpui::Focusable for CodeViewState {
|
||||||
|
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for CodeViewState {
|
impl Render for CodeViewState {
|
||||||
@@ -96,24 +278,36 @@ impl Render for CodeViewState {
|
|||||||
let mono_font = theme.mono_font;
|
let mono_font = theme.mono_font;
|
||||||
let text_size = theme.sizes.small.text_size;
|
let text_size = theme.sizes.small.text_size;
|
||||||
let line_height = text_size * 1.5;
|
let line_height = text_size * 1.5;
|
||||||
|
let char_width = text_size * 0.62;
|
||||||
|
let root_font_size = theme.root_font_size;
|
||||||
|
|
||||||
let id = self.id.clone();
|
let id = self.id.clone();
|
||||||
let state = cx.entity();
|
let state = cx.entity();
|
||||||
let line_count = self.code.lines.len();
|
let line_count = self.code.lines.len();
|
||||||
|
let selection = self.selection();
|
||||||
|
|
||||||
let gutter_chars = line_count.max(1).to_string().len().max(2);
|
let gutter_chars = line_count.max(1).to_string().len().max(2);
|
||||||
let gutter_width = text_size * 0.62 * gutter_chars as f32 + text_size;
|
let gutter_width = text_size * 0.62 * gutter_chars as f32 + text_size;
|
||||||
|
|
||||||
|
let bounds_cell: std::rc::Rc<std::cell::RefCell<Option<Bounds<Pixels>>>> =
|
||||||
|
std::rc::Rc::new(std::cell::RefCell::new(None));
|
||||||
|
|
||||||
let rows = uniform_list((id.clone(), "rows"), line_count, {
|
let rows = uniform_list((id.clone(), "rows"), line_count, {
|
||||||
let id = id.clone();
|
let id = id.clone();
|
||||||
|
let state = state.clone();
|
||||||
move |range, _window, cx| {
|
move |range, _window, cx| {
|
||||||
let code = state.read(cx);
|
let code = state.read(cx);
|
||||||
|
let active_line = code.active_line;
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
|
|
||||||
range
|
range
|
||||||
.map(|line_ix| {
|
.map(|line_ix| {
|
||||||
render_row(
|
render_row(
|
||||||
code,
|
code,
|
||||||
&id,
|
&id,
|
||||||
line_ix,
|
line_ix,
|
||||||
|
active_line == Some(line_ix),
|
||||||
|
selection,
|
||||||
theme.mono_font,
|
theme.mono_font,
|
||||||
theme.colors,
|
theme.colors,
|
||||||
theme.ramps,
|
theme.ramps,
|
||||||
@@ -136,16 +330,135 @@ impl Render for CodeViewState {
|
|||||||
.text_size(text_size)
|
.text_size(text_size)
|
||||||
.font_family(mono_font)
|
.font_family(mono_font)
|
||||||
.text_color(colors.foreground)
|
.text_color(colors.foreground)
|
||||||
.when_some(self.a11y_label.clone(), |view, label| {
|
.key_context(CODE_VIEW_CONTEXT)
|
||||||
view.aria_label(label)
|
.track_focus(&self.focus_handle)
|
||||||
|
.on_mouse_down(MouseButton::Left, {
|
||||||
|
let state = state.clone();
|
||||||
|
let bounds_cell = bounds_cell.clone();
|
||||||
|
move |event: &MouseDownEvent, window: &mut Window, cx: &mut App| {
|
||||||
|
window.focus(&state.read(cx).focus_handle.clone(), cx);
|
||||||
|
|
||||||
|
let bounds = bounds_cell.borrow();
|
||||||
|
let Some(bounds) = bounds.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let text_x = bounds.origin.x + gutter_width + root_font_size;
|
||||||
|
let point = {
|
||||||
|
let code = state.read(cx);
|
||||||
|
code.position_at(
|
||||||
|
*bounds,
|
||||||
|
event.position,
|
||||||
|
Pixels::ZERO,
|
||||||
|
line_height,
|
||||||
|
char_width,
|
||||||
|
text_x,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
state.update(cx, |state, cx| {
|
||||||
|
if event.modifiers.shift {
|
||||||
|
if let Some((anchor, _)) = state.selection {
|
||||||
|
state.selection = Some((anchor, point));
|
||||||
|
} else {
|
||||||
|
state.selection = Some((point, point));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.selection = Some((point, point));
|
||||||
|
}
|
||||||
|
state.selecting = true;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.when_some(self.a11y_label.clone(), |view, label| {
|
.on_mouse_move({
|
||||||
view.aria_label(label)
|
let state = state.clone();
|
||||||
|
let bounds_cell = bounds_cell.clone();
|
||||||
|
move |event: &MouseMoveEvent, _window: &mut Window, cx: &mut App| {
|
||||||
|
let bounds = bounds_cell.borrow();
|
||||||
|
let Some(bounds) = bounds.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let text_x = bounds.origin.x + gutter_width + root_font_size;
|
||||||
|
let point = {
|
||||||
|
let code = state.read(cx);
|
||||||
|
code.position_at(
|
||||||
|
*bounds,
|
||||||
|
event.position,
|
||||||
|
ScrollbarHandle::offset(&code.handle).y,
|
||||||
|
line_height,
|
||||||
|
char_width,
|
||||||
|
text_x,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let selecting = state.read(cx).selecting;
|
||||||
|
if selecting {
|
||||||
|
let changed = state
|
||||||
|
.read(cx)
|
||||||
|
.selection
|
||||||
|
.map(|(_, head)| head != point)
|
||||||
|
.unwrap_or(true);
|
||||||
|
if changed {
|
||||||
|
state.update(cx, |state, cx| {
|
||||||
|
if let Some((anchor, _)) = state.selection {
|
||||||
|
state.selection = Some((anchor, point));
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let changed = state.read(cx).active_line != Some(point.row);
|
||||||
|
if changed {
|
||||||
|
state.update(cx, |state, cx| {
|
||||||
|
state.active_line = Some(point.row);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
.on_mouse_up(MouseButton::Left, {
|
||||||
|
let state = state.clone();
|
||||||
|
move |_: &MouseUpEvent, _window: &mut Window, cx: &mut App| {
|
||||||
|
if state.read(cx).selecting {
|
||||||
|
state.update(cx, |state, cx| {
|
||||||
|
state.selecting = false;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_action({
|
||||||
|
let state = state.clone();
|
||||||
|
move |_: &Copy, _window, cx| {
|
||||||
|
if let Some(text) = state.read(cx).selected_text() {
|
||||||
|
cx.write_to_clipboard(ClipboardItem::new_string(text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
canvas(
|
||||||
|
{
|
||||||
|
let bounds_cell = bounds_cell.clone();
|
||||||
|
move |bounds, _window, _cx| {
|
||||||
|
*bounds_cell.borrow_mut() = Some(bounds);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|_, _, _, _| {},
|
||||||
|
)
|
||||||
|
.absolute()
|
||||||
|
.inset_0(),
|
||||||
|
)
|
||||||
.child(rows)
|
.child(rows)
|
||||||
.child(
|
.child(
|
||||||
Scrollbar::vertical(self.handle.clone()).visibility(ScrollbarVisibility::Platform),
|
Scrollbar::vertical(self.handle.clone()).visibility(ScrollbarVisibility::Platform),
|
||||||
)
|
)
|
||||||
|
.when_some(self.a11y_label.clone(), |view, label| {
|
||||||
|
view.aria_label(label)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,6 +467,8 @@ fn render_row(
|
|||||||
code: &CodeViewState,
|
code: &CodeViewState,
|
||||||
id: &ElementId,
|
id: &ElementId,
|
||||||
line_ix: usize,
|
line_ix: usize,
|
||||||
|
active: bool,
|
||||||
|
selection: Option<(SelectionPoint, SelectionPoint)>,
|
||||||
mono_font: &'static str,
|
mono_font: &'static str,
|
||||||
colors: ColorTokens,
|
colors: ColorTokens,
|
||||||
ramps: Ramps,
|
ramps: Ramps,
|
||||||
@@ -162,22 +477,74 @@ fn render_row(
|
|||||||
gutter_width: Pixels,
|
gutter_width: Pixels,
|
||||||
) -> gpui::AnyElement {
|
) -> gpui::AnyElement {
|
||||||
let line = code.code().lines.get(line_ix).cloned().unwrap_or_default();
|
let line = code.code().lines.get(line_ix).cloned().unwrap_or_default();
|
||||||
// One text element per line, colored by byte-range highlights
|
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
let mut highlights: Vec<(std::ops::Range<usize>, HighlightStyle)> = Vec::new();
|
for span in &line.spans {
|
||||||
|
text.push_str(&span.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
let char_count = text.chars().count();
|
||||||
|
let mut roles = vec![Role::Text; char_count];
|
||||||
|
let mut offset = 0usize;
|
||||||
|
|
||||||
for span in &line.spans {
|
for span in &line.spans {
|
||||||
let start = text.len();
|
let chars = span.text.chars().count();
|
||||||
text.push_str(&span.text);
|
for slot in &mut roles[offset..offset + chars] {
|
||||||
if span.role != Role::Text {
|
*slot = span.role;
|
||||||
highlights.push((
|
|
||||||
start..text.len(),
|
|
||||||
HighlightStyle {
|
|
||||||
color: Some(role_color(span.role, colors, ramps)),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
offset += chars;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut selected = vec![false; char_count];
|
||||||
|
|
||||||
|
if let Some((start, end)) = selection
|
||||||
|
&& line_ix >= start.row
|
||||||
|
&& line_ix <= end.row
|
||||||
|
{
|
||||||
|
let start_col = if line_ix == start.row {
|
||||||
|
start.col.min(char_count)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
let end_col = if line_ix == end.row {
|
||||||
|
end.col.min(char_count)
|
||||||
|
} else {
|
||||||
|
char_count
|
||||||
|
};
|
||||||
|
|
||||||
|
for slot in &mut selected[start_col..end_col] {
|
||||||
|
*slot = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut highlights: Vec<(std::ops::Range<usize>, HighlightStyle)> = Vec::new();
|
||||||
|
let mut run_start = 0usize;
|
||||||
|
let mut run_end = 0usize;
|
||||||
|
let mut run_role = Role::Text;
|
||||||
|
let mut run_selected = false;
|
||||||
|
|
||||||
|
for (char_ix, (byte_ix, character)) in text.char_indices().enumerate() {
|
||||||
|
let fresh = char_ix == 0 || roles[char_ix] != run_role || selected[char_ix] != run_selected;
|
||||||
|
if fresh {
|
||||||
|
if char_ix > 0 {
|
||||||
|
highlights.push((
|
||||||
|
run_start..run_end,
|
||||||
|
run_style(run_role, run_selected, colors, ramps),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
run_start = byte_ix;
|
||||||
|
run_role = roles[char_ix];
|
||||||
|
run_selected = selected[char_ix];
|
||||||
|
}
|
||||||
|
run_end = byte_ix + character.len_utf8();
|
||||||
|
}
|
||||||
|
|
||||||
|
if run_end > run_start || !text.is_empty() {
|
||||||
|
highlights.push((
|
||||||
|
run_start..run_end,
|
||||||
|
run_style(run_role, run_selected, colors, ramps),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let style = TextStyle {
|
let style = TextStyle {
|
||||||
@@ -185,6 +552,7 @@ fn render_row(
|
|||||||
font_family: SharedString::from(mono_font),
|
font_family: SharedString::from(mono_font),
|
||||||
font_size: text_size.into(),
|
font_size: text_size.into(),
|
||||||
line_height: line_height.into(),
|
line_height: line_height.into(),
|
||||||
|
white_space: gpui::WhiteSpace::Nowrap,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -194,31 +562,80 @@ fn render_row(
|
|||||||
move || name
|
move || name
|
||||||
})
|
})
|
||||||
.id(("code-row", line_ix))
|
.id(("code-row", line_ix))
|
||||||
|
.w_full()
|
||||||
.h(line_height)
|
.h(line_height)
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
|
.when(active, |row| {
|
||||||
|
row.bg(Hsla {
|
||||||
|
a: colors.muted.a * 0.45,
|
||||||
|
..colors.muted
|
||||||
|
})
|
||||||
|
})
|
||||||
.child(
|
.child(
|
||||||
div()
|
gutter_cell(
|
||||||
.w(gutter_width)
|
gutter_width,
|
||||||
.flex_none()
|
line_height,
|
||||||
.pr_2()
|
mono_font,
|
||||||
.text_right()
|
text_size,
|
||||||
.font_family(mono_font)
|
if active {
|
||||||
.text_size(text_size)
|
colors.foreground
|
||||||
.text_color(colors.muted_foreground)
|
} else {
|
||||||
.child(format!("{}", line_ix + 1)),
|
colors.muted_foreground
|
||||||
|
},
|
||||||
|
format!("{}", line_ix + 1),
|
||||||
|
)
|
||||||
|
.justify_end()
|
||||||
|
.pr_2()
|
||||||
|
.whitespace_nowrap(),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.min_w_0()
|
.min_w_0()
|
||||||
|
.ml_4()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
|
.whitespace_nowrap()
|
||||||
.child(gpui::StyledText::new(text).with_default_highlights(&style, highlights)),
|
.child(gpui::StyledText::new(text).with_default_highlights(&style, highlights)),
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_style(role: Role, selected: bool, colors: ColorTokens, ramps: Ramps) -> HighlightStyle {
|
||||||
|
HighlightStyle {
|
||||||
|
color: Some(role_color(role, colors, ramps)),
|
||||||
|
background_color: selected.then_some(colors.selection),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A gutter cell sized to the whole row.
|
||||||
|
fn gutter_cell<E>(
|
||||||
|
width: Pixels,
|
||||||
|
height: Pixels,
|
||||||
|
mono_font: &'static str,
|
||||||
|
text_size: Pixels,
|
||||||
|
color: Hsla,
|
||||||
|
content: E,
|
||||||
|
) -> gpui::Div
|
||||||
|
where
|
||||||
|
E: IntoElement,
|
||||||
|
{
|
||||||
|
div()
|
||||||
|
.w(width)
|
||||||
|
.h(height)
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.flex_none()
|
||||||
|
.font_family(mono_font)
|
||||||
|
.text_size(text_size)
|
||||||
|
.line_height(gpui::relative(1.0))
|
||||||
|
.text_color(color)
|
||||||
|
.child(content)
|
||||||
|
}
|
||||||
|
|
||||||
/// Maps a highlight role to the theme's semantic palette.
|
/// Maps a highlight role to the theme's semantic palette.
|
||||||
fn role_color(role: Role, colors: ColorTokens, ramps: Ramps) -> Hsla {
|
fn role_color(role: Role, colors: ColorTokens, ramps: Ramps) -> Hsla {
|
||||||
match role {
|
match role {
|
||||||
|
|||||||
@@ -54,10 +54,62 @@ use tree_sitter::StreamingIterator as _;
|
|||||||
|
|
||||||
/// Highlights `source` with `language`'s tree-sitter grammar.
|
/// Highlights `source` with `language`'s tree-sitter grammar.
|
||||||
pub fn highlight(source: &str, language: Language) -> HighlightedCode {
|
pub fn highlight(source: &str, language: Language) -> HighlightedCode {
|
||||||
|
highlight_with_tree(source, language, None).0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Highlights `source`, reusing `old_tree` so only the changed region is re-lexed.
|
||||||
|
pub fn highlight_with_tree(
|
||||||
|
source: &str,
|
||||||
|
language: Language,
|
||||||
|
old_tree: Option<&tree_sitter::Tree>,
|
||||||
|
) -> (HighlightedCode, Option<tree_sitter::Tree>) {
|
||||||
let Some((grammar, query_source)) = grammar_for(language) else {
|
let Some((grammar, query_source)) = grammar_for(language) else {
|
||||||
return build_lines(source, &[]);
|
return (plain(source), None);
|
||||||
};
|
};
|
||||||
highlight_with(grammar, query_source, source).unwrap_or_else(|| build_lines(source, &[]))
|
|
||||||
|
let mut parser = tree_sitter::Parser::new();
|
||||||
|
if parser.set_language(&grammar).is_err() {
|
||||||
|
return (plain(source), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(tree) = parser.parse(source, old_tree) else {
|
||||||
|
return (plain(source), None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(query) = tree_sitter::Query::new(&grammar, query_source) else {
|
||||||
|
return (plain(source), None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let roles: Vec<Option<Role>> = query
|
||||||
|
.capture_names()
|
||||||
|
.iter()
|
||||||
|
.map(|name| role_for_capture(name))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// One role per byte; matches arrive in query order
|
||||||
|
let mut role_at = vec![None; source.len()];
|
||||||
|
let mut cursor = tree_sitter::QueryCursor::new();
|
||||||
|
let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
|
||||||
|
|
||||||
|
while let Some(matched) = matches.next() {
|
||||||
|
for capture in matched.captures {
|
||||||
|
let Some(role) = roles.get(capture.index as usize).copied().flatten() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for byte in capture.node.byte_range() {
|
||||||
|
if let Some(slot) = role_at.get_mut(byte) {
|
||||||
|
*slot = Some(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(build_lines(source, &role_at), Some(tree))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Source rendered without any grammar: plain text roles.
|
||||||
|
pub(crate) fn plain(source: &str) -> HighlightedCode {
|
||||||
|
build_lines(source, &[])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The grammar and its highlight query for a language, when compiled in.
|
/// The grammar and its highlight query for a language, when compiled in.
|
||||||
@@ -90,43 +142,6 @@ fn grammar_for(language: Language) -> Option<(tree_sitter::Language, &'static st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn highlight_with(
|
|
||||||
grammar: tree_sitter::Language,
|
|
||||||
query_source: &str,
|
|
||||||
source: &str,
|
|
||||||
) -> Option<HighlightedCode> {
|
|
||||||
let mut parser = tree_sitter::Parser::new();
|
|
||||||
parser.set_language(&grammar).ok()?;
|
|
||||||
|
|
||||||
let tree = parser.parse(source, None)?;
|
|
||||||
let query = tree_sitter::Query::new(&grammar, query_source).ok()?;
|
|
||||||
|
|
||||||
let roles: Vec<Option<Role>> = query
|
|
||||||
.capture_names()
|
|
||||||
.iter()
|
|
||||||
.map(|name| role_for_capture(name))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut role_at = vec![None; source.len()];
|
|
||||||
let mut cursor = tree_sitter::QueryCursor::new();
|
|
||||||
let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
|
|
||||||
|
|
||||||
while let Some(matched) = matches.next() {
|
|
||||||
for capture in matched.captures {
|
|
||||||
let Some(role) = roles.get(capture.index as usize).copied().flatten() else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
for byte in capture.node.byte_range() {
|
|
||||||
if let Some(slot) = role_at.get_mut(byte) {
|
|
||||||
*slot = Some(role);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(build_lines(source, &role_at))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Maps a query capture name to a role; unmapped captures render as text.
|
/// Maps a query capture name to a role; unmapped captures render as text.
|
||||||
fn role_for_capture(name: &str) -> Option<Role> {
|
fn role_for_capture(name: &str) -> Option<Role> {
|
||||||
match name.split('.').next()? {
|
match name.split('.').next()? {
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ use std::cell::RefCell;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, Styled as _,
|
AppContext as _, Context, Entity, Focusable as _, IntoElement, ParentElement as _, Render,
|
||||||
TestAppContext, VisualTestContext, Window, div,
|
Styled as _, TestAppContext, VisualTestContext, Window, div,
|
||||||
};
|
};
|
||||||
use gpui_native_editor::code_view::CodeViewState;
|
use gpui_native_editor::code_view::{CodeViewState, bind_code_view_keys};
|
||||||
use gpui_native_editor::highlight::{HighlightedCode, Language, Role, highlight};
|
use gpui_native_editor::highlight::{HighlightedCode, Language, Role, highlight};
|
||||||
|
|
||||||
fn spans_with_role(code: &HighlightedCode, role: Role) -> Vec<&str> {
|
fn spans_with_role(code: &HighlightedCode, role: Role) -> Vec<&str> {
|
||||||
@@ -66,7 +66,11 @@ fn a_capture_spanning_newlines_styles_both_lines() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn set_source_rehighlights_with_the_new_language() {
|
fn set_source_rehighlights_with_the_new_language() {
|
||||||
let mut cx = TestAppContext::single();
|
let mut cx = TestAppContext::single();
|
||||||
let state = cx.new(|_| CodeViewState::new("test-code", "fn main() {}\n", Language::Rust));
|
cx.update(|cx| {
|
||||||
|
gpui_native_theme::Theme::init(cx);
|
||||||
|
bind_code_view_keys(cx);
|
||||||
|
});
|
||||||
|
let state = cx.new(|cx| CodeViewState::new("test-code", "fn main() {}\n", Language::Rust, cx));
|
||||||
|
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
state.update(cx, |state, cx| {
|
state.update(cx, |state, cx| {
|
||||||
@@ -98,16 +102,19 @@ fn code_view_window(
|
|||||||
line_count: usize,
|
line_count: usize,
|
||||||
) -> (TestAppContext, VisualTestContext, Entity<CodeViewState>) {
|
) -> (TestAppContext, VisualTestContext, Entity<CodeViewState>) {
|
||||||
let mut cx = TestAppContext::single();
|
let mut cx = TestAppContext::single();
|
||||||
cx.update(gpui_native_theme::Theme::init);
|
cx.update(|cx| {
|
||||||
|
gpui_native_theme::Theme::init(cx);
|
||||||
|
bind_code_view_keys(cx);
|
||||||
|
});
|
||||||
let source: String = (0..line_count)
|
let source: String = (0..line_count)
|
||||||
.map(|line| format!("fn line_{line}() {{}}\n"))
|
.map(|line| format!("fn line_{line}() {{}}\n"))
|
||||||
.collect();
|
.collect();
|
||||||
let state = CodeViewState::new("test-code", source, Language::Rust);
|
|
||||||
let state_cell: Rc<RefCell<Option<Entity<CodeViewState>>>> = Rc::new(RefCell::new(None));
|
let state_cell: Rc<RefCell<Option<Entity<CodeViewState>>>> = Rc::new(RefCell::new(None));
|
||||||
let cell = state_cell.clone();
|
let cell = state_cell.clone();
|
||||||
let window = cx.add_window(move |_, cx| {
|
let window = cx.add_window(move |window, cx| {
|
||||||
let code = cx.new(|_| state);
|
let code = cx.new(|cx| CodeViewState::new("test-code", source, Language::Rust, cx));
|
||||||
cell.borrow_mut().replace(code.clone());
|
cell.borrow_mut().replace(code.clone());
|
||||||
|
window.focus(&code.read(cx).focus_handle(cx), cx);
|
||||||
Host { code }
|
Host { code }
|
||||||
});
|
});
|
||||||
let state = state_cell.borrow().clone().expect("state captured");
|
let state = state_cell.borrow().clone().expect("state captured");
|
||||||
@@ -145,3 +152,99 @@ fn rows_render_virtualized_and_scroll() {
|
|||||||
"the first row scrolled out"
|
"the first row scrolled out"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hovering_a_line_activates_it() {
|
||||||
|
let (cx, mut visual, state) = code_view_window(80);
|
||||||
|
|
||||||
|
let row = visual
|
||||||
|
.debug_bounds("test-code-row-3")
|
||||||
|
.expect("row 3 renders in the viewport");
|
||||||
|
visual.simulate_event(gpui::MouseMoveEvent {
|
||||||
|
position: row.center(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let active = cx.update(|cx| state.read(cx).active_line());
|
||||||
|
assert_eq!(active, Some(3), "the hovered line becomes the active line");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn selecting_then_copying_writes_the_clipboard() {
|
||||||
|
let (cx, mut visual, state) = code_view_window(10);
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
state.update(cx, |state, cx| {
|
||||||
|
state.select_range(
|
||||||
|
gpui_native_editor::code_view::SelectionPoint { row: 0, col: 0 },
|
||||||
|
gpui_native_editor::code_view::SelectionPoint { row: 0, col: 3 },
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
visual.simulate_keystrokes("cmd-c");
|
||||||
|
|
||||||
|
let clipboard = cx.update(|cx| {
|
||||||
|
cx.read_from_clipboard()
|
||||||
|
.and_then(|item| item.text().map(|text| text.to_string()))
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
clipboard.as_deref(),
|
||||||
|
Some("fn "),
|
||||||
|
"the copy chord copies the selection"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_line_replaces_a_single_line_and_rehighlights() {
|
||||||
|
let mut cx = TestAppContext::single();
|
||||||
|
cx.update(|cx| {
|
||||||
|
gpui_native_theme::Theme::init(cx);
|
||||||
|
});
|
||||||
|
let state = cx.new(|cx| {
|
||||||
|
CodeViewState::new(
|
||||||
|
"test-code",
|
||||||
|
"fn a() {}\nfn b() {}\nfn c() {}\n",
|
||||||
|
Language::Rust,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
state.update(cx, |state, cx| state.update_line(1, "struct S;", cx));
|
||||||
|
});
|
||||||
|
|
||||||
|
let (source, line_count, keyword_on_line_1) = cx.update(|cx| {
|
||||||
|
let state = state.read(cx);
|
||||||
|
(
|
||||||
|
state.source().to_string(),
|
||||||
|
state.code().lines.len(),
|
||||||
|
state.code().lines[1]
|
||||||
|
.spans
|
||||||
|
.iter()
|
||||||
|
.any(|span| span.role == Role::Keyword),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(source, "fn a() {}\nstruct S;\nfn c() {}\n");
|
||||||
|
assert_eq!(line_count, 4, "the line count survives the replacement");
|
||||||
|
assert!(keyword_on_line_1, "struct highlights as a keyword");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_line_splices_multiline_text() {
|
||||||
|
let mut cx = TestAppContext::single();
|
||||||
|
cx.update(|cx| {
|
||||||
|
gpui_native_theme::Theme::init(cx);
|
||||||
|
});
|
||||||
|
let state =
|
||||||
|
cx.new(|cx| CodeViewState::new("test-code", "fn a() {}\nfn b() {}\n", Language::Rust, cx));
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
state.update(cx, |state, cx| state.update_line(1, "one\ntwo\nthree", cx));
|
||||||
|
});
|
||||||
|
|
||||||
|
let source = cx.update(|cx| state.read(cx).source().to_string());
|
||||||
|
assert_eq!(source, "fn a() {}\none\ntwo\nthree\n");
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use gpui::{
|
|||||||
ScrollDelta, ScrollEvent, Styled as _, TestAppContext, VisualTestContext, Window, div, point,
|
ScrollDelta, ScrollEvent, Styled as _, TestAppContext, VisualTestContext, Window, div, point,
|
||||||
px,
|
px,
|
||||||
};
|
};
|
||||||
use gpui_native_editor::code_view::CodeViewState;
|
use gpui_native_editor::code_view::{CodeViewState, bind_code_view_keys};
|
||||||
use gpui_native_editor::highlight::Language;
|
use gpui_native_editor::highlight::Language;
|
||||||
|
|
||||||
/// Deterministic pseudo-Rust with mixed tokens, roughly `lines` lines long.
|
/// Deterministic pseudo-Rust with mixed tokens, roughly `lines` lines long.
|
||||||
@@ -50,12 +50,17 @@ impl Render for Host {
|
|||||||
#[test]
|
#[test]
|
||||||
fn scrolling_a_large_file_is_not_pathologically_slow() {
|
fn scrolling_a_large_file_is_not_pathologically_slow() {
|
||||||
let mut cx = TestAppContext::single();
|
let mut cx = TestAppContext::single();
|
||||||
cx.update(gpui_native_theme::Theme::init);
|
cx.update(|cx| {
|
||||||
|
gpui_native_theme::Theme::init(cx);
|
||||||
|
bind_code_view_keys(cx);
|
||||||
|
});
|
||||||
|
|
||||||
let source = large_source(5000);
|
let source = large_source(5000);
|
||||||
let started = Instant::now();
|
let started = Instant::now();
|
||||||
let state = CodeViewState::new("bench-code", source.clone(), Language::Rust);
|
let (state, highlight_elapsed) = cx.update(|cx| {
|
||||||
let highlight_elapsed = started.elapsed();
|
let state = CodeViewState::new("bench-code", source.clone(), Language::Rust, cx);
|
||||||
|
(state, started.elapsed())
|
||||||
|
});
|
||||||
|
|
||||||
let state_cell: Rc<RefCell<Option<Entity<CodeViewState>>>> = Rc::new(RefCell::new(None));
|
let state_cell: Rc<RefCell<Option<Entity<CodeViewState>>>> = Rc::new(RefCell::new(None));
|
||||||
let cell = state_cell.clone();
|
let cell = state_cell.clone();
|
||||||
|
|||||||
+11
-5
@@ -9,15 +9,21 @@ is the first downstream consumer; its screens drive the order.
|
|||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
- [ ] **Diff view** (M15, `editor`).
|
- [ ] **Code view interactions** (M15, `editor`) — active line, text
|
||||||
- [ ] API sketch
|
selection with copy, incremental line re-highlight.
|
||||||
- [ ] Implementation
|
- [x] API sketch
|
||||||
- [ ] Tests
|
- [x] Implementation
|
||||||
- [ ] Gallery panel
|
- [x] Tests
|
||||||
|
- [x] Gallery panel
|
||||||
- [ ] Review
|
- [ ] Review
|
||||||
|
|
||||||
## Queue — to follow
|
## Queue — to follow
|
||||||
|
|
||||||
|
- [ ] **Diff view** (`editor`) — implemented, then reverted to
|
||||||
|
`stash@{0}` ("pre-revert: diff view + gutter tweaks"); restore with
|
||||||
|
`git stash pop` and re-land after the code view interactions settle.
|
||||||
|
- [ ] **Editor follow-ups** — language detection by file path, meta line
|
||||||
|
overlay, per-view config overrides (line height, gutter visibility).
|
||||||
- [ ] **Markdown view** (M16, `editor`) — only if a consumer confirms.
|
- [ ] **Markdown view** (M16, `editor`) — only if a consumer confirms.
|
||||||
- [ ] **Pickers** — Calendar / DatePicker, TimeField, ColorPicker (decide at
|
- [ ] **Pickers** — Calendar / DatePicker, TimeField, ColorPicker (decide at
|
||||||
unit time).
|
unit time).
|
||||||
|
|||||||
@@ -2845,7 +2845,8 @@ struct CodeViewDemo {
|
|||||||
|
|
||||||
impl CodeViewDemo {
|
impl CodeViewDemo {
|
||||||
fn new(cx: &mut Context<Self>) -> Self {
|
fn new(cx: &mut Context<Self>) -> Self {
|
||||||
let code = cx.new(|_| CodeViewState::new("code-view-demo", RUST_SAMPLE, Language::Rust));
|
let code =
|
||||||
|
cx.new(|cx| CodeViewState::new("code-view-demo", RUST_SAMPLE, Language::Rust, cx));
|
||||||
// The panel is a cached view in the dock: it re-renders only when
|
// The panel is a cached view in the dock: it re-renders only when
|
||||||
// the code state it renders is notified.
|
// the code state it renders is notified.
|
||||||
let subscription = cx.observe(&code, |_, _, cx| cx.notify());
|
let subscription = cx.observe(&code, |_, _, cx| cx.notify());
|
||||||
@@ -3214,6 +3215,7 @@ fn main() {
|
|||||||
.with_http_client(std::sync::Arc::new(reqwest_client::ReqwestClient::new()))
|
.with_http_client(std::sync::Arc::new(reqwest_client::ReqwestClient::new()))
|
||||||
.run(|cx: &mut App| {
|
.run(|cx: &mut App| {
|
||||||
gpui_native_core::init(cx);
|
gpui_native_core::init(cx);
|
||||||
|
gpui_native_editor::code_view::bind_code_view_keys(cx);
|
||||||
cx.bind_keys([
|
cx.bind_keys([
|
||||||
KeyBinding::new("cmd-k", OpenCommandPalette, None),
|
KeyBinding::new("cmd-k", OpenCommandPalette, None),
|
||||||
KeyBinding::new("ctrl-k", OpenCommandPalette, None),
|
KeyBinding::new("ctrl-k", OpenCommandPalette, None),
|
||||||
|
|||||||
Reference in New Issue
Block a user