Merge remote-tracking branch 'origin/main' into tokio
This commit is contained in:
@@ -814,14 +814,20 @@ pub mod tests {
|
||||
DisplayPoint::new(0, 7)
|
||||
);
|
||||
assert_eq!(
|
||||
movement::up(&snapshot, DisplayPoint::new(1, 10), SelectionGoal::None),
|
||||
movement::up(
|
||||
&snapshot,
|
||||
DisplayPoint::new(1, 10),
|
||||
SelectionGoal::None,
|
||||
false
|
||||
),
|
||||
(DisplayPoint::new(0, 7), SelectionGoal::Column(10))
|
||||
);
|
||||
assert_eq!(
|
||||
movement::down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(0, 7),
|
||||
SelectionGoal::Column(10)
|
||||
SelectionGoal::Column(10),
|
||||
false
|
||||
),
|
||||
(DisplayPoint::new(1, 10), SelectionGoal::Column(10))
|
||||
);
|
||||
@@ -829,7 +835,8 @@ pub mod tests {
|
||||
movement::down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(1, 10),
|
||||
SelectionGoal::Column(10)
|
||||
SelectionGoal::Column(10),
|
||||
false
|
||||
),
|
||||
(DisplayPoint::new(2, 4), SelectionGoal::Column(10))
|
||||
);
|
||||
|
||||
@@ -1134,8 +1134,10 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext<Self>) {
|
||||
self.display_map
|
||||
.update(cx, |map, _| map.clip_at_line_ends = clip);
|
||||
if self.display_map.read(cx).clip_at_line_ends != clip {
|
||||
self.display_map
|
||||
.update(cx, |map, _| map.clip_at_line_ends = clip);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_keymap_context_layer<Tag: 'static>(&mut self, context: gpui::keymap::Context) {
|
||||
@@ -3579,13 +3581,13 @@ impl Editor {
|
||||
if !selection.is_empty() {
|
||||
selection.goal = SelectionGoal::None;
|
||||
}
|
||||
let (cursor, goal) = movement::up(&map, selection.start, selection.goal);
|
||||
let (cursor, goal) = movement::up(&map, selection.start, selection.goal, false);
|
||||
selection.collapse_to(cursor, goal);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext<Self>) {
|
||||
self.move_selection_heads(cx, movement::up)
|
||||
self.move_selection_heads(cx, |map, head, goal| movement::up(map, head, goal, false))
|
||||
}
|
||||
|
||||
pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
|
||||
@@ -3606,13 +3608,13 @@ impl Editor {
|
||||
if !selection.is_empty() {
|
||||
selection.goal = SelectionGoal::None;
|
||||
}
|
||||
let (cursor, goal) = movement::down(&map, selection.end, selection.goal);
|
||||
let (cursor, goal) = movement::down(&map, selection.end, selection.goal, false);
|
||||
selection.collapse_to(cursor, goal);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext<Self>) {
|
||||
self.move_selection_heads(cx, movement::down)
|
||||
self.move_selection_heads(cx, |map, head, goal| movement::down(map, head, goal, false))
|
||||
}
|
||||
|
||||
pub fn move_to_previous_word_start(
|
||||
|
||||
@@ -16,6 +16,7 @@ use gpui::{
|
||||
PathBuilder,
|
||||
},
|
||||
json::{self, ToJson},
|
||||
platform::CursorStyle,
|
||||
text_layout::{self, Line, RunStyle, TextLayoutCache},
|
||||
AppContext, Axis, Border, Element, ElementBox, Event, EventContext, LayoutContext,
|
||||
MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
|
||||
@@ -329,6 +330,7 @@ impl EditorElement {
|
||||
let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.);
|
||||
|
||||
cx.scene.push_layer(Some(bounds));
|
||||
cx.scene.push_cursor_style(bounds, CursorStyle::IBeam);
|
||||
|
||||
for (range, color) in &layout.highlighted_ranges {
|
||||
self.paint_highlighted_range(
|
||||
|
||||
@@ -28,6 +28,7 @@ pub fn up(
|
||||
map: &DisplaySnapshot,
|
||||
start: DisplayPoint,
|
||||
goal: SelectionGoal,
|
||||
preserve_column_at_start: bool,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let mut goal_column = if let SelectionGoal::Column(column) = goal {
|
||||
column
|
||||
@@ -42,6 +43,8 @@ pub fn up(
|
||||
);
|
||||
if point.row() < start.row() {
|
||||
*point.column_mut() = map.column_from_chars(point.row(), goal_column);
|
||||
} else if preserve_column_at_start {
|
||||
return (start, goal);
|
||||
} else {
|
||||
point = DisplayPoint::new(0, 0);
|
||||
goal_column = 0;
|
||||
@@ -63,6 +66,7 @@ pub fn down(
|
||||
map: &DisplaySnapshot,
|
||||
start: DisplayPoint,
|
||||
goal: SelectionGoal,
|
||||
preserve_column_at_end: bool,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let mut goal_column = if let SelectionGoal::Column(column) = goal {
|
||||
column
|
||||
@@ -74,6 +78,8 @@ pub fn down(
|
||||
let mut point = map.clip_point(DisplayPoint::new(next_row, 0), Bias::Right);
|
||||
if point.row() > start.row() {
|
||||
*point.column_mut() = map.column_from_chars(point.row(), goal_column);
|
||||
} else if preserve_column_at_end {
|
||||
return (start, goal);
|
||||
} else {
|
||||
point = map.max_point();
|
||||
goal_column = map.column_to_chars(point.row(), point.column())
|
||||
@@ -503,41 +509,81 @@ mod tests {
|
||||
|
||||
// Can't move up into the first excerpt's header
|
||||
assert_eq!(
|
||||
up(&snapshot, DisplayPoint::new(2, 2), SelectionGoal::Column(2)),
|
||||
up(
|
||||
&snapshot,
|
||||
DisplayPoint::new(2, 2),
|
||||
SelectionGoal::Column(2),
|
||||
false
|
||||
),
|
||||
(DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
|
||||
);
|
||||
assert_eq!(
|
||||
up(&snapshot, DisplayPoint::new(2, 0), SelectionGoal::None),
|
||||
up(
|
||||
&snapshot,
|
||||
DisplayPoint::new(2, 0),
|
||||
SelectionGoal::None,
|
||||
false
|
||||
),
|
||||
(DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
|
||||
);
|
||||
|
||||
// Move up and down within first excerpt
|
||||
assert_eq!(
|
||||
up(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
|
||||
up(
|
||||
&snapshot,
|
||||
DisplayPoint::new(3, 4),
|
||||
SelectionGoal::Column(4),
|
||||
false
|
||||
),
|
||||
(DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
|
||||
);
|
||||
assert_eq!(
|
||||
down(&snapshot, DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
|
||||
down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(2, 3),
|
||||
SelectionGoal::Column(4),
|
||||
false
|
||||
),
|
||||
(DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
|
||||
);
|
||||
|
||||
// Move up and down across second excerpt's header
|
||||
assert_eq!(
|
||||
up(&snapshot, DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
|
||||
up(
|
||||
&snapshot,
|
||||
DisplayPoint::new(6, 5),
|
||||
SelectionGoal::Column(5),
|
||||
false
|
||||
),
|
||||
(DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
|
||||
);
|
||||
assert_eq!(
|
||||
down(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
|
||||
down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(3, 4),
|
||||
SelectionGoal::Column(5),
|
||||
false
|
||||
),
|
||||
(DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
|
||||
);
|
||||
|
||||
// Can't move down off the end
|
||||
assert_eq!(
|
||||
down(&snapshot, DisplayPoint::new(7, 0), SelectionGoal::Column(0)),
|
||||
down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(7, 0),
|
||||
SelectionGoal::Column(0),
|
||||
false
|
||||
),
|
||||
(DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
|
||||
);
|
||||
assert_eq!(
|
||||
down(&snapshot, DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
|
||||
down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(7, 2),
|
||||
SelectionGoal::Column(2),
|
||||
false
|
||||
),
|
||||
(DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -161,29 +161,25 @@ impl View for GoToLine {
|
||||
self.max_point.row + 1
|
||||
);
|
||||
|
||||
Align::new(
|
||||
ConstrainedBox::new(
|
||||
Container::new(
|
||||
Flex::new(Axis::Vertical)
|
||||
.with_child(
|
||||
Container::new(ChildView::new(&self.line_editor).boxed())
|
||||
.with_style(theme.input_editor.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(Label::new(label, theme.empty.label.clone()).boxed())
|
||||
.with_style(theme.empty.container)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(theme.container)
|
||||
.boxed(),
|
||||
ConstrainedBox::new(
|
||||
Container::new(
|
||||
Flex::new(Axis::Vertical)
|
||||
.with_child(
|
||||
Container::new(ChildView::new(&self.line_editor).boxed())
|
||||
.with_style(theme.input_editor.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(Label::new(label, theme.empty.label.clone()).boxed())
|
||||
.with_style(theme.empty.container)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_max_width(500.0)
|
||||
.with_style(theme.container)
|
||||
.boxed(),
|
||||
)
|
||||
.top()
|
||||
.with_max_width(500.0)
|
||||
.named("go to line")
|
||||
}
|
||||
|
||||
|
||||
+64
-33
@@ -4,7 +4,7 @@ use crate::{
|
||||
elements::ElementBox,
|
||||
executor::{self, Task},
|
||||
keymap::{self, Binding, Keystroke},
|
||||
platform::{self, CursorStyle, Platform, PromptLevel, WindowOptions},
|
||||
platform::{self, Platform, PromptLevel, WindowOptions},
|
||||
presenter::Presenter,
|
||||
util::post_inc,
|
||||
AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache,
|
||||
@@ -31,10 +31,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
pin::Pin,
|
||||
rc::{self, Rc},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering::SeqCst},
|
||||
Arc, Weak,
|
||||
},
|
||||
sync::{Arc, Weak},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
@@ -766,7 +763,6 @@ pub struct MutableAppContext {
|
||||
pending_global_notifications: HashSet<TypeId>,
|
||||
pending_flushes: usize,
|
||||
flushing_effects: bool,
|
||||
next_cursor_style_handle_id: Arc<AtomicUsize>,
|
||||
halt_action_dispatch: bool,
|
||||
}
|
||||
|
||||
@@ -818,7 +814,6 @@ impl MutableAppContext {
|
||||
pending_global_notifications: HashSet::new(),
|
||||
pending_flushes: 0,
|
||||
flushing_effects: false,
|
||||
next_cursor_style_handle_id: Default::default(),
|
||||
halt_action_dispatch: false,
|
||||
}
|
||||
}
|
||||
@@ -1596,6 +1591,7 @@ impl MutableAppContext {
|
||||
Window {
|
||||
root_view: root_view.clone().into(),
|
||||
focused_view_id: Some(root_view.id()),
|
||||
is_active: true,
|
||||
invalidation: None,
|
||||
},
|
||||
);
|
||||
@@ -1643,10 +1639,17 @@ impl MutableAppContext {
|
||||
}));
|
||||
}
|
||||
|
||||
{
|
||||
let mut app = self.upgrade();
|
||||
window.on_active_status_change(Box::new(move |is_active| {
|
||||
app.update(|cx| cx.window_changed_active_status(window_id, is_active))
|
||||
}));
|
||||
}
|
||||
|
||||
{
|
||||
let mut app = self.upgrade();
|
||||
window.on_resize(Box::new(move || {
|
||||
app.update(|cx| cx.resize_window(window_id))
|
||||
app.update(|cx| cx.window_was_resized(window_id))
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1861,6 +1864,10 @@ impl MutableAppContext {
|
||||
.get_or_insert(WindowInvalidation::default());
|
||||
}
|
||||
}
|
||||
Effect::ActivateWindow {
|
||||
window_id,
|
||||
is_active,
|
||||
} => self.handle_activation_effect(window_id, is_active),
|
||||
Effect::RefreshWindows => {
|
||||
refreshing = true;
|
||||
}
|
||||
@@ -1919,11 +1926,18 @@ impl MutableAppContext {
|
||||
}
|
||||
}
|
||||
|
||||
fn resize_window(&mut self, window_id: usize) {
|
||||
fn window_was_resized(&mut self, window_id: usize) {
|
||||
self.pending_effects
|
||||
.push_back(Effect::ResizeWindow { window_id });
|
||||
}
|
||||
|
||||
fn window_changed_active_status(&mut self, window_id: usize, is_active: bool) {
|
||||
self.pending_effects.push_back(Effect::ActivateWindow {
|
||||
window_id,
|
||||
is_active,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn refresh_windows(&mut self) {
|
||||
self.pending_effects.push_back(Effect::RefreshWindows);
|
||||
}
|
||||
@@ -1949,16 +1963,6 @@ impl MutableAppContext {
|
||||
self.presenters_and_platform_windows = presenters;
|
||||
}
|
||||
|
||||
pub fn set_cursor_style(&mut self, style: CursorStyle) -> CursorStyleHandle {
|
||||
self.platform.set_cursor_style(style);
|
||||
let id = self.next_cursor_style_handle_id.fetch_add(1, SeqCst);
|
||||
CursorStyleHandle {
|
||||
id,
|
||||
next_cursor_style_handle_id: self.next_cursor_style_handle_id.clone(),
|
||||
platform: self.platform(),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_subscription_effect(
|
||||
&mut self,
|
||||
entity_id: usize,
|
||||
@@ -2219,6 +2223,34 @@ impl MutableAppContext {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_activation_effect(&mut self, window_id: usize, active: bool) {
|
||||
if self
|
||||
.cx
|
||||
.windows
|
||||
.get(&window_id)
|
||||
.map(|w| w.is_active)
|
||||
.map_or(false, |cur_active| cur_active == active)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.update(|this| {
|
||||
let window = this.cx.windows.get_mut(&window_id)?;
|
||||
window.is_active = active;
|
||||
let view_id = window.focused_view_id?;
|
||||
if let Some(mut view) = this.cx.views.remove(&(window_id, view_id)) {
|
||||
if active {
|
||||
view.on_focus(this, window_id, view_id);
|
||||
} else {
|
||||
view.on_blur(this, window_id, view_id);
|
||||
}
|
||||
this.cx.views.insert((window_id, view_id), view);
|
||||
}
|
||||
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_focus_effect(&mut self, window_id: usize, focused_id: Option<usize>) {
|
||||
self.pending_focus_index.take();
|
||||
|
||||
@@ -2582,6 +2614,7 @@ impl ReadView for AppContext {
|
||||
struct Window {
|
||||
root_view: AnyViewHandle,
|
||||
focused_view_id: Option<usize>,
|
||||
is_active: bool,
|
||||
invalidation: Option<WindowInvalidation>,
|
||||
}
|
||||
|
||||
@@ -2648,6 +2681,10 @@ pub enum Effect {
|
||||
ResizeWindow {
|
||||
window_id: usize,
|
||||
},
|
||||
ActivateWindow {
|
||||
window_id: usize,
|
||||
is_active: bool,
|
||||
},
|
||||
RefreshWindows,
|
||||
}
|
||||
|
||||
@@ -2729,6 +2766,14 @@ impl Debug for Effect {
|
||||
.debug_struct("Effect::RefreshWindow")
|
||||
.field("window_id", window_id)
|
||||
.finish(),
|
||||
Effect::ActivateWindow {
|
||||
window_id,
|
||||
is_active,
|
||||
} => f
|
||||
.debug_struct("Effect::ActivateWindow")
|
||||
.field("window_id", window_id)
|
||||
.field("is_active", is_active)
|
||||
.finish(),
|
||||
Effect::RefreshWindows => f.debug_struct("Effect::FullViewRefresh").finish(),
|
||||
}
|
||||
}
|
||||
@@ -4452,20 +4497,6 @@ impl<T> Drop for ElementStateHandle<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CursorStyleHandle {
|
||||
id: usize,
|
||||
next_cursor_style_handle_id: Arc<AtomicUsize>,
|
||||
platform: Arc<dyn Platform>,
|
||||
}
|
||||
|
||||
impl Drop for CursorStyleHandle {
|
||||
fn drop(&mut self) {
|
||||
if self.id + 1 == self.next_cursor_style_handle_id.load(SeqCst) {
|
||||
self.platform.set_cursor_style(CursorStyle::Arrow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub enum Subscription {
|
||||
Subscription {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use pathfinder_geometry::rect::RectF;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
color::Color,
|
||||
geometry::{
|
||||
deserialize_vec2f,
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
json::ToJson,
|
||||
platform::CursorStyle,
|
||||
scene::{self, Border, Quad},
|
||||
Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize)]
|
||||
pub struct ContainerStyle {
|
||||
@@ -27,6 +27,8 @@ pub struct ContainerStyle {
|
||||
pub corner_radius: f32,
|
||||
#[serde(default)]
|
||||
pub shadow: Option<Shadow>,
|
||||
#[serde(default)]
|
||||
pub cursor: Option<CursorStyle>,
|
||||
}
|
||||
|
||||
pub struct Container {
|
||||
@@ -128,6 +130,11 @@ impl Container {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_cursor(mut self, style: CursorStyle) -> Self {
|
||||
self.style.cursor = Some(style);
|
||||
self
|
||||
}
|
||||
|
||||
fn margin_size(&self) -> Vector2F {
|
||||
vec2f(
|
||||
self.style.margin.left + self.style.margin.right,
|
||||
@@ -205,6 +212,10 @@ impl Element for Container {
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(style) = self.style.cursor {
|
||||
cx.scene.push_cursor_style(quad_bounds, style);
|
||||
}
|
||||
|
||||
let child_origin =
|
||||
quad_bounds.origin() + vec2f(self.style.padding.left, self.style.padding.top);
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ use crate::{
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
platform::CursorStyle,
|
||||
CursorStyleHandle, DebugContext, Element, ElementBox, ElementStateContext, ElementStateHandle,
|
||||
Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
|
||||
DebugContext, Element, ElementBox, ElementStateContext, ElementStateHandle, Event,
|
||||
EventContext, LayoutContext, PaintContext, SizeConstraint,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -25,7 +25,6 @@ pub struct MouseState {
|
||||
pub hovered: bool,
|
||||
pub clicked: bool,
|
||||
prev_drag_position: Option<Vector2F>,
|
||||
cursor_style_handle: Option<CursorStyleHandle>,
|
||||
}
|
||||
|
||||
impl MouseEventHandler {
|
||||
@@ -72,6 +71,14 @@ impl MouseEventHandler {
|
||||
self.padding = padding;
|
||||
self
|
||||
}
|
||||
|
||||
fn hit_bounds(&self, bounds: RectF) -> RectF {
|
||||
RectF::from_points(
|
||||
bounds.origin() - vec2f(self.padding.left, self.padding.top),
|
||||
bounds.lower_right() + vec2f(self.padding.right, self.padding.bottom),
|
||||
)
|
||||
.round_out()
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for MouseEventHandler {
|
||||
@@ -93,6 +100,10 @@ impl Element for MouseEventHandler {
|
||||
_: &mut Self::LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) -> Self::PaintState {
|
||||
if let Some(cursor_style) = self.cursor_style {
|
||||
cx.scene
|
||||
.push_cursor_style(self.hit_bounds(bounds), cursor_style);
|
||||
}
|
||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||
}
|
||||
|
||||
@@ -105,19 +116,13 @@ impl Element for MouseEventHandler {
|
||||
_: &mut Self::PaintState,
|
||||
cx: &mut EventContext,
|
||||
) -> bool {
|
||||
let cursor_style = self.cursor_style;
|
||||
let hit_bounds = self.hit_bounds(visible_bounds);
|
||||
let mouse_down_handler = self.mouse_down_handler.as_mut();
|
||||
let click_handler = self.click_handler.as_mut();
|
||||
let drag_handler = self.drag_handler.as_mut();
|
||||
|
||||
let handled_in_child = self.child.dispatch_event(event, cx);
|
||||
|
||||
let hit_bounds = RectF::from_points(
|
||||
visible_bounds.origin() - vec2f(self.padding.left, self.padding.top),
|
||||
visible_bounds.lower_right() + vec2f(self.padding.right, self.padding.bottom),
|
||||
)
|
||||
.round_out();
|
||||
|
||||
self.state.update(cx, |state, cx| match event {
|
||||
Event::MouseMoved {
|
||||
position,
|
||||
@@ -127,16 +132,6 @@ impl Element for MouseEventHandler {
|
||||
let mouse_in = hit_bounds.contains_point(*position);
|
||||
if state.hovered != mouse_in {
|
||||
state.hovered = mouse_in;
|
||||
if let Some(cursor_style) = cursor_style {
|
||||
if !state.clicked {
|
||||
if state.hovered {
|
||||
state.cursor_style_handle =
|
||||
Some(cx.set_cursor_style(cursor_style));
|
||||
} else {
|
||||
state.cursor_style_handle = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
return true;
|
||||
}
|
||||
@@ -160,9 +155,6 @@ impl Element for MouseEventHandler {
|
||||
state.prev_drag_position = None;
|
||||
if !handled_in_child && state.clicked {
|
||||
state.clicked = false;
|
||||
if !state.hovered {
|
||||
state.cursor_style_handle = None;
|
||||
}
|
||||
cx.notify();
|
||||
if let Some(handler) = click_handler {
|
||||
if hit_bounds.contains_point(*position) {
|
||||
|
||||
@@ -21,6 +21,7 @@ use anyhow::{anyhow, Result};
|
||||
use async_task::Runnable;
|
||||
pub use event::{Event, NavigationDirection};
|
||||
use postage::oneshot;
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
any::Any,
|
||||
path::{Path, PathBuf},
|
||||
@@ -86,6 +87,7 @@ pub trait Dispatcher: Send + Sync {
|
||||
pub trait Window: WindowContext {
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
fn on_event(&mut self, callback: Box<dyn FnMut(Event)>);
|
||||
fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>);
|
||||
fn on_resize(&mut self, callback: Box<dyn FnMut()>);
|
||||
fn on_close(&mut self, callback: Box<dyn FnOnce()>);
|
||||
fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
|
||||
@@ -125,11 +127,12 @@ pub enum PromptLevel {
|
||||
Critical,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[derive(Copy, Clone, Debug, Deserialize)]
|
||||
pub enum CursorStyle {
|
||||
Arrow,
|
||||
ResizeLeftRight,
|
||||
PointingHand,
|
||||
IBeam,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
|
||||
@@ -583,6 +583,7 @@ impl platform::Platform for MacPlatform {
|
||||
CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
|
||||
CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
|
||||
CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
|
||||
CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor],
|
||||
};
|
||||
let _: () = msg_send![cursor, set];
|
||||
}
|
||||
|
||||
@@ -73,6 +73,14 @@ unsafe fn build_classes() {
|
||||
sel!(windowDidResize:),
|
||||
window_did_resize as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(windowDidBecomeKey:),
|
||||
window_did_change_key_status as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(windowDidResignKey:),
|
||||
window_did_change_key_status as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel));
|
||||
decl.register()
|
||||
};
|
||||
@@ -157,6 +165,7 @@ struct WindowState {
|
||||
id: usize,
|
||||
native_window: id,
|
||||
event_callback: Option<Box<dyn FnMut(Event)>>,
|
||||
activate_callback: Option<Box<dyn FnMut(bool)>>,
|
||||
resize_callback: Option<Box<dyn FnMut()>>,
|
||||
close_callback: Option<Box<dyn FnOnce()>>,
|
||||
synthetic_drag_counter: usize,
|
||||
@@ -234,6 +243,7 @@ impl Window {
|
||||
event_callback: None,
|
||||
resize_callback: None,
|
||||
close_callback: None,
|
||||
activate_callback: None,
|
||||
synthetic_drag_counter: 0,
|
||||
executor,
|
||||
scene_to_render: Default::default(),
|
||||
@@ -333,6 +343,10 @@ impl platform::Window for Window {
|
||||
self.0.as_ref().borrow_mut().close_callback = Some(callback);
|
||||
}
|
||||
|
||||
fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>) {
|
||||
self.0.as_ref().borrow_mut().activate_callback = Some(callback);
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
level: platform::PromptLevel,
|
||||
@@ -598,6 +612,29 @@ extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
|
||||
window_state.as_ref().borrow().move_traffic_light();
|
||||
}
|
||||
|
||||
extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) {
|
||||
let is_active = if selector == sel!(windowDidBecomeKey:) {
|
||||
true
|
||||
} else if selector == sel!(windowDidResignKey:) {
|
||||
false
|
||||
} else {
|
||||
unreachable!();
|
||||
};
|
||||
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
let executor = window_state.as_ref().borrow().executor.clone();
|
||||
executor
|
||||
.spawn(async move {
|
||||
let mut window_state_borrow = window_state.as_ref().borrow_mut();
|
||||
if let Some(mut callback) = window_state_borrow.activate_callback.take() {
|
||||
drop(window_state_borrow);
|
||||
callback(is_active);
|
||||
window_state.borrow_mut().activate_callback = Some(callback);
|
||||
};
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
extern "C" fn close_window(this: &Object, _: Sel) {
|
||||
unsafe {
|
||||
let close_callback = {
|
||||
|
||||
@@ -229,6 +229,8 @@ impl super::Window for Window {
|
||||
self.event_handlers.push(callback);
|
||||
}
|
||||
|
||||
fn on_active_status_change(&mut self, _: Box<dyn FnMut(bool)>) {}
|
||||
|
||||
fn on_resize(&mut self, callback: Box<dyn FnMut()>) {
|
||||
self.resize_handlers.push(callback);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
font_cache::FontCache,
|
||||
geometry::rect::RectF,
|
||||
json::{self, ToJson},
|
||||
platform::Event,
|
||||
platform::{CursorStyle, Event},
|
||||
text_layout::TextLayoutCache,
|
||||
Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox,
|
||||
ElementStateContext, Entity, FontSystem, ModelHandle, ReadModel, ReadView, Scene,
|
||||
@@ -22,6 +22,7 @@ pub struct Presenter {
|
||||
window_id: usize,
|
||||
pub(crate) rendered_views: HashMap<usize, ElementBox>,
|
||||
parents: HashMap<usize, usize>,
|
||||
cursor_styles: Vec<(RectF, CursorStyle)>,
|
||||
font_cache: Arc<FontCache>,
|
||||
text_layout_cache: TextLayoutCache,
|
||||
asset_cache: Arc<AssetCache>,
|
||||
@@ -42,6 +43,7 @@ impl Presenter {
|
||||
window_id,
|
||||
rendered_views: cx.render_views(window_id, titlebar_height),
|
||||
parents: HashMap::new(),
|
||||
cursor_styles: Default::default(),
|
||||
font_cache,
|
||||
text_layout_cache,
|
||||
asset_cache,
|
||||
@@ -118,6 +120,7 @@ impl Presenter {
|
||||
RectF::new(Vector2F::zero(), window_size),
|
||||
);
|
||||
self.text_layout_cache.finish_frame();
|
||||
self.cursor_styles = scene.cursor_styles();
|
||||
|
||||
if let Some(event) = self.last_mouse_moved_event.clone() {
|
||||
self.dispatch_event(event, cx)
|
||||
@@ -171,8 +174,22 @@ impl Presenter {
|
||||
pub fn dispatch_event(&mut self, event: Event, cx: &mut MutableAppContext) {
|
||||
if let Some(root_view_id) = cx.root_view_id(self.window_id) {
|
||||
match event {
|
||||
Event::MouseMoved { .. } => {
|
||||
Event::MouseMoved {
|
||||
position,
|
||||
left_mouse_down,
|
||||
} => {
|
||||
self.last_mouse_moved_event = Some(event.clone());
|
||||
|
||||
if !left_mouse_down {
|
||||
let mut style_to_assign = CursorStyle::Arrow;
|
||||
for (bounds, style) in self.cursor_styles.iter().rev() {
|
||||
if bounds.contains_point(position) {
|
||||
style_to_assign = *style;
|
||||
break;
|
||||
}
|
||||
}
|
||||
cx.platform().set_cursor_style(style_to_assign);
|
||||
}
|
||||
}
|
||||
Event::LeftMouseDragged { position } => {
|
||||
self.last_mouse_moved_event = Some(Event::MouseMoved {
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::{
|
||||
fonts::{FontId, GlyphId},
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
json::ToJson,
|
||||
platform::CursorStyle,
|
||||
ImageData,
|
||||
};
|
||||
|
||||
@@ -32,6 +33,7 @@ pub struct Layer {
|
||||
image_glyphs: Vec<ImageGlyph>,
|
||||
icons: Vec<Icon>,
|
||||
paths: Vec<Path>,
|
||||
cursor_styles: Vec<(RectF, CursorStyle)>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
@@ -173,6 +175,13 @@ impl Scene {
|
||||
self.stacking_contexts.iter().flat_map(|s| &s.layers)
|
||||
}
|
||||
|
||||
pub fn cursor_styles(&self) -> Vec<(RectF, CursorStyle)> {
|
||||
self.layers()
|
||||
.flat_map(|layer| &layer.cursor_styles)
|
||||
.copied()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn push_stacking_context(&mut self, clip_bounds: Option<RectF>) {
|
||||
self.active_stacking_context_stack
|
||||
.push(self.stacking_contexts.len());
|
||||
@@ -197,6 +206,10 @@ impl Scene {
|
||||
self.active_layer().push_quad(quad)
|
||||
}
|
||||
|
||||
pub fn push_cursor_style(&mut self, bounds: RectF, style: CursorStyle) {
|
||||
self.active_layer().push_cursor_style(bounds, style);
|
||||
}
|
||||
|
||||
pub fn push_image(&mut self, image: Image) {
|
||||
self.active_layer().push_image(image)
|
||||
}
|
||||
@@ -285,6 +298,7 @@ impl Layer {
|
||||
glyphs: Default::default(),
|
||||
icons: Default::default(),
|
||||
paths: Default::default(),
|
||||
cursor_styles: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,6 +316,14 @@ impl Layer {
|
||||
self.quads.as_slice()
|
||||
}
|
||||
|
||||
fn push_cursor_style(&mut self, bounds: RectF, style: CursorStyle) {
|
||||
if let Some(bounds) = bounds.intersection(self.clip_bounds.unwrap_or(bounds)) {
|
||||
if can_draw(bounds) {
|
||||
self.cursor_styles.push((bounds, style));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn push_underline(&mut self, underline: Underline) {
|
||||
if underline.width > 0. {
|
||||
self.underlines.push(underline);
|
||||
|
||||
@@ -15,6 +15,7 @@ test-support = [
|
||||
"lsp/test-support",
|
||||
"text/test-support",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript",
|
||||
"util/test-support",
|
||||
]
|
||||
|
||||
@@ -45,7 +46,8 @@ similar = "1.3"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "1.2"
|
||||
tree-sitter = "0.20"
|
||||
tree-sitter-rust = { version = "0.20.0", optional = true }
|
||||
tree-sitter-rust = { version = "*", optional = true }
|
||||
tree-sitter-typescript = { version = "*", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
@@ -57,6 +59,6 @@ util = { path = "../util", features = ["test-support"] }
|
||||
ctor = "0.1"
|
||||
env_logger = "0.8"
|
||||
rand = "0.8.3"
|
||||
tree-sitter-json = "0.19.0"
|
||||
tree-sitter-rust = "0.20.0"
|
||||
tree-sitter-json = "*"
|
||||
tree-sitter-rust = "*"
|
||||
unindent = "0.1.7"
|
||||
|
||||
@@ -38,7 +38,7 @@ use tree_sitter::{InputEdit, QueryCursor, Tree};
|
||||
use util::TryFutureExt as _;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use tree_sitter_rust;
|
||||
pub use {tree_sitter_rust, tree_sitter_typescript};
|
||||
|
||||
pub use lsp::DiagnosticSeverity;
|
||||
|
||||
@@ -1638,6 +1638,34 @@ impl BufferSnapshot {
|
||||
.and_then(|language| language.grammar.as_ref())
|
||||
}
|
||||
|
||||
pub fn range_for_word_token_at<T: ToOffset + ToPoint>(
|
||||
&self,
|
||||
position: T,
|
||||
) -> Option<Range<usize>> {
|
||||
let offset = position.to_offset(self);
|
||||
|
||||
// Find the first leaf node that touches the position.
|
||||
let tree = self.tree.as_ref()?;
|
||||
let mut cursor = tree.root_node().walk();
|
||||
while cursor.goto_first_child_for_byte(offset).is_some() {}
|
||||
let node = cursor.node();
|
||||
if node.child_count() > 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Check that the leaf node contains word characters.
|
||||
let range = node.byte_range();
|
||||
if self
|
||||
.text_for_range(range.clone())
|
||||
.flat_map(str::chars)
|
||||
.any(|c| c.is_alphanumeric())
|
||||
{
|
||||
return Some(range);
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
|
||||
let tree = self.tree.as_ref()?;
|
||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||
@@ -1730,7 +1758,7 @@ impl BufferSnapshot {
|
||||
.and_then(|language| language.grammar.as_ref())?;
|
||||
|
||||
let mut cursor = QueryCursorHandle::new();
|
||||
cursor.set_byte_range(range);
|
||||
cursor.set_byte_range(range.clone());
|
||||
let matches = cursor.matches(
|
||||
&grammar.outline_query,
|
||||
tree.root_node(),
|
||||
@@ -1750,7 +1778,10 @@ impl BufferSnapshot {
|
||||
let items = matches
|
||||
.filter_map(|mat| {
|
||||
let item_node = mat.nodes_for_capture_index(item_capture_ix).next()?;
|
||||
let range = item_node.start_byte()..item_node.end_byte();
|
||||
let item_range = item_node.start_byte()..item_node.end_byte();
|
||||
if item_range.end < range.start || item_range.start > range.end {
|
||||
return None;
|
||||
}
|
||||
let mut text = String::new();
|
||||
let mut name_ranges = Vec::new();
|
||||
let mut highlight_ranges = Vec::new();
|
||||
@@ -1808,15 +1839,15 @@ impl BufferSnapshot {
|
||||
}
|
||||
|
||||
while stack.last().map_or(false, |prev_range| {
|
||||
!prev_range.contains(&range.start) || !prev_range.contains(&range.end)
|
||||
!prev_range.contains(&item_range.start) || !prev_range.contains(&item_range.end)
|
||||
}) {
|
||||
stack.pop();
|
||||
}
|
||||
stack.push(range.clone());
|
||||
stack.push(item_range.clone());
|
||||
|
||||
Some(OutlineItem {
|
||||
depth: stack.len() - 1,
|
||||
range: self.anchor_after(range.start)..self.anchor_before(range.end),
|
||||
range: self.anchor_after(item_range.start)..self.anchor_before(item_range.end),
|
||||
text,
|
||||
highlight_ranges,
|
||||
name_ranges,
|
||||
|
||||
@@ -51,6 +51,10 @@ impl HighlightMap {
|
||||
}
|
||||
|
||||
impl HighlightId {
|
||||
pub fn is_default(&self) -> bool {
|
||||
*self == DEFAULT_SYNTAX_HIGHLIGHT_ID
|
||||
}
|
||||
|
||||
pub fn style(&self, theme: &SyntaxTheme) -> Option<HighlightStyle> {
|
||||
theme
|
||||
.highlights
|
||||
|
||||
@@ -546,7 +546,9 @@ impl Language {
|
||||
{
|
||||
let end_offset = offset + chunk.text.len();
|
||||
if let Some(highlight_id) = chunk.syntax_highlight_id {
|
||||
result.push((offset..end_offset, highlight_id));
|
||||
if !highlight_id.is_default() {
|
||||
result.push((offset..end_offset, highlight_id));
|
||||
}
|
||||
}
|
||||
offset = end_offset;
|
||||
}
|
||||
|
||||
@@ -99,8 +99,6 @@ impl<D: PickerDelegate> View for Picker<D> {
|
||||
.constrained()
|
||||
.with_max_width(self.max_size.x())
|
||||
.with_max_height(self.max_size.y())
|
||||
.aligned()
|
||||
.top()
|
||||
.named("picker")
|
||||
}
|
||||
|
||||
|
||||
+125
-35
@@ -669,6 +669,10 @@ impl Project {
|
||||
.map(|worktree| worktree.read(cx).id())
|
||||
}
|
||||
|
||||
pub fn can_share(&self, cx: &AppContext) -> bool {
|
||||
self.is_local() && self.visible_worktrees(cx).next().is_some()
|
||||
}
|
||||
|
||||
pub fn share(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
let rpc = self.client.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
@@ -2488,26 +2492,51 @@ impl Project {
|
||||
};
|
||||
|
||||
source_buffer_handle.read_with(&cx, |this, _| {
|
||||
let snapshot = this.snapshot();
|
||||
let clipped_position = this.clip_point_utf16(position, Bias::Left);
|
||||
let mut range_for_token = None;
|
||||
Ok(completions
|
||||
.into_iter()
|
||||
.filter_map(|lsp_completion| {
|
||||
let (old_range, new_text) = match lsp_completion.text_edit.as_ref() {
|
||||
// If the language server provides a range to overwrite, then
|
||||
// check that the range is valid.
|
||||
Some(lsp::CompletionTextEdit::Edit(edit)) => {
|
||||
(range_from_lsp(edit.range), edit.new_text.clone())
|
||||
}
|
||||
None => {
|
||||
let clipped_position =
|
||||
this.clip_point_utf16(position, Bias::Left);
|
||||
if position != clipped_position {
|
||||
let range = range_from_lsp(edit.range);
|
||||
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
|
||||
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
|
||||
if start != range.start || end != range.end {
|
||||
log::info!("completion out of expected range");
|
||||
return None;
|
||||
}
|
||||
(
|
||||
this.common_prefix_at(
|
||||
clipped_position,
|
||||
&lsp_completion.label,
|
||||
),
|
||||
lsp_completion.label.clone(),
|
||||
snapshot.anchor_before(start)..snapshot.anchor_after(end),
|
||||
edit.new_text.clone(),
|
||||
)
|
||||
}
|
||||
// If the language server does not provide a range, then infer
|
||||
// the range based on the syntax tree.
|
||||
None => {
|
||||
if position != clipped_position {
|
||||
log::info!("completion out of expected range");
|
||||
return None;
|
||||
}
|
||||
let Range { start, end } = range_for_token
|
||||
.get_or_insert_with(|| {
|
||||
let offset = position.to_offset(&snapshot);
|
||||
snapshot
|
||||
.range_for_word_token_at(offset)
|
||||
.unwrap_or_else(|| offset..offset)
|
||||
})
|
||||
.clone();
|
||||
let text = lsp_completion
|
||||
.insert_text
|
||||
.as_ref()
|
||||
.unwrap_or(&lsp_completion.label)
|
||||
.clone();
|
||||
(
|
||||
snapshot.anchor_before(start)..snapshot.anchor_after(end),
|
||||
text.clone(),
|
||||
)
|
||||
}
|
||||
Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => {
|
||||
@@ -2516,28 +2545,20 @@ impl Project {
|
||||
}
|
||||
};
|
||||
|
||||
let clipped_start = this.clip_point_utf16(old_range.start, Bias::Left);
|
||||
let clipped_end = this.clip_point_utf16(old_range.end, Bias::Left);
|
||||
if clipped_start == old_range.start && clipped_end == old_range.end {
|
||||
Some(Completion {
|
||||
old_range: this.anchor_before(old_range.start)
|
||||
..this.anchor_after(old_range.end),
|
||||
new_text,
|
||||
label: language
|
||||
.as_ref()
|
||||
.and_then(|l| l.label_for_completion(&lsp_completion))
|
||||
.unwrap_or_else(|| {
|
||||
CodeLabel::plain(
|
||||
lsp_completion.label.clone(),
|
||||
lsp_completion.filter_text.as_deref(),
|
||||
)
|
||||
}),
|
||||
lsp_completion,
|
||||
})
|
||||
} else {
|
||||
log::info!("completion out of expected range");
|
||||
None
|
||||
}
|
||||
Some(Completion {
|
||||
old_range,
|
||||
new_text,
|
||||
label: language
|
||||
.as_ref()
|
||||
.and_then(|l| l.label_for_completion(&lsp_completion))
|
||||
.unwrap_or_else(|| {
|
||||
CodeLabel::plain(
|
||||
lsp_completion.label.clone(),
|
||||
lsp_completion.filter_text.as_deref(),
|
||||
)
|
||||
}),
|
||||
lsp_completion,
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
@@ -4889,8 +4910,8 @@ mod tests {
|
||||
use futures::{future, StreamExt};
|
||||
use gpui::test::subscribe;
|
||||
use language::{
|
||||
tree_sitter_rust, Diagnostic, FakeLspAdapter, LanguageConfig, OffsetRangeExt, Point,
|
||||
ToPoint,
|
||||
tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig,
|
||||
OffsetRangeExt, Point, ToPoint,
|
||||
};
|
||||
use lsp::Url;
|
||||
use serde_json::json;
|
||||
@@ -6507,6 +6528,75 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "TypeScript".into(),
|
||||
path_suffixes: vec!["ts".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_typescript::language_typescript()),
|
||||
);
|
||||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
"a.ts": "",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, cx);
|
||||
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
|
||||
|
||||
let (tree, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("/dir", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let worktree_id = tree.read_with(cx, |tree, _| tree.id());
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fake_server = fake_language_servers.next().await.unwrap();
|
||||
|
||||
let text = "let a = b.fqn";
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
|
||||
let completions = project.update(cx, |project, cx| {
|
||||
project.completions(&buffer, text.len(), cx)
|
||||
});
|
||||
|
||||
fake_server
|
||||
.handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
lsp::CompletionItem {
|
||||
label: "fullyQualifiedName?".into(),
|
||||
insert_text: Some("fullyQualifiedName".into()),
|
||||
..Default::default()
|
||||
},
|
||||
])))
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
let completions = completions.await.unwrap();
|
||||
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
assert_eq!(completions.len(), 1);
|
||||
assert_eq!(completions[0].new_text, "fullyQualifiedName");
|
||||
assert_eq!(
|
||||
completions[0].old_range.to_offset(&snapshot),
|
||||
text.len() - 3..text.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
|
||||
let mut language = Language::new(
|
||||
|
||||
@@ -43,6 +43,7 @@ pub struct Workspace {
|
||||
pub status_bar: StatusBar,
|
||||
pub toolbar: Toolbar,
|
||||
pub disconnected_overlay: ContainedText,
|
||||
pub modal: ContainerStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
@@ -54,8 +55,10 @@ pub struct Titlebar {
|
||||
pub avatar_width: f32,
|
||||
pub avatar_ribbon: AvatarRibbon,
|
||||
pub offline_icon: OfflineIcon,
|
||||
pub share_icon_color: Color,
|
||||
pub share_icon_active_color: Color,
|
||||
pub share_icon: ShareIcon,
|
||||
pub hovered_share_icon: ShareIcon,
|
||||
pub active_share_icon: ShareIcon,
|
||||
pub hovered_active_share_icon: ShareIcon,
|
||||
pub avatar: ImageStyle,
|
||||
pub sign_in_prompt: ContainedText,
|
||||
pub hovered_sign_in_prompt: ContainedText,
|
||||
@@ -78,6 +81,13 @@ pub struct OfflineIcon {
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct ShareIcon {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct Tab {
|
||||
pub height: f32,
|
||||
|
||||
@@ -36,7 +36,12 @@ impl ThemeSelector {
|
||||
let handle = cx.weak_handle();
|
||||
let picker = cx.add_view(|cx| Picker::new(handle, cx));
|
||||
let original_theme = cx.global::<Settings>().theme.clone();
|
||||
let theme_names = registry.list().collect::<Vec<_>>();
|
||||
let mut theme_names = registry.list().collect::<Vec<_>>();
|
||||
theme_names.sort_unstable_by(|a, b| {
|
||||
a.ends_with("dark")
|
||||
.cmp(&b.ends_with("dark"))
|
||||
.then_with(|| a.cmp(&b))
|
||||
});
|
||||
let matches = theme_names
|
||||
.iter()
|
||||
.map(|name| StringMatch {
|
||||
|
||||
@@ -28,7 +28,7 @@ mod test {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true, "").await;
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.simulate_keystroke("i");
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
cx.simulate_keystrokes(["T", "e", "s", "t"]);
|
||||
|
||||
+92
-52
@@ -4,7 +4,7 @@ use editor::{
|
||||
movement, Bias, DisplayPoint,
|
||||
};
|
||||
use gpui::{actions, impl_actions, MutableAppContext};
|
||||
use language::SelectionGoal;
|
||||
use language::{Selection, SelectionGoal};
|
||||
use serde::Deserialize;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -14,22 +14,15 @@ use crate::{
|
||||
Vim,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Motion {
|
||||
Left,
|
||||
Down,
|
||||
Up,
|
||||
Right,
|
||||
NextWordStart {
|
||||
ignore_punctuation: bool,
|
||||
stop_at_newline: bool,
|
||||
},
|
||||
NextWordEnd {
|
||||
ignore_punctuation: bool,
|
||||
},
|
||||
PreviousWordStart {
|
||||
ignore_punctuation: bool,
|
||||
},
|
||||
NextWordStart { ignore_punctuation: bool },
|
||||
NextWordEnd { ignore_punctuation: bool },
|
||||
PreviousWordStart { ignore_punctuation: bool },
|
||||
StartOfLine,
|
||||
EndOfLine,
|
||||
StartOfDocument,
|
||||
@@ -41,8 +34,6 @@ pub enum Motion {
|
||||
struct NextWordStart {
|
||||
#[serde(default)]
|
||||
ignore_punctuation: bool,
|
||||
#[serde(default)]
|
||||
stop_at_newline: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
@@ -87,19 +78,8 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| motion(Motion::EndOfDocument, cx));
|
||||
|
||||
cx.add_action(
|
||||
|_: &mut Workspace,
|
||||
&NextWordStart {
|
||||
ignore_punctuation,
|
||||
stop_at_newline,
|
||||
}: &NextWordStart,
|
||||
cx: _| {
|
||||
motion(
|
||||
Motion::NextWordStart {
|
||||
ignore_punctuation,
|
||||
stop_at_newline,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
|_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
|
||||
motion(Motion::NextWordStart { ignore_punctuation }, cx)
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
@@ -128,29 +108,48 @@ fn motion(motion: Motion, cx: &mut MutableAppContext) {
|
||||
}
|
||||
}
|
||||
|
||||
// Motion handling is specified here:
|
||||
// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
|
||||
impl Motion {
|
||||
pub fn linewise(self) -> bool {
|
||||
use Motion::*;
|
||||
match self {
|
||||
Down | Up | StartOfDocument | EndOfDocument => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inclusive(self) -> bool {
|
||||
use Motion::*;
|
||||
if self.linewise() {
|
||||
return true;
|
||||
}
|
||||
|
||||
match self {
|
||||
EndOfLine | NextWordEnd { .. } => true,
|
||||
Left | Right | StartOfLine | NextWordStart { .. } | PreviousWordStart { .. } => false,
|
||||
_ => panic!("Exclusivity not defined for {self:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_point(
|
||||
self,
|
||||
map: &DisplaySnapshot,
|
||||
point: DisplayPoint,
|
||||
goal: SelectionGoal,
|
||||
block_cursor_positioning: bool,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
use Motion::*;
|
||||
match self {
|
||||
Left => (left(map, point), SelectionGoal::None),
|
||||
Down => movement::down(map, point, goal),
|
||||
Up => movement::up(map, point, goal),
|
||||
Down => movement::down(map, point, goal, true),
|
||||
Up => movement::up(map, point, goal, true),
|
||||
Right => (right(map, point), SelectionGoal::None),
|
||||
NextWordStart {
|
||||
ignore_punctuation,
|
||||
stop_at_newline,
|
||||
} => (
|
||||
next_word_start(map, point, ignore_punctuation, stop_at_newline),
|
||||
NextWordStart { ignore_punctuation } => (
|
||||
next_word_start(map, point, ignore_punctuation),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
NextWordEnd { ignore_punctuation } => (
|
||||
next_word_end(map, point, ignore_punctuation, block_cursor_positioning),
|
||||
next_word_end(map, point, ignore_punctuation),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
PreviousWordStart { ignore_punctuation } => (
|
||||
@@ -164,11 +163,55 @@ impl Motion {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn line_wise(self) -> bool {
|
||||
use Motion::*;
|
||||
match self {
|
||||
Down | Up | StartOfDocument | EndOfDocument => true,
|
||||
_ => false,
|
||||
// Expands a selection using self motion for an operator
|
||||
pub fn expand_selection(
|
||||
self,
|
||||
map: &DisplaySnapshot,
|
||||
selection: &mut Selection<DisplayPoint>,
|
||||
expand_to_surrounding_newline: bool,
|
||||
) {
|
||||
let (head, goal) = self.move_point(map, selection.head(), selection.goal);
|
||||
selection.set_head(head, goal);
|
||||
|
||||
if self.linewise() {
|
||||
selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
|
||||
|
||||
if expand_to_surrounding_newline {
|
||||
if selection.end.row() < map.max_point().row() {
|
||||
*selection.end.row_mut() += 1;
|
||||
*selection.end.column_mut() = 0;
|
||||
// Don't reset the end here
|
||||
return;
|
||||
} else if selection.start.row() > 0 {
|
||||
*selection.start.row_mut() -= 1;
|
||||
*selection.start.column_mut() = map.line_len(selection.start.row());
|
||||
}
|
||||
}
|
||||
|
||||
selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
|
||||
} else {
|
||||
// If the motion is exclusive and the end of the motion is in column 1, the
|
||||
// end of the motion is moved to the end of the previous line and the motion
|
||||
// becomes inclusive. Example: "}" moves to the first line after a paragraph,
|
||||
// but "d}" will not include that line.
|
||||
let mut inclusive = self.inclusive();
|
||||
if !inclusive
|
||||
&& selection.end.row() > selection.start.row()
|
||||
&& selection.end.column() == 0
|
||||
&& selection.end.row() > 0
|
||||
{
|
||||
inclusive = true;
|
||||
*selection.end.row_mut() -= 1;
|
||||
*selection.end.column_mut() = 0;
|
||||
selection.end = map.clip_point(
|
||||
map.next_line_boundary(selection.end.to_point(map)).1,
|
||||
Bias::Left,
|
||||
);
|
||||
}
|
||||
|
||||
if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
|
||||
*selection.end.column_mut() += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,7 +230,6 @@ fn next_word_start(
|
||||
map: &DisplaySnapshot,
|
||||
point: DisplayPoint,
|
||||
ignore_punctuation: bool,
|
||||
stop_at_newline: bool,
|
||||
) -> DisplayPoint {
|
||||
let mut crossed_newline = false;
|
||||
movement::find_boundary(map, point, |left, right| {
|
||||
@@ -196,8 +238,8 @@ fn next_word_start(
|
||||
let at_newline = right == '\n';
|
||||
|
||||
let found = (left_kind != right_kind && !right.is_whitespace())
|
||||
|| (at_newline && (crossed_newline || stop_at_newline))
|
||||
|| (at_newline && left == '\n'); // Prevents skipping repeated empty lines
|
||||
|| at_newline && crossed_newline
|
||||
|| at_newline && left == '\n'; // Prevents skipping repeated empty lines
|
||||
|
||||
if at_newline {
|
||||
crossed_newline = true;
|
||||
@@ -210,7 +252,6 @@ fn next_word_end(
|
||||
map: &DisplaySnapshot,
|
||||
mut point: DisplayPoint,
|
||||
ignore_punctuation: bool,
|
||||
before_end_character: bool,
|
||||
) -> DisplayPoint {
|
||||
*point.column_mut() += 1;
|
||||
point = movement::find_boundary(map, point, |left, right| {
|
||||
@@ -221,13 +262,12 @@ fn next_word_end(
|
||||
});
|
||||
// find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
|
||||
// we have backtraced already
|
||||
if before_end_character
|
||||
&& !map
|
||||
.chars_at(point)
|
||||
.skip(1)
|
||||
.next()
|
||||
.map(|c| c == '\n')
|
||||
.unwrap_or(true)
|
||||
if !map
|
||||
.chars_at(point)
|
||||
.skip(1)
|
||||
.next()
|
||||
.map(|c| c == '\n')
|
||||
.unwrap_or(true)
|
||||
{
|
||||
*point.column_mut() = point.column().saturating_sub(1);
|
||||
}
|
||||
|
||||
+287
-641
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,436 @@
|
||||
use crate::{motion::Motion, state::Mode, Vim};
|
||||
use editor::{char_kind, movement};
|
||||
use gpui::{impl_actions, MutableAppContext, ViewContext};
|
||||
use serde::Deserialize;
|
||||
use workspace::Workspace;
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ChangeWord {
|
||||
#[serde(default)]
|
||||
ignore_punctuation: bool,
|
||||
}
|
||||
|
||||
impl_actions!(vim, [ChangeWord]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(change_word);
|
||||
}
|
||||
|
||||
pub fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.move_selections(cx, |map, selection| {
|
||||
motion.expand_selection(map, selection, false);
|
||||
});
|
||||
editor.insert(&"", cx);
|
||||
});
|
||||
});
|
||||
vim.switch_mode(Mode::Insert, cx)
|
||||
}
|
||||
|
||||
// From the docs https://vimhelp.org/change.txt.html#cw
|
||||
// Special case: When the cursor is in a word, "cw" and "cW" do not include the
|
||||
// white space after a word, they only change up to the end of the word. This is
|
||||
// because Vim interprets "cw" as change-word, and a word does not include the
|
||||
// following white space.
|
||||
fn change_word(
|
||||
_: &mut Workspace,
|
||||
&ChangeWord { ignore_punctuation }: &ChangeWord,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.move_selections(cx, |map, selection| {
|
||||
if selection.end.column() == map.line_len(selection.end.row()) {
|
||||
return;
|
||||
}
|
||||
|
||||
selection.end = movement::find_boundary(map, selection.end, |left, right| {
|
||||
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
||||
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
||||
|
||||
left_kind != right_kind || left == '\n' || right == '\n'
|
||||
});
|
||||
});
|
||||
editor.insert(&"", cx);
|
||||
});
|
||||
});
|
||||
vim.switch_mode(Mode::Insert, cx);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::{state::Mode, vim_test_context::VimTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_h(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["c", "h"]).mode_after(Mode::Insert);
|
||||
cx.assert("Te|st", "T|st");
|
||||
cx.assert("T|est", "|est");
|
||||
cx.assert("|Test", "|Test");
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
Test
|
||||
|test"},
|
||||
indoc! {"
|
||||
Test
|
||||
|test"},
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_l(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["c", "l"]).mode_after(Mode::Insert);
|
||||
cx.assert("Te|st", "Te|t");
|
||||
cx.assert("Tes|t", "Tes|");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_w(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["c", "w"]).mode_after(Mode::Insert);
|
||||
cx.assert("Te|st", "Te|");
|
||||
cx.assert("T|est test", "T| test");
|
||||
cx.assert("Test| test", "Test|test");
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
Test te|st
|
||||
test"},
|
||||
indoc! {"
|
||||
Test te|
|
||||
test"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
Test tes|t
|
||||
test"},
|
||||
indoc! {"
|
||||
Test tes|
|
||||
test"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
Test test
|
||||
|
|
||||
test"},
|
||||
indoc! {"
|
||||
Test test
|
||||
|
|
||||
test"},
|
||||
);
|
||||
|
||||
let mut cx = cx.binding(["c", "shift-W"]);
|
||||
cx.assert("Test te|st-test test", "Test te| test");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_e(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["c", "e"]).mode_after(Mode::Insert);
|
||||
cx.assert("Te|st Test", "Te| Test");
|
||||
cx.assert("T|est test", "T| test");
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
Test te|st
|
||||
test"},
|
||||
indoc! {"
|
||||
Test te|
|
||||
test"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
Test tes|t
|
||||
test"},
|
||||
"Test tes|",
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
Test test
|
||||
|
|
||||
test"},
|
||||
indoc! {"
|
||||
Test test
|
||||
|
|
||||
test"},
|
||||
);
|
||||
|
||||
let mut cx = cx.binding(["c", "shift-E"]);
|
||||
cx.assert("Test te|st-test test", "Test te| test");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_b(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["c", "b"]).mode_after(Mode::Insert);
|
||||
cx.assert("Te|st Test", "|st Test");
|
||||
cx.assert("Test |test", "|test");
|
||||
cx.assert("Test1 test2 |test3", "Test1 |test3");
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
Test test
|
||||
|test"},
|
||||
indoc! {"
|
||||
Test |
|
||||
test"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
Test test
|
||||
|
|
||||
test"},
|
||||
indoc! {"
|
||||
Test |
|
||||
|
||||
test"},
|
||||
);
|
||||
|
||||
let mut cx = cx.binding(["c", "shift-B"]);
|
||||
cx.assert("Test test-test |test", "Test |test");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_end_of_line(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["c", "shift-$"]).mode_after(Mode::Insert);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The q|uick
|
||||
brown fox"},
|
||||
indoc! {"
|
||||
The q|
|
||||
brown fox"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
|
|
||||
brown fox"},
|
||||
indoc! {"
|
||||
The quick
|
||||
|
|
||||
brown fox"},
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_0(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["c", "0"]).mode_after(Mode::Insert);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The q|uick
|
||||
brown fox"},
|
||||
indoc! {"
|
||||
|uick
|
||||
brown fox"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
|
|
||||
brown fox"},
|
||||
indoc! {"
|
||||
The quick
|
||||
|
|
||||
brown fox"},
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_k(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["c", "k"]).mode_after(Mode::Insert);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown |fox
|
||||
jumps over"},
|
||||
indoc! {"
|
||||
|
|
||||
jumps over"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps |over"},
|
||||
indoc! {"
|
||||
The quick
|
||||
|"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The q|uick
|
||||
brown fox
|
||||
jumps over"},
|
||||
indoc! {"
|
||||
|
|
||||
brown fox
|
||||
jumps over"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
|
|
||||
brown fox
|
||||
jumps over"},
|
||||
indoc! {"
|
||||
|
|
||||
brown fox
|
||||
jumps over"},
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_j(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["c", "j"]).mode_after(Mode::Insert);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown |fox
|
||||
jumps over"},
|
||||
indoc! {"
|
||||
The quick
|
||||
|"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps |over"},
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
|"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The q|uick
|
||||
brown fox
|
||||
jumps over"},
|
||||
indoc! {"
|
||||
|
|
||||
jumps over"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
|"},
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
|"},
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["c", "shift-G"]).mode_after(Mode::Insert);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown| fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
indoc! {"
|
||||
The quick
|
||||
|"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown| fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
indoc! {"
|
||||
The quick
|
||||
|"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
the l|azy"},
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
|"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
|"},
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
|"},
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_gg(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["c", "g", "g"]).mode_after(Mode::Insert);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown| fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
indoc! {"
|
||||
|
|
||||
jumps over
|
||||
the lazy"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
the l|azy"},
|
||||
"|",
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The q|uick
|
||||
brown fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
indoc! {"
|
||||
|
|
||||
brown fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
|
|
||||
brown fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
indoc! {"
|
||||
|
|
||||
brown fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
use crate::{motion::Motion, Vim};
|
||||
use editor::Bias;
|
||||
use gpui::MutableAppContext;
|
||||
use language::SelectionGoal;
|
||||
|
||||
pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.move_selections(cx, |map, selection| {
|
||||
let original_head = selection.head();
|
||||
motion.expand_selection(map, selection, true);
|
||||
selection.goal = SelectionGoal::Column(original_head.column());
|
||||
});
|
||||
editor.insert(&"", cx);
|
||||
|
||||
// Fixup cursor position after the deletion
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
editor.move_cursors(cx, |map, mut cursor, goal| {
|
||||
if motion.linewise() {
|
||||
if let SelectionGoal::Column(column) = goal {
|
||||
*cursor.column_mut() = column
|
||||
}
|
||||
}
|
||||
|
||||
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::vim_test_context::VimTestContext;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_h(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["d", "h"]);
|
||||
cx.assert("Te|st", "T|st");
|
||||
cx.assert("T|est", "|est");
|
||||
cx.assert("|Test", "|Test");
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
Test
|
||||
|test"},
|
||||
indoc! {"
|
||||
Test
|
||||
|test"},
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_l(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["d", "l"]);
|
||||
cx.assert("|Test", "|est");
|
||||
cx.assert("Te|st", "Te|t");
|
||||
cx.assert("Tes|t", "Te|s");
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
Tes|t
|
||||
test"},
|
||||
indoc! {"
|
||||
Te|s
|
||||
test"},
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_w(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["d", "w"]);
|
||||
cx.assert("Te|st", "T|e");
|
||||
cx.assert("T|est test", "T|test");
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
Test te|st
|
||||
test"},
|
||||
indoc! {"
|
||||
Test t|e
|
||||
test"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
Test tes|t
|
||||
test"},
|
||||
indoc! {"
|
||||
Test te|s
|
||||
test"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
Test test
|
||||
|
|
||||
test"},
|
||||
indoc! {"
|
||||
Test test
|
||||
|
|
||||
test"},
|
||||
);
|
||||
|
||||
let mut cx = cx.binding(["d", "shift-W"]);
|
||||
cx.assert("Test te|st-test test", "Test te|test");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_e(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["d", "e"]);
|
||||
cx.assert("Te|st Test", "Te| Test");
|
||||
cx.assert("T|est test", "T| test");
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
Test te|st
|
||||
test"},
|
||||
indoc! {"
|
||||
Test t|e
|
||||
test"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
Test tes|t
|
||||
test"},
|
||||
"Test te|s",
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
Test test
|
||||
|
|
||||
test"},
|
||||
indoc! {"
|
||||
Test test
|
||||
|
|
||||
test"},
|
||||
);
|
||||
|
||||
let mut cx = cx.binding(["d", "shift-E"]);
|
||||
cx.assert("Test te|st-test test", "Test te| test");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_b(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["d", "b"]);
|
||||
cx.assert("Te|st Test", "|st Test");
|
||||
cx.assert("Test |test", "|test");
|
||||
cx.assert("Test1 test2 |test3", "Test1 |test3");
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
Test test
|
||||
|test"},
|
||||
// Trailing whitespace after cursor
|
||||
indoc! {"
|
||||
Test|
|
||||
test"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
Test test
|
||||
|
|
||||
test"},
|
||||
// Trailing whitespace after cursor
|
||||
indoc! {"
|
||||
Test|
|
||||
|
||||
test"},
|
||||
);
|
||||
|
||||
let mut cx = cx.binding(["d", "shift-B"]);
|
||||
cx.assert("Test test-test |test", "Test |test");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_end_of_line(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["d", "shift-$"]);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The q|uick
|
||||
brown fox"},
|
||||
indoc! {"
|
||||
The |q
|
||||
brown fox"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
|
|
||||
brown fox"},
|
||||
indoc! {"
|
||||
The quick
|
||||
|
|
||||
brown fox"},
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_0(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["d", "0"]);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The q|uick
|
||||
brown fox"},
|
||||
indoc! {"
|
||||
|uick
|
||||
brown fox"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
|
|
||||
brown fox"},
|
||||
indoc! {"
|
||||
The quick
|
||||
|
|
||||
brown fox"},
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_k(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["d", "k"]);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown |fox
|
||||
jumps over"},
|
||||
"jumps |over",
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps |over"},
|
||||
"The qu|ick",
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The q|uick
|
||||
brown fox
|
||||
jumps over"},
|
||||
indoc! {"
|
||||
brown| fox
|
||||
jumps over"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
|brown fox
|
||||
jumps over"},
|
||||
"|jumps over",
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_j(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["d", "j"]);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown |fox
|
||||
jumps over"},
|
||||
"The qu|ick",
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps |over"},
|
||||
indoc! {"
|
||||
The quick
|
||||
brown |fox"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The q|uick
|
||||
brown fox
|
||||
jumps over"},
|
||||
"jumps| over",
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
|"},
|
||||
indoc! {"
|
||||
The quick
|
||||
|brown fox"},
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["d", "shift-G"]);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown| fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
"The q|uick",
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown| fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
"The q|uick",
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
the l|azy"},
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps| over"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
|"},
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
|jumps over"},
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_gg(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["d", "g", "g"]);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown| fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
indoc! {"
|
||||
jumps| over
|
||||
the lazy"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
the l|azy"},
|
||||
"|",
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The q|uick
|
||||
brown fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
indoc! {"
|
||||
brown| fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
|
|
||||
brown fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
indoc! {"
|
||||
|brown fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
#[cfg(test)]
|
||||
mod vim_test_context;
|
||||
|
||||
mod editor_events;
|
||||
mod insert;
|
||||
mod motion;
|
||||
mod normal;
|
||||
mod state;
|
||||
#[cfg(test)]
|
||||
mod vim_test_context;
|
||||
|
||||
use collections::HashMap;
|
||||
use editor::{CursorShape, Editor};
|
||||
@@ -25,6 +26,7 @@ impl_actions!(vim, [SwitchMode, PushOperator]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
editor_events::init(cx);
|
||||
normal::init(cx);
|
||||
insert::init(cx);
|
||||
motion::init(cx);
|
||||
|
||||
@@ -142,14 +144,14 @@ mod test {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, false, "").await;
|
||||
let mut cx = VimTestContext::new(cx, false).await;
|
||||
cx.simulate_keystrokes(["h", "j", "k", "l"]);
|
||||
cx.assert_editor_state("hjkl|");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true, "").await;
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.simulate_keystroke("i");
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
|
||||
@@ -15,11 +15,7 @@ pub struct VimTestContext<'a> {
|
||||
}
|
||||
|
||||
impl<'a> VimTestContext<'a> {
|
||||
pub async fn new(
|
||||
cx: &'a mut gpui::TestAppContext,
|
||||
enabled: bool,
|
||||
initial_editor_text: &str,
|
||||
) -> VimTestContext<'a> {
|
||||
pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> {
|
||||
cx.update(|cx| {
|
||||
editor::init(cx);
|
||||
crate::init(cx);
|
||||
@@ -38,10 +34,7 @@ impl<'a> VimTestContext<'a> {
|
||||
params
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/root",
|
||||
json!({ "dir": { "test.txt": initial_editor_text } }),
|
||||
)
|
||||
.insert_tree("/root", json!({ "dir": { "test.txt": "" } }))
|
||||
.await;
|
||||
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
|
||||
@@ -202,6 +195,14 @@ impl<'a> VimTestContext<'a> {
|
||||
assert_eq!(self.mode(), mode_after);
|
||||
assert_eq!(self.active_operator(), None);
|
||||
}
|
||||
|
||||
pub fn binding<const COUNT: usize>(
|
||||
mut self,
|
||||
keystrokes: [&'static str; COUNT],
|
||||
) -> VimBindingTestContext<'a, COUNT> {
|
||||
let mode = self.mode();
|
||||
VimBindingTestContext::new(keystrokes, mode, mode, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for VimTestContext<'a> {
|
||||
@@ -211,3 +212,61 @@ impl<'a> Deref for VimTestContext<'a> {
|
||||
self.cx
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VimBindingTestContext<'a, const COUNT: usize> {
|
||||
cx: VimTestContext<'a>,
|
||||
keystrokes_under_test: [&'static str; COUNT],
|
||||
initial_mode: Mode,
|
||||
mode_after: Mode,
|
||||
}
|
||||
|
||||
impl<'a, const COUNT: usize> VimBindingTestContext<'a, COUNT> {
|
||||
pub fn new(
|
||||
keystrokes_under_test: [&'static str; COUNT],
|
||||
initial_mode: Mode,
|
||||
mode_after: Mode,
|
||||
cx: VimTestContext<'a>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cx,
|
||||
keystrokes_under_test,
|
||||
initial_mode,
|
||||
mode_after,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn binding<const NEW_COUNT: usize>(
|
||||
self,
|
||||
keystrokes_under_test: [&'static str; NEW_COUNT],
|
||||
) -> VimBindingTestContext<'a, NEW_COUNT> {
|
||||
VimBindingTestContext {
|
||||
keystrokes_under_test,
|
||||
cx: self.cx,
|
||||
initial_mode: self.initial_mode,
|
||||
mode_after: self.mode_after,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mode_after(mut self, mode_after: Mode) -> Self {
|
||||
self.mode_after = mode_after;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn assert(&mut self, initial_state: &str, state_after: &str) {
|
||||
self.cx.assert_binding(
|
||||
self.keystrokes_under_test,
|
||||
initial_state,
|
||||
self.initial_mode,
|
||||
state_after,
|
||||
self.mode_after,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, const COUNT: usize> Deref for VimBindingTestContext<'a, COUNT> {
|
||||
type Target = VimTestContext<'a>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.cx
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1297,7 +1297,7 @@ impl Workspace {
|
||||
if project.is_local() {
|
||||
if project.is_shared() {
|
||||
project.unshare(cx);
|
||||
} else {
|
||||
} else if project.can_share(cx) {
|
||||
project.share(cx).detach();
|
||||
}
|
||||
}
|
||||
@@ -1475,28 +1475,38 @@ impl Workspace {
|
||||
}
|
||||
|
||||
fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let mut worktree_root_names = String::new();
|
||||
{
|
||||
let mut worktrees = self.project.read(cx).visible_worktrees(cx).peekable();
|
||||
while let Some(worktree) = worktrees.next() {
|
||||
worktree_root_names.push_str(worktree.read(cx).root_name());
|
||||
if worktrees.peek().is_some() {
|
||||
worktree_root_names.push_str(", ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ConstrainedBox::new(
|
||||
Container::new(
|
||||
Stack::new()
|
||||
.with_child(
|
||||
Align::new(
|
||||
Label::new("zed".into(), theme.workspace.titlebar.title.clone())
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
Label::new(worktree_root_names, theme.workspace.titlebar.title.clone())
|
||||
.aligned()
|
||||
.left()
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Align::new(
|
||||
Flex::row()
|
||||
.with_children(self.render_share_icon(theme, cx))
|
||||
.with_children(self.render_collaborators(theme, cx))
|
||||
.with_child(self.render_current_user(
|
||||
.with_children(self.render_current_user(
|
||||
self.user_store.read(cx).current_user().as_ref(),
|
||||
self.project.read(cx).replica_id(),
|
||||
theme,
|
||||
cx,
|
||||
))
|
||||
.with_children(self.render_connection_status(cx))
|
||||
.with_children(self.render_share_icon(theme, cx))
|
||||
.boxed(),
|
||||
)
|
||||
.right()
|
||||
@@ -1540,25 +1550,30 @@ impl Workspace {
|
||||
replica_id: ReplicaId,
|
||||
theme: &Theme,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
) -> Option<ElementBox> {
|
||||
let status = *self.client.status().borrow();
|
||||
if let Some(avatar) = user.and_then(|user| user.avatar.clone()) {
|
||||
self.render_avatar(avatar, replica_id, None, theme, cx)
|
||||
Some(self.render_avatar(avatar, replica_id, None, theme, cx))
|
||||
} else if matches!(status, client::Status::UpgradeRequired) {
|
||||
None
|
||||
} else {
|
||||
MouseEventHandler::new::<Authenticate, _, _>(0, cx, |state, _| {
|
||||
let style = if state.hovered {
|
||||
&theme.workspace.titlebar.hovered_sign_in_prompt
|
||||
} else {
|
||||
&theme.workspace.titlebar.sign_in_prompt
|
||||
};
|
||||
Label::new("Sign in".to_string(), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.on_click(|cx| cx.dispatch_action(Authenticate))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.aligned()
|
||||
.boxed()
|
||||
Some(
|
||||
MouseEventHandler::new::<Authenticate, _, _>(0, cx, |state, _| {
|
||||
let style = if state.hovered {
|
||||
&theme.workspace.titlebar.hovered_sign_in_prompt
|
||||
} else {
|
||||
&theme.workspace.titlebar.sign_in_prompt
|
||||
};
|
||||
Label::new("Sign in".to_string(), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.on_click(|cx| cx.dispatch_action(Authenticate))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1598,6 +1613,8 @@ impl Workspace {
|
||||
)
|
||||
.constrained()
|
||||
.with_width(theme.workspace.right_sidebar.width)
|
||||
.contained()
|
||||
.with_margin_left(2.)
|
||||
.boxed();
|
||||
|
||||
if let Some(peer_id) = peer_id {
|
||||
@@ -1611,22 +1628,39 @@ impl Workspace {
|
||||
}
|
||||
|
||||
fn render_share_icon(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> Option<ElementBox> {
|
||||
if self.project().read(cx).is_local() && self.client.user_id().is_some() {
|
||||
let color = if self.project().read(cx).is_shared() {
|
||||
theme.workspace.titlebar.share_icon_active_color
|
||||
} else {
|
||||
theme.workspace.titlebar.share_icon_color
|
||||
};
|
||||
if self.project().read(cx).is_local()
|
||||
&& self.client.user_id().is_some()
|
||||
&& self.project().read(cx).can_share(cx)
|
||||
{
|
||||
Some(
|
||||
MouseEventHandler::new::<ToggleShare, _, _>(0, cx, |_, _| {
|
||||
Align::new(
|
||||
Svg::new("icons/broadcast-24.svg")
|
||||
.with_color(color)
|
||||
.constrained()
|
||||
.with_width(24.)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed()
|
||||
MouseEventHandler::new::<ToggleShare, _, _>(0, cx, |state, cx| {
|
||||
let style = if self.project().read(cx).is_shared() {
|
||||
if state.hovered {
|
||||
&theme.workspace.titlebar.hovered_active_share_icon
|
||||
} else {
|
||||
&theme.workspace.titlebar.active_share_icon
|
||||
}
|
||||
} else {
|
||||
if state.hovered {
|
||||
&theme.workspace.titlebar.hovered_share_icon
|
||||
} else {
|
||||
&theme.workspace.titlebar.share_icon
|
||||
}
|
||||
};
|
||||
Svg::new("icons/share.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_height(14.)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_width(24.)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(theme.workspace.right_sidebar.width)
|
||||
.aligned()
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(|cx| cx.dispatch_action(ToggleShare))
|
||||
@@ -1983,7 +2017,14 @@ impl View for Workspace {
|
||||
content.add_child(self.right_sidebar.render(&theme, cx));
|
||||
content.boxed()
|
||||
})
|
||||
.with_children(self.modal.as_ref().map(|m| ChildView::new(m).boxed()))
|
||||
.with_children(self.modal.as_ref().map(|m| {
|
||||
ChildView::new(m)
|
||||
.contained()
|
||||
.with_style(theme.workspace.modal)
|
||||
.aligned()
|
||||
.top()
|
||||
.boxed()
|
||||
}))
|
||||
.flex(1.0, true)
|
||||
.boxed(),
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.29.0"
|
||||
version = "0.30.0"
|
||||
|
||||
[lib]
|
||||
name = "zed"
|
||||
|
||||
@@ -63,7 +63,7 @@ pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegi
|
||||
languages
|
||||
}
|
||||
|
||||
fn language(
|
||||
pub(crate) fn language(
|
||||
name: &str,
|
||||
grammar: tree_sitter::Language,
|
||||
lsp_adapter: Option<Arc<dyn LspAdapter>>,
|
||||
|
||||
@@ -56,7 +56,16 @@
|
||||
[
|
||||
"."
|
||||
";"
|
||||
] @delimiter
|
||||
] @punctuation.delimiter
|
||||
|
||||
[
|
||||
"{"
|
||||
"}"
|
||||
"("
|
||||
")"
|
||||
"["
|
||||
"]"
|
||||
] @punctuation.bracket
|
||||
|
||||
[
|
||||
(string_literal)
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"?."
|
||||
"."
|
||||
","
|
||||
":"
|
||||
] @punctuation.delimiter
|
||||
|
||||
[
|
||||
|
||||
@@ -11,4 +11,11 @@
|
||||
(true)
|
||||
(false)
|
||||
(null)
|
||||
] @constant
|
||||
] @constant
|
||||
|
||||
[
|
||||
"{"
|
||||
"}"
|
||||
"["
|
||||
"]"
|
||||
] @punctuation.bracket
|
||||
@@ -12,7 +12,7 @@
|
||||
(list_marker_star)
|
||||
(list_marker_dot)
|
||||
(list_marker_parenthesis)
|
||||
] @list_marker
|
||||
] @punctuation.list_marker
|
||||
|
||||
[
|
||||
(indented_code_block)
|
||||
|
||||
@@ -34,6 +34,20 @@
|
||||
((identifier) @constant
|
||||
(#match? @constant "^[A-Z][A-Z\\d_]+$"))
|
||||
|
||||
[
|
||||
"("
|
||||
")"
|
||||
"{"
|
||||
"}"
|
||||
"["
|
||||
"]"
|
||||
] @punctuation.bracket
|
||||
|
||||
(_
|
||||
.
|
||||
"<" @punctuation.bracket
|
||||
">" @punctuation.bracket)
|
||||
|
||||
[
|
||||
"as"
|
||||
"async"
|
||||
|
||||
@@ -20,14 +20,18 @@
|
||||
; Punctuation
|
||||
;------------
|
||||
|
||||
"." @punctuation.delimiter
|
||||
"," @punctuation.delimiter
|
||||
[
|
||||
"."
|
||||
","
|
||||
] @punctuation.delimiter
|
||||
|
||||
"=" @operator
|
||||
|
||||
"[" @punctuation.bracket
|
||||
"]" @punctuation.bracket
|
||||
"[[" @punctuation.bracket
|
||||
"]]" @punctuation.bracket
|
||||
"{" @punctuation.bracket
|
||||
"}" @punctuation.bracket
|
||||
[
|
||||
"["
|
||||
"]"
|
||||
"[["
|
||||
"]]"
|
||||
"{"
|
||||
"}"
|
||||
] @punctuation.bracket
|
||||
|
||||
@@ -144,3 +144,54 @@ impl LspAdapter for TypeScriptLspAdapter {
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::MutableAppContext;
|
||||
use unindent::Unindent;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_outline(cx: &mut MutableAppContext) {
|
||||
let language = crate::languages::language(
|
||||
"typescript",
|
||||
tree_sitter_typescript::language_typescript(),
|
||||
None,
|
||||
);
|
||||
|
||||
let text = r#"
|
||||
function a() {
|
||||
// local variables are omitted
|
||||
let a1 = 1;
|
||||
// all functions are included
|
||||
async function a2() {}
|
||||
}
|
||||
// top-level variables are included
|
||||
let b: C
|
||||
function getB() {}
|
||||
// exported variables are included
|
||||
export const d = e;
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let buffer = cx.add_model(|cx| {
|
||||
language::Buffer::new(0, text, cx).with_language(Arc::new(language), cx)
|
||||
});
|
||||
let outline = buffer.read(cx).snapshot().outline(None).unwrap();
|
||||
assert_eq!(
|
||||
outline
|
||||
.items
|
||||
.iter()
|
||||
.map(|item| (item.text.as_str(), item.depth))
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
("function a ( )", 0),
|
||||
("async function a2 ( )", 1),
|
||||
("let b", 0),
|
||||
("function getB ( )", 0),
|
||||
("const d", 0),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"?."
|
||||
"."
|
||||
","
|
||||
":"
|
||||
] @punctuation.delimiter
|
||||
|
||||
[
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
"enum" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(type_alias_declaration
|
||||
"type" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(function_declaration
|
||||
"async"? @context
|
||||
"function" @context
|
||||
@@ -18,6 +22,12 @@
|
||||
"interface" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(export_statement
|
||||
(lexical_declaration
|
||||
["let" "const"] @context
|
||||
(variable_declarator
|
||||
name: (_) @name) @item))
|
||||
|
||||
(program
|
||||
(lexical_declaration
|
||||
["let" "const"] @context
|
||||
|
||||
+10
-2
@@ -169,11 +169,19 @@ fn main() {
|
||||
.detach();
|
||||
|
||||
languages.set_language_server_download_dir(zed::ROOT_PATH.clone());
|
||||
languages.set_theme(&settings.theme.editor.syntax);
|
||||
let languages = Arc::new(languages);
|
||||
|
||||
cx.observe_global::<Settings, _>({
|
||||
let languages = languages.clone();
|
||||
move |settings, _| {
|
||||
languages.set_theme(&settings.theme.editor.syntax);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
cx.set_global(settings);
|
||||
|
||||
let app_state = Arc::new(AppState {
|
||||
languages: Arc::new(languages),
|
||||
languages,
|
||||
themes,
|
||||
channel_list,
|
||||
client,
|
||||
|
||||
Reference in New Issue
Block a user