Merge branch 'main' into settings-file

This commit is contained in:
Max Brunsfeld 2022-03-11 10:04:17 -08:00
commit 6091caee8e
25 changed files with 858 additions and 227 deletions

1
Cargo.lock generated
View file

@ -5853,6 +5853,7 @@ dependencies = [
"project", "project",
"serde", "serde",
"serde_json", "serde_json",
"smallvec",
"theme", "theme",
"util", "util",
] ]

View file

@ -631,6 +631,9 @@ impl Client {
} else { } else {
log::info!("unhandled message {}", type_name); log::info!("unhandled message {}", type_name);
} }
// Don't starve the main thread when receiving lots of messages at once.
smol::future::yield_now().await;
} }
} }
}) })

View file

@ -6,7 +6,7 @@ mod wrap_map;
use crate::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint}; use crate::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
use block_map::{BlockMap, BlockPoint}; use block_map::{BlockMap, BlockPoint};
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use fold_map::{FoldMap, ToFoldPoint as _}; use fold_map::FoldMap;
use gpui::{fonts::FontId, Entity, ModelContext, ModelHandle}; use gpui::{fonts::FontId, Entity, ModelContext, ModelHandle};
use language::{Point, Subscription as BufferSubscription}; use language::{Point, Subscription as BufferSubscription};
use std::ops::Range; use std::ops::Range;
@ -200,7 +200,7 @@ impl DisplaySnapshot {
pub fn prev_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) { pub fn prev_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
loop { loop {
let mut fold_point = point.to_fold_point(&self.folds_snapshot, Bias::Left); let mut fold_point = self.folds_snapshot.to_fold_point(point, Bias::Left);
*fold_point.column_mut() = 0; *fold_point.column_mut() = 0;
point = fold_point.to_buffer_point(&self.folds_snapshot); point = fold_point.to_buffer_point(&self.folds_snapshot);
@ -216,7 +216,7 @@ impl DisplaySnapshot {
pub fn next_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) { pub fn next_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
loop { loop {
let mut fold_point = point.to_fold_point(&self.folds_snapshot, Bias::Right); let mut fold_point = self.folds_snapshot.to_fold_point(point, Bias::Right);
*fold_point.column_mut() = self.folds_snapshot.line_len(fold_point.row()); *fold_point.column_mut() = self.folds_snapshot.line_len(fold_point.row());
point = fold_point.to_buffer_point(&self.folds_snapshot); point = fold_point.to_buffer_point(&self.folds_snapshot);
@ -231,7 +231,7 @@ impl DisplaySnapshot {
} }
fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint { fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint {
let fold_point = point.to_fold_point(&self.folds_snapshot, bias); let fold_point = self.folds_snapshot.to_fold_point(point, bias);
let tab_point = self.tabs_snapshot.to_tab_point(fold_point); let tab_point = self.tabs_snapshot.to_tab_point(fold_point);
let wrap_point = self.wraps_snapshot.from_tab_point(tab_point); let wrap_point = self.wraps_snapshot.from_tab_point(tab_point);
let block_point = self.blocks_snapshot.to_block_point(wrap_point); let block_point = self.blocks_snapshot.to_block_point(wrap_point);

View file

@ -12,10 +12,6 @@ use std::{
}; };
use sum_tree::{Bias, Cursor, FilterCursor, SumTree}; use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
pub trait ToFoldPoint {
fn to_fold_point(&self, snapshot: &FoldSnapshot, bias: Bias) -> FoldPoint;
}
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct FoldPoint(pub super::Point); pub struct FoldPoint(pub super::Point);
@ -75,26 +71,6 @@ impl FoldPoint {
} }
} }
impl ToFoldPoint for Point {
fn to_fold_point(&self, snapshot: &FoldSnapshot, bias: Bias) -> FoldPoint {
let mut cursor = snapshot.transforms.cursor::<(Point, FoldPoint)>();
cursor.seek(self, Bias::Right, &());
if cursor.item().map_or(false, |t| t.is_fold()) {
if bias == Bias::Left || *self == cursor.start().0 {
cursor.start().1
} else {
cursor.end(&()).1
}
} else {
let overshoot = *self - cursor.start().0;
FoldPoint(cmp::min(
cursor.start().1 .0 + overshoot,
cursor.end(&()).1 .0,
))
}
}
}
pub struct FoldMapWriter<'a>(&'a mut FoldMap); pub struct FoldMapWriter<'a>(&'a mut FoldMap);
impl<'a> FoldMapWriter<'a> { impl<'a> FoldMapWriter<'a> {
@ -554,6 +530,24 @@ impl FoldSnapshot {
summary summary
} }
pub fn to_fold_point(&self, point: Point, bias: Bias) -> FoldPoint {
let mut cursor = self.transforms.cursor::<(Point, FoldPoint)>();
cursor.seek(&point, Bias::Right, &());
if cursor.item().map_or(false, |t| t.is_fold()) {
if bias == Bias::Left || point == cursor.start().0 {
cursor.start().1
} else {
cursor.end(&()).1
}
} else {
let overshoot = point - cursor.start().0;
FoldPoint(cmp::min(
cursor.start().1 .0 + overshoot,
cursor.end(&()).1 .0,
))
}
}
pub fn len(&self) -> FoldOffset { pub fn len(&self) -> FoldOffset {
FoldOffset(self.transforms.summary().output.bytes) FoldOffset(self.transforms.summary().output.bytes)
} }
@ -1356,7 +1350,7 @@ mod tests {
let buffer_point = fold_point.to_buffer_point(&snapshot); let buffer_point = fold_point.to_buffer_point(&snapshot);
let buffer_offset = buffer_point.to_offset(&buffer_snapshot); let buffer_offset = buffer_point.to_offset(&buffer_snapshot);
assert_eq!( assert_eq!(
buffer_point.to_fold_point(&snapshot, Right), snapshot.to_fold_point(buffer_point, Right),
fold_point, fold_point,
"{:?} -> fold point", "{:?} -> fold point",
buffer_point, buffer_point,
@ -1428,10 +1422,8 @@ mod tests {
} }
for fold_range in map.merged_fold_ranges() { for fold_range in map.merged_fold_ranges() {
let fold_point = fold_range let fold_point =
.start snapshot.to_fold_point(fold_range.start.to_point(&buffer_snapshot), Right);
.to_point(&buffer_snapshot)
.to_fold_point(&snapshot, Right);
assert!(snapshot.is_line_folded(fold_point.row())); assert!(snapshot.is_line_folded(fold_point.row()));
} }

View file

@ -1,4 +1,4 @@
use super::fold_map::{self, FoldEdit, FoldPoint, FoldSnapshot, ToFoldPoint}; use super::fold_map::{self, FoldEdit, FoldPoint, FoldSnapshot};
use crate::MultiBufferSnapshot; use crate::MultiBufferSnapshot;
use language::{rope, Chunk}; use language::{rope, Chunk};
use parking_lot::Mutex; use parking_lot::Mutex;
@ -201,10 +201,6 @@ impl TabSnapshot {
TabPoint::new(input.row(), expanded as u32) TabPoint::new(input.row(), expanded as u32)
} }
pub fn from_point(&self, point: Point, bias: Bias) -> TabPoint {
self.to_tab_point(point.to_fold_point(&self.fold_snapshot, bias))
}
pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, usize, usize) { pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, usize, usize) {
let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0)); let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
let expanded = output.column() as usize; let expanded = output.column() as usize;
@ -217,6 +213,10 @@ impl TabSnapshot {
) )
} }
pub fn from_point(&self, point: Point, bias: Bias) -> TabPoint {
self.to_tab_point(self.fold_snapshot.to_fold_point(point, bias))
}
pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point { pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
self.to_fold_point(point, bias) self.to_fold_point(point, bias)
.0 .0

View file

@ -2638,11 +2638,26 @@ impl Editor {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
for selection in &mut selections { for selection in &mut selections {
if selection.is_empty() { if selection.is_empty() {
let head = selection.head().to_display_point(&display_map); let old_head = selection.head();
let cursor = movement::left(&display_map, head) let mut new_head =
.unwrap() movement::left(&display_map, old_head.to_display_point(&display_map))
.to_point(&display_map); .unwrap()
selection.set_head(cursor); .to_point(&display_map);
if let Some((buffer, line_buffer_range)) = display_map
.buffer_snapshot
.buffer_line_for_row(old_head.row)
{
let indent_column = buffer.indent_column_for_line(line_buffer_range.start.row);
if old_head.column <= indent_column && old_head.column > 0 {
let indent = buffer.indent_size();
new_head = cmp::min(
new_head,
Point::new(old_head.row, ((old_head.column - 1) / indent) * indent),
);
}
}
selection.set_head(new_head);
selection.goal = SelectionGoal::None; selection.goal = SelectionGoal::None;
} }
} }
@ -7153,14 +7168,13 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_backspace(cx: &mut gpui::MutableAppContext) { fn test_backspace(cx: &mut gpui::MutableAppContext) {
let buffer =
MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx);
let settings = Settings::test(&cx); let settings = Settings::test(&cx);
let (_, view) = cx.add_window(Default::default(), |cx| { let (_, view) = cx.add_window(Default::default(), |cx| {
build_editor(buffer.clone(), settings, cx) build_editor(MultiBuffer::build_simple("", cx), settings, cx)
}); });
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
view.set_text("one two three\nfour five six\nseven eight nine\nten\n", cx);
view.select_display_ranges( view.select_display_ranges(
&[ &[
// an empty selection - the preceding character is deleted // an empty selection - the preceding character is deleted
@ -7173,12 +7187,28 @@ mod tests {
cx, cx,
); );
view.backspace(&Backspace, cx); view.backspace(&Backspace, cx);
}); assert_eq!(view.text(cx), "oe two three\nfou five six\nseven ten\n");
assert_eq!( view.set_text(" one\n two\n three\n four", cx);
buffer.read(cx).read(cx).text(), view.select_display_ranges(
"oe two three\nfou five six\nseven ten\n" &[
); // cursors at the the end of leading indent - last indent is deleted
DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4),
DisplayPoint::new(1, 8)..DisplayPoint::new(1, 8),
// cursors inside leading indent - overlapping indent deletions are coalesced
DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4),
DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
DisplayPoint::new(2, 6)..DisplayPoint::new(2, 6),
// cursor at the beginning of a line - preceding newline is deleted
DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
// selection inside leading indent - only the selected character is deleted
DisplayPoint::new(3, 2)..DisplayPoint::new(3, 3),
],
cx,
);
view.backspace(&Backspace, cx);
assert_eq!(view.text(cx), "one\n two\n three four");
});
} }
#[gpui::test] #[gpui::test]

View file

@ -1657,7 +1657,7 @@ impl MultiBufferSnapshot {
} }
} }
fn buffer_line_for_row(&self, row: u32) -> Option<(&BufferSnapshot, Range<Point>)> { pub fn buffer_line_for_row(&self, row: u32) -> Option<(&BufferSnapshot, Range<Point>)> {
let mut cursor = self.excerpts.cursor::<Point>(); let mut cursor = self.excerpts.cursor::<Point>();
cursor.seek(&Point::new(row, 0), Bias::Right, &()); cursor.seek(&Point::new(row, 0), Bias::Right, &());
if let Some(excerpt) = cursor.item() { if let Some(excerpt) = cursor.item() {

View file

@ -3,13 +3,15 @@ use serde_json::json;
use crate::{ use crate::{
geometry::vector::Vector2F, DebugContext, Element, ElementBox, Event, EventContext, geometry::vector::Vector2F, DebugContext, Element, ElementBox, Event, EventContext,
LayoutContext, PaintContext, SizeConstraint, LayoutContext, NavigationDirection, PaintContext, SizeConstraint,
}; };
pub struct EventHandler { pub struct EventHandler {
child: ElementBox, child: ElementBox,
capture: Option<Box<dyn FnMut(&Event, RectF, &mut EventContext) -> bool>>, capture: Option<Box<dyn FnMut(&Event, RectF, &mut EventContext) -> bool>>,
mouse_down: Option<Box<dyn FnMut(&mut EventContext) -> bool>>, mouse_down: Option<Box<dyn FnMut(&mut EventContext) -> bool>>,
right_mouse_down: Option<Box<dyn FnMut(&mut EventContext) -> bool>>,
navigate_mouse_down: Option<Box<dyn FnMut(NavigationDirection, &mut EventContext) -> bool>>,
} }
impl EventHandler { impl EventHandler {
@ -18,6 +20,8 @@ impl EventHandler {
child, child,
capture: None, capture: None,
mouse_down: None, mouse_down: None,
right_mouse_down: None,
navigate_mouse_down: None,
} }
} }
@ -29,6 +33,22 @@ impl EventHandler {
self self
} }
pub fn on_right_mouse_down<F>(mut self, callback: F) -> Self
where
F: 'static + FnMut(&mut EventContext) -> bool,
{
self.right_mouse_down = Some(Box::new(callback));
self
}
pub fn on_navigate_mouse_down<F>(mut self, callback: F) -> Self
where
F: 'static + FnMut(NavigationDirection, &mut EventContext) -> bool,
{
self.navigate_mouse_down = Some(Box::new(callback));
self
}
pub fn capture<F>(mut self, callback: F) -> Self pub fn capture<F>(mut self, callback: F) -> Self
where where
F: 'static + FnMut(&Event, RectF, &mut EventContext) -> bool, F: 'static + FnMut(&Event, RectF, &mut EventContext) -> bool,
@ -87,6 +107,26 @@ impl Element for EventHandler {
} }
false false
} }
Event::RightMouseDown { position, .. } => {
if let Some(callback) = self.right_mouse_down.as_mut() {
if bounds.contains_point(*position) {
return callback(cx);
}
}
false
}
Event::NavigateMouseDown {
position,
direction,
..
} => {
if let Some(callback) = self.navigate_mouse_down.as_mut() {
if bounds.contains_point(*position) {
return callback(*direction, cx);
}
}
false
}
_ => false, _ => false,
} }
} }

View file

@ -29,7 +29,7 @@ pub mod keymap;
pub mod platform; pub mod platform;
pub use gpui_macros::test; pub use gpui_macros::test;
pub use platform::FontSystem; pub use platform::FontSystem;
pub use platform::{Event, PathPromptOptions, Platform, PromptLevel}; pub use platform::{Event, NavigationDirection, PathPromptOptions, Platform, PromptLevel};
pub use presenter::{ pub use presenter::{
Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt, Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt,
}; };

View file

@ -19,7 +19,7 @@ use crate::{
}; };
use anyhow::Result; use anyhow::Result;
use async_task::Runnable; use async_task::Runnable;
pub use event::Event; pub use event::{Event, NavigationDirection};
use postage::oneshot; use postage::oneshot;
use std::{ use std::{
any::Any, any::Any,

View file

@ -1,5 +1,11 @@
use crate::{geometry::vector::Vector2F, keymap::Keystroke}; use crate::{geometry::vector::Vector2F, keymap::Keystroke};
#[derive(Copy, Clone, Debug)]
pub enum NavigationDirection {
Back,
Forward,
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Event { pub enum Event {
KeyDown { KeyDown {
@ -26,6 +32,30 @@ pub enum Event {
LeftMouseDragged { LeftMouseDragged {
position: Vector2F, position: Vector2F,
}, },
RightMouseDown {
position: Vector2F,
ctrl: bool,
alt: bool,
shift: bool,
cmd: bool,
click_count: usize,
},
RightMouseUp {
position: Vector2F,
},
NavigateMouseDown {
position: Vector2F,
direction: NavigationDirection,
ctrl: bool,
alt: bool,
shift: bool,
cmd: bool,
click_count: usize,
},
NavigateMouseUp {
position: Vector2F,
direction: NavigationDirection,
},
MouseMoved { MouseMoved {
position: Vector2F, position: Vector2F,
left_mouse_down: bool, left_mouse_down: bool,

View file

@ -1,4 +1,8 @@
use crate::{geometry::vector::vec2f, keymap::Keystroke, platform::Event}; use crate::{
geometry::vector::vec2f,
keymap::Keystroke,
platform::{Event, NavigationDirection},
};
use cocoa::{ use cocoa::{
appkit::{NSEvent, NSEventModifierFlags, NSEventType}, appkit::{NSEvent, NSEventModifierFlags, NSEventType},
base::{id, nil, YES}, base::{id, nil, YES},
@ -125,6 +129,64 @@ impl Event {
window_height - native_event.locationInWindow().y as f32, window_height - native_event.locationInWindow().y as f32,
), ),
}), }),
NSEventType::NSRightMouseDown => {
let modifiers = native_event.modifierFlags();
window_height.map(|window_height| Self::RightMouseDown {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
click_count: native_event.clickCount() as usize,
})
}
NSEventType::NSRightMouseUp => window_height.map(|window_height| Self::RightMouseUp {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
}),
NSEventType::NSOtherMouseDown => {
let direction = match native_event.buttonNumber() {
3 => NavigationDirection::Back,
4 => NavigationDirection::Forward,
// Other mouse buttons aren't tracked currently
_ => return None,
};
let modifiers = native_event.modifierFlags();
window_height.map(|window_height| Self::NavigateMouseDown {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
direction,
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
click_count: native_event.clickCount() as usize,
})
}
NSEventType::NSOtherMouseUp => {
let direction = match native_event.buttonNumber() {
3 => NavigationDirection::Back,
4 => NavigationDirection::Forward,
// Other mouse buttons aren't tracked currently
_ => return None,
};
window_height.map(|window_height| Self::NavigateMouseUp {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
direction,
})
}
NSEventType::NSLeftMouseDragged => { NSEventType::NSLeftMouseDragged => {
window_height.map(|window_height| Self::LeftMouseDragged { window_height.map(|window_height| Self::LeftMouseDragged {
position: vec2f( position: vec2f(

View file

@ -95,6 +95,22 @@ unsafe fn build_classes() {
sel!(mouseUp:), sel!(mouseUp:),
handle_view_event as extern "C" fn(&Object, Sel, id), handle_view_event as extern "C" fn(&Object, Sel, id),
); );
decl.add_method(
sel!(rightMouseDown:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(rightMouseUp:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(otherMouseDown:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(otherMouseUp:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method( decl.add_method(
sel!(mouseMoved:), sel!(mouseMoved:),
handle_view_event as extern "C" fn(&Object, Sel, id), handle_view_event as extern "C" fn(&Object, Sel, id),

View file

@ -47,9 +47,6 @@ lazy_static! {
static ref QUERY_CURSORS: Mutex<Vec<QueryCursor>> = Default::default(); static ref QUERY_CURSORS: Mutex<Vec<QueryCursor>> = Default::default();
} }
// TODO - Make this configurable
const INDENT_SIZE: u32 = 4;
pub struct Buffer { pub struct Buffer {
text: TextBuffer, text: TextBuffer,
file: Option<Box<dyn File>>, file: Option<Box<dyn File>>,
@ -70,6 +67,7 @@ pub struct Buffer {
file_update_count: usize, file_update_count: usize,
completion_triggers: Vec<String>, completion_triggers: Vec<String>,
deferred_ops: OperationQueue<Operation>, deferred_ops: OperationQueue<Operation>,
indent_size: u32,
} }
pub struct BufferSnapshot { pub struct BufferSnapshot {
@ -81,9 +79,9 @@ pub struct BufferSnapshot {
file_update_count: usize, file_update_count: usize,
remote_selections: TreeMap<ReplicaId, SelectionSet>, remote_selections: TreeMap<ReplicaId, SelectionSet>,
selections_update_count: usize, selections_update_count: usize,
is_parsing: bool,
language: Option<Arc<Language>>, language: Option<Arc<Language>>,
parse_count: usize, parse_count: usize,
indent_size: u32,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -416,6 +414,8 @@ impl Buffer {
file_update_count: 0, file_update_count: 0,
completion_triggers: Default::default(), completion_triggers: Default::default(),
deferred_ops: OperationQueue::new(), deferred_ops: OperationQueue::new(),
// TODO: make this configurable
indent_size: 4,
} }
} }
@ -428,10 +428,10 @@ impl Buffer {
diagnostics: self.diagnostics.clone(), diagnostics: self.diagnostics.clone(),
diagnostics_update_count: self.diagnostics_update_count, diagnostics_update_count: self.diagnostics_update_count,
file_update_count: self.file_update_count, file_update_count: self.file_update_count,
is_parsing: self.parsing_in_background,
language: self.language.clone(), language: self.language.clone(),
parse_count: self.parse_count, parse_count: self.parse_count,
selections_update_count: self.selections_update_count, selections_update_count: self.selections_update_count,
indent_size: self.indent_size,
} }
} }
@ -768,7 +768,11 @@ impl Buffer {
.before_edit .before_edit
.indent_column_for_line(suggestion.basis_row) .indent_column_for_line(suggestion.basis_row)
}); });
let delta = if suggestion.indent { INDENT_SIZE } else { 0 }; let delta = if suggestion.indent {
snapshot.indent_size
} else {
0
};
old_suggestions.insert( old_suggestions.insert(
*old_to_new_rows.get(&old_row).unwrap(), *old_to_new_rows.get(&old_row).unwrap(),
indentation_basis + delta, indentation_basis + delta,
@ -787,7 +791,11 @@ impl Buffer {
.into_iter() .into_iter()
.flatten(); .flatten();
for (new_row, suggestion) in new_edited_row_range.zip(suggestions) { for (new_row, suggestion) in new_edited_row_range.zip(suggestions) {
let delta = if suggestion.indent { INDENT_SIZE } else { 0 }; let delta = if suggestion.indent {
snapshot.indent_size
} else {
0
};
let new_indentation = indent_columns let new_indentation = indent_columns
.get(&suggestion.basis_row) .get(&suggestion.basis_row)
.copied() .copied()
@ -819,7 +827,11 @@ impl Buffer {
.into_iter() .into_iter()
.flatten(); .flatten();
for (row, suggestion) in inserted_row_range.zip(suggestions) { for (row, suggestion) in inserted_row_range.zip(suggestions) {
let delta = if suggestion.indent { INDENT_SIZE } else { 0 }; let delta = if suggestion.indent {
snapshot.indent_size
} else {
0
};
let new_indentation = indent_columns let new_indentation = indent_columns
.get(&suggestion.basis_row) .get(&suggestion.basis_row)
.copied() .copied()
@ -1868,6 +1880,10 @@ impl BufferSnapshot {
pub fn file_update_count(&self) -> usize { pub fn file_update_count(&self) -> usize {
self.file_update_count self.file_update_count
} }
pub fn indent_size(&self) -> u32 {
self.indent_size
}
} }
impl Clone for BufferSnapshot { impl Clone for BufferSnapshot {
@ -1881,9 +1897,9 @@ impl Clone for BufferSnapshot {
selections_update_count: self.selections_update_count, selections_update_count: self.selections_update_count,
diagnostics_update_count: self.diagnostics_update_count, diagnostics_update_count: self.diagnostics_update_count,
file_update_count: self.file_update_count, file_update_count: self.file_update_count,
is_parsing: self.is_parsing,
language: self.language.clone(), language: self.language.clone(),
parse_count: self.parse_count, parse_count: self.parse_count,
indent_size: self.indent_size,
} }
} }
} }

View file

@ -35,6 +35,7 @@ type ResponseHandler = Box<dyn Send + FnOnce(Result<&str, Error>)>;
pub struct LanguageServer { pub struct LanguageServer {
next_id: AtomicUsize, next_id: AtomicUsize,
outbound_tx: channel::Sender<Vec<u8>>, outbound_tx: channel::Sender<Vec<u8>>,
name: String,
capabilities: ServerCapabilities, capabilities: ServerCapabilities,
notification_handlers: Arc<RwLock<HashMap<&'static str, NotificationHandler>>>, notification_handlers: Arc<RwLock<HashMap<&'static str, NotificationHandler>>>,
response_handlers: Arc<Mutex<HashMap<usize, ResponseHandler>>>, response_handlers: Arc<Mutex<HashMap<usize, ResponseHandler>>>,
@ -123,9 +124,11 @@ impl LanguageServer {
.spawn()?; .spawn()?;
let stdin = server.stdin.take().unwrap(); let stdin = server.stdin.take().unwrap();
let stdout = server.stdout.take().unwrap(); let stdout = server.stdout.take().unwrap();
Ok(Self::new_internal( let mut server = Self::new_internal(stdin, stdout, root_path, options, background);
stdin, stdout, root_path, options, background, if let Some(name) = binary_path.file_name() {
)) server.name = name.to_string_lossy().to_string();
}
Ok(server)
} }
fn new_internal<Stdin, Stdout>( fn new_internal<Stdin, Stdout>(
@ -227,6 +230,7 @@ impl LanguageServer {
Self { Self {
notification_handlers, notification_handlers,
response_handlers, response_handlers,
name: Default::default(),
capabilities: Default::default(), capabilities: Default::default(),
next_id: Default::default(), next_id: Default::default(),
outbound_tx, outbound_tx,
@ -297,7 +301,13 @@ impl LanguageServer {
}; };
let response = this.request::<request::Initialize>(params).await?; let response = this.request::<request::Initialize>(params).await?;
Arc::get_mut(&mut this).unwrap().capabilities = response.capabilities; {
let this = Arc::get_mut(&mut this).unwrap();
if let Some(info) = response.server_info {
this.name = info.name;
}
this.capabilities = response.capabilities;
}
this.notify::<notification::Initialized>(InitializedParams {})?; this.notify::<notification::Initialized>(InitializedParams {})?;
Ok(this) Ok(this)
} }
@ -360,6 +370,10 @@ impl LanguageServer {
} }
} }
pub fn name<'a>(self: &'a Arc<Self>) -> &'a str {
&self.name
}
pub fn capabilities<'a>(self: &'a Arc<Self>) -> &'a ServerCapabilities { pub fn capabilities<'a>(self: &'a Arc<Self>) -> &'a ServerCapabilities {
&self.capabilities &self.capabilities
} }

View file

@ -7,7 +7,7 @@ pub mod worktree;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
use clock::ReplicaId; use clock::ReplicaId;
use collections::{hash_map, HashMap, HashSet}; use collections::{hash_map, BTreeMap, HashMap, HashSet};
use futures::{future::Shared, Future, FutureExt, StreamExt, TryFutureExt}; use futures::{future::Shared, Future, FutureExt, StreamExt, TryFutureExt};
use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet}; use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
use gpui::{ use gpui::{
@ -28,7 +28,6 @@ use rand::prelude::*;
use search::SearchQuery; use search::SearchQuery;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use similar::{ChangeTag, TextDiff}; use similar::{ChangeTag, TextDiff};
use smol::block_on;
use std::{ use std::{
cell::RefCell, cell::RefCell,
cmp::{self, Ordering}, cmp::{self, Ordering},
@ -52,6 +51,8 @@ pub struct Project {
languages: Arc<LanguageRegistry>, languages: Arc<LanguageRegistry>,
language_servers: HashMap<(WorktreeId, Arc<str>), Arc<LanguageServer>>, language_servers: HashMap<(WorktreeId, Arc<str>), Arc<LanguageServer>>,
started_language_servers: HashMap<(WorktreeId, Arc<str>), Task<Option<Arc<LanguageServer>>>>, started_language_servers: HashMap<(WorktreeId, Arc<str>), Task<Option<Arc<LanguageServer>>>>,
language_server_statuses: BTreeMap<usize, LanguageServerStatus>,
next_language_server_id: usize,
client: Arc<client::Client>, client: Arc<client::Client>,
user_store: ModelHandle<UserStore>, user_store: ModelHandle<UserStore>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
@ -115,6 +116,33 @@ pub enum Event {
DiagnosticsUpdated(ProjectPath), DiagnosticsUpdated(ProjectPath),
} }
enum LanguageServerEvent {
WorkStart {
token: String,
},
WorkProgress {
token: String,
progress: LanguageServerProgress,
},
WorkEnd {
token: String,
},
DiagnosticsUpdate(lsp::PublishDiagnosticsParams),
}
pub struct LanguageServerStatus {
pub name: String,
pub pending_work: BTreeMap<String, LanguageServerProgress>,
pending_diagnostic_updates: isize,
}
#[derive(Clone, Debug)]
pub struct LanguageServerProgress {
pub message: Option<String>,
pub percentage: Option<usize>,
pub last_update_at: Instant,
}
#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] #[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
pub struct ProjectPath { pub struct ProjectPath {
pub worktree_id: WorktreeId, pub worktree_id: WorktreeId,
@ -203,8 +231,8 @@ impl Project {
client.add_entity_message_handler(Self::handle_add_collaborator); client.add_entity_message_handler(Self::handle_add_collaborator);
client.add_entity_message_handler(Self::handle_buffer_reloaded); client.add_entity_message_handler(Self::handle_buffer_reloaded);
client.add_entity_message_handler(Self::handle_buffer_saved); client.add_entity_message_handler(Self::handle_buffer_saved);
client.add_entity_message_handler(Self::handle_disk_based_diagnostics_updated); client.add_entity_message_handler(Self::handle_start_language_server);
client.add_entity_message_handler(Self::handle_disk_based_diagnostics_updating); client.add_entity_message_handler(Self::handle_update_language_server);
client.add_entity_message_handler(Self::handle_remove_collaborator); client.add_entity_message_handler(Self::handle_remove_collaborator);
client.add_entity_message_handler(Self::handle_register_worktree); client.add_entity_message_handler(Self::handle_register_worktree);
client.add_entity_message_handler(Self::handle_unregister_worktree); client.add_entity_message_handler(Self::handle_unregister_worktree);
@ -304,6 +332,8 @@ impl Project {
language_servers_with_diagnostics_running: 0, language_servers_with_diagnostics_running: 0,
language_servers: Default::default(), language_servers: Default::default(),
started_language_servers: Default::default(), started_language_servers: Default::default(),
language_server_statuses: Default::default(),
next_language_server_id: 0,
nonce: StdRng::from_entropy().gen(), nonce: StdRng::from_entropy().gen(),
} }
}) })
@ -373,6 +403,21 @@ impl Project {
language_servers_with_diagnostics_running: 0, language_servers_with_diagnostics_running: 0,
language_servers: Default::default(), language_servers: Default::default(),
started_language_servers: Default::default(), started_language_servers: Default::default(),
language_server_statuses: response
.language_servers
.into_iter()
.map(|server| {
(
server.id as usize,
LanguageServerStatus {
name: server.name,
pending_work: Default::default(),
pending_diagnostic_updates: 0,
},
)
})
.collect(),
next_language_server_id: 0,
opened_buffers: Default::default(), opened_buffers: Default::default(),
buffer_snapshots: Default::default(), buffer_snapshots: Default::default(),
nonce: StdRng::from_entropy().gen(), nonce: StdRng::from_entropy().gen(),
@ -1155,92 +1200,71 @@ impl Project {
language: Arc<Language>, language: Arc<Language>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
enum LspEvent {
DiagnosticsStart,
DiagnosticsUpdate(lsp::PublishDiagnosticsParams),
DiagnosticsFinish,
}
let key = (worktree_id, language.name()); let key = (worktree_id, language.name());
self.started_language_servers self.started_language_servers
.entry(key.clone()) .entry(key.clone())
.or_insert_with(|| { .or_insert_with(|| {
let server_id = post_inc(&mut self.next_language_server_id);
let language_server = self.languages.start_language_server( let language_server = self.languages.start_language_server(
language.clone(), language.clone(),
worktree_path, worktree_path,
self.client.http_client(), self.client.http_client(),
cx, cx,
); );
let rpc = self.client.clone();
cx.spawn_weak(|this, mut cx| async move { cx.spawn_weak(|this, mut cx| async move {
let mut language_server = language_server?.await.log_err()?; let mut language_server = language_server?.await.log_err()?;
let this = this.upgrade(&cx)?; let this = this.upgrade(&cx)?;
let (language_server_events_tx, language_server_events_rx) =
smol::channel::unbounded();
let disk_based_sources = language
.disk_based_diagnostic_sources()
.cloned()
.unwrap_or_default();
let disk_based_diagnostics_progress_token =
language.disk_based_diagnostics_progress_token().cloned();
let has_disk_based_diagnostic_progress_token =
disk_based_diagnostics_progress_token.is_some();
let (diagnostics_tx, diagnostics_rx) = smol::channel::unbounded();
// Listen for `PublishDiagnostics` notifications.
language_server language_server
.on_notification::<lsp::notification::PublishDiagnostics, _>({ .on_notification::<lsp::notification::PublishDiagnostics, _>({
let diagnostics_tx = diagnostics_tx.clone(); let language_server_events_tx = language_server_events_tx.clone();
move |params| { move |params| {
if !has_disk_based_diagnostic_progress_token { language_server_events_tx
block_on(diagnostics_tx.send(LspEvent::DiagnosticsStart)).ok(); .try_send(LanguageServerEvent::DiagnosticsUpdate(params))
}
block_on(diagnostics_tx.send(LspEvent::DiagnosticsUpdate(params)))
.ok(); .ok();
if !has_disk_based_diagnostic_progress_token {
block_on(diagnostics_tx.send(LspEvent::DiagnosticsFinish)).ok();
}
} }
}) })
.detach(); .detach();
// Listen for `Progress` notifications. Send an event when the language server
// transitions between running jobs and not running any jobs.
let mut running_jobs_for_this_server: i32 = 0;
language_server language_server
.on_notification::<lsp::notification::Progress, _>(move |params| { .on_notification::<lsp::notification::Progress, _>(move |params| {
let token = match params.token { let token = match params.token {
lsp::NumberOrString::Number(_) => None, lsp::NumberOrString::String(token) => token,
lsp::NumberOrString::String(token) => Some(token), lsp::NumberOrString::Number(token) => {
log::info!("skipping numeric progress token {}", token);
return;
}
}; };
if token == disk_based_diagnostics_progress_token { match params.value {
match params.value { lsp::ProgressParamsValue::WorkDone(progress) => match progress {
lsp::ProgressParamsValue::WorkDone(progress) => { lsp::WorkDoneProgress::Begin(_) => {
match progress { language_server_events_tx
lsp::WorkDoneProgress::Begin(_) => { .try_send(LanguageServerEvent::WorkStart { token })
running_jobs_for_this_server += 1; .ok();
if running_jobs_for_this_server == 1 {
block_on(
diagnostics_tx
.send(LspEvent::DiagnosticsStart),
)
.ok();
}
}
lsp::WorkDoneProgress::End(_) => {
running_jobs_for_this_server -= 1;
if running_jobs_for_this_server == 0 {
block_on(
diagnostics_tx
.send(LspEvent::DiagnosticsFinish),
)
.ok();
}
}
_ => {}
}
} }
} lsp::WorkDoneProgress::Report(report) => {
language_server_events_tx
.try_send(LanguageServerEvent::WorkProgress {
token,
progress: LanguageServerProgress {
message: report.message,
percentage: report
.percentage
.map(|p| p as usize),
last_update_at: Instant::now(),
},
})
.ok();
}
lsp::WorkDoneProgress::End(_) => {
language_server_events_tx
.try_send(LanguageServerEvent::WorkEnd { token })
.ok();
}
},
} }
}) })
.detach(); .detach();
@ -1249,43 +1273,14 @@ impl Project {
cx.spawn(|mut cx| { cx.spawn(|mut cx| {
let this = this.downgrade(); let this = this.downgrade();
async move { async move {
while let Ok(message) = diagnostics_rx.recv().await { while let Ok(event) = language_server_events_rx.recv().await {
let this = this.upgrade(&cx)?; let this = this.upgrade(&cx)?;
match message { this.update(&mut cx, |this, cx| {
LspEvent::DiagnosticsStart => { this.on_lsp_event(server_id, event, &language, cx)
this.update(&mut cx, |this, cx| { });
this.disk_based_diagnostics_started(cx);
if let Some(project_id) = this.remote_id() { // Don't starve the main thread when lots of events arrive all at once.
rpc.send(proto::DiskBasedDiagnosticsUpdating { smol::future::yield_now().await;
project_id,
})
.log_err();
}
});
}
LspEvent::DiagnosticsUpdate(mut params) => {
language.process_diagnostics(&mut params);
this.update(&mut cx, |this, cx| {
this.update_diagnostics(
params,
&disk_based_sources,
cx,
)
.log_err();
});
}
LspEvent::DiagnosticsFinish => {
this.update(&mut cx, |this, cx| {
this.disk_based_diagnostics_finished(cx);
if let Some(project_id) = this.remote_id() {
rpc.send(proto::DiskBasedDiagnosticsUpdated {
project_id,
})
.log_err();
}
});
}
}
} }
Some(()) Some(())
} }
@ -1296,6 +1291,26 @@ impl Project {
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.language_servers this.language_servers
.insert(key.clone(), language_server.clone()); .insert(key.clone(), language_server.clone());
this.language_server_statuses.insert(
server_id,
LanguageServerStatus {
name: language_server.name().to_string(),
pending_work: Default::default(),
pending_diagnostic_updates: 0,
},
);
if let Some(project_id) = this.remote_id() {
this.client
.send(proto::StartLanguageServer {
project_id,
server: Some(proto::LanguageServer {
id: server_id as u64,
name: language_server.name().to_string(),
}),
})
.log_err();
}
// Tell the language server about every open buffer in the worktree that matches the language. // Tell the language server about every open buffer in the worktree that matches the language.
for buffer in this.opened_buffers.values() { for buffer in this.opened_buffers.values() {
@ -1350,6 +1365,7 @@ impl Project {
} }
} }
cx.notify();
Some(()) Some(())
}); });
@ -1358,6 +1374,185 @@ impl Project {
}); });
} }
fn on_lsp_event(
&mut self,
language_server_id: usize,
event: LanguageServerEvent,
language: &Arc<Language>,
cx: &mut ModelContext<Self>,
) {
let disk_diagnostics_token = language.disk_based_diagnostics_progress_token();
let language_server_status =
if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
status
} else {
return;
};
match event {
LanguageServerEvent::WorkStart { token } => {
if Some(&token) == disk_diagnostics_token {
language_server_status.pending_diagnostic_updates += 1;
if language_server_status.pending_diagnostic_updates == 1 {
self.disk_based_diagnostics_started(cx);
self.broadcast_language_server_update(
language_server_id,
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(
proto::LspDiskBasedDiagnosticsUpdating {},
),
);
}
} else {
self.on_lsp_work_start(language_server_id, token.clone(), cx);
self.broadcast_language_server_update(
language_server_id,
proto::update_language_server::Variant::WorkStart(proto::LspWorkStart {
token,
}),
);
}
}
LanguageServerEvent::WorkProgress { token, progress } => {
if Some(&token) != disk_diagnostics_token {
self.on_lsp_work_progress(
language_server_id,
token.clone(),
progress.clone(),
cx,
);
self.broadcast_language_server_update(
language_server_id,
proto::update_language_server::Variant::WorkProgress(
proto::LspWorkProgress {
token,
message: progress.message,
percentage: progress.percentage.map(|p| p as u32),
},
),
);
}
}
LanguageServerEvent::WorkEnd { token } => {
if Some(&token) == disk_diagnostics_token {
language_server_status.pending_diagnostic_updates -= 1;
if language_server_status.pending_diagnostic_updates == 0 {
self.disk_based_diagnostics_finished(cx);
self.broadcast_language_server_update(
language_server_id,
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
proto::LspDiskBasedDiagnosticsUpdated {},
),
);
}
} else {
self.on_lsp_work_end(language_server_id, token.clone(), cx);
self.broadcast_language_server_update(
language_server_id,
proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd {
token,
}),
);
}
}
LanguageServerEvent::DiagnosticsUpdate(mut params) => {
language.process_diagnostics(&mut params);
if disk_diagnostics_token.is_none() {
self.disk_based_diagnostics_started(cx);
self.broadcast_language_server_update(
language_server_id,
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(
proto::LspDiskBasedDiagnosticsUpdating {},
),
);
}
self.update_diagnostics(
params,
language
.disk_based_diagnostic_sources()
.unwrap_or(&Default::default()),
cx,
)
.log_err();
if disk_diagnostics_token.is_none() {
self.disk_based_diagnostics_finished(cx);
self.broadcast_language_server_update(
language_server_id,
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
proto::LspDiskBasedDiagnosticsUpdated {},
),
);
}
}
}
}
fn on_lsp_work_start(
&mut self,
language_server_id: usize,
token: String,
cx: &mut ModelContext<Self>,
) {
if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
status.pending_work.insert(
token,
LanguageServerProgress {
message: None,
percentage: None,
last_update_at: Instant::now(),
},
);
cx.notify();
}
}
fn on_lsp_work_progress(
&mut self,
language_server_id: usize,
token: String,
progress: LanguageServerProgress,
cx: &mut ModelContext<Self>,
) {
if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
status.pending_work.insert(token, progress);
cx.notify();
}
}
fn on_lsp_work_end(
&mut self,
language_server_id: usize,
token: String,
cx: &mut ModelContext<Self>,
) {
if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
status.pending_work.remove(&token);
cx.notify();
}
}
fn broadcast_language_server_update(
&self,
language_server_id: usize,
event: proto::update_language_server::Variant,
) {
if let Some(project_id) = self.remote_id() {
self.client
.send(proto::UpdateLanguageServer {
project_id,
language_server_id: language_server_id as u64,
variant: Some(event),
})
.log_err();
}
}
pub fn language_server_statuses(
&self,
) -> impl DoubleEndedIterator<Item = &LanguageServerStatus> {
self.language_server_statuses.values()
}
pub fn update_diagnostics( pub fn update_diagnostics(
&mut self, &mut self,
params: lsp::PublishDiagnosticsParams, params: lsp::PublishDiagnosticsParams,
@ -3096,23 +3291,76 @@ impl Project {
}) })
} }
async fn handle_disk_based_diagnostics_updating( async fn handle_start_language_server(
this: ModelHandle<Self>, this: ModelHandle<Self>,
_: TypedEnvelope<proto::DiskBasedDiagnosticsUpdating>, envelope: TypedEnvelope<proto::StartLanguageServer>,
_: Arc<Client>, _: Arc<Client>,
mut cx: AsyncAppContext, mut cx: AsyncAppContext,
) -> Result<()> { ) -> Result<()> {
this.update(&mut cx, |this, cx| this.disk_based_diagnostics_started(cx)); let server = envelope
.payload
.server
.ok_or_else(|| anyhow!("invalid server"))?;
this.update(&mut cx, |this, cx| {
this.language_server_statuses.insert(
server.id as usize,
LanguageServerStatus {
name: server.name,
pending_work: Default::default(),
pending_diagnostic_updates: 0,
},
);
cx.notify();
});
Ok(()) Ok(())
} }
async fn handle_disk_based_diagnostics_updated( async fn handle_update_language_server(
this: ModelHandle<Self>, this: ModelHandle<Self>,
_: TypedEnvelope<proto::DiskBasedDiagnosticsUpdated>, envelope: TypedEnvelope<proto::UpdateLanguageServer>,
_: Arc<Client>, _: Arc<Client>,
mut cx: AsyncAppContext, mut cx: AsyncAppContext,
) -> Result<()> { ) -> Result<()> {
this.update(&mut cx, |this, cx| this.disk_based_diagnostics_finished(cx)); let language_server_id = envelope.payload.language_server_id as usize;
match envelope
.payload
.variant
.ok_or_else(|| anyhow!("invalid variant"))?
{
proto::update_language_server::Variant::WorkStart(payload) => {
this.update(&mut cx, |this, cx| {
this.on_lsp_work_start(language_server_id, payload.token, cx);
})
}
proto::update_language_server::Variant::WorkProgress(payload) => {
this.update(&mut cx, |this, cx| {
this.on_lsp_work_progress(
language_server_id,
payload.token,
LanguageServerProgress {
message: payload.message,
percentage: payload.percentage.map(|p| p as usize),
last_update_at: Instant::now(),
},
cx,
);
})
}
proto::update_language_server::Variant::WorkEnd(payload) => {
this.update(&mut cx, |this, cx| {
this.on_lsp_work_end(language_server_id, payload.token, cx);
})
}
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(_) => {
this.update(&mut cx, |this, cx| {
this.disk_based_diagnostics_started(cx);
})
}
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(_) => {
this.update(&mut cx, |this, cx| this.disk_based_diagnostics_finished(cx));
}
}
Ok(()) Ok(())
} }

View file

@ -37,8 +37,8 @@ message Envelope {
UnregisterWorktree unregister_worktree = 29; UnregisterWorktree unregister_worktree = 29;
UpdateWorktree update_worktree = 31; UpdateWorktree update_worktree = 31;
UpdateDiagnosticSummary update_diagnostic_summary = 32; UpdateDiagnosticSummary update_diagnostic_summary = 32;
DiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 33; StartLanguageServer start_language_server = 33;
DiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 34; UpdateLanguageServer update_language_server = 34;
OpenBuffer open_buffer = 35; OpenBuffer open_buffer = 35;
OpenBufferResponse open_buffer_response = 36; OpenBufferResponse open_buffer_response = 36;
@ -122,6 +122,7 @@ message JoinProjectResponse {
uint32 replica_id = 1; uint32 replica_id = 1;
repeated Worktree worktrees = 2; repeated Worktree worktrees = 2;
repeated Collaborator collaborators = 3; repeated Collaborator collaborators = 3;
repeated LanguageServer language_servers = 4;
} }
message LeaveProject { message LeaveProject {
@ -410,6 +411,16 @@ message LocalTimestamp {
uint32 value = 2; uint32 value = 2;
} }
message LanguageServer {
uint64 id = 1;
string name = 2;
}
message StartLanguageServer {
uint64 project_id = 1;
LanguageServer server = 2;
}
message UpdateDiagnosticSummary { message UpdateDiagnosticSummary {
uint64 project_id = 1; uint64 project_id = 1;
uint64 worktree_id = 2; uint64 worktree_id = 2;
@ -424,14 +435,36 @@ message DiagnosticSummary {
uint32 hint_count = 5; uint32 hint_count = 5;
} }
message DiskBasedDiagnosticsUpdating { message UpdateLanguageServer {
uint64 project_id = 1; uint64 project_id = 1;
uint64 language_server_id = 2;
oneof variant {
LspWorkStart work_start = 3;
LspWorkProgress work_progress = 4;
LspWorkEnd work_end = 5;
LspDiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 6;
LspDiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 7;
}
} }
message DiskBasedDiagnosticsUpdated { message LspWorkStart {
uint64 project_id = 1; string token = 1;
} }
message LspWorkProgress {
string token = 1;
optional string message = 2;
optional uint32 percentage = 3;
}
message LspWorkEnd {
string token = 1;
}
message LspDiskBasedDiagnosticsUpdating {}
message LspDiskBasedDiagnosticsUpdated {}
message GetChannels {} message GetChannels {}
message GetChannelsResponse { message GetChannelsResponse {

View file

@ -96,6 +96,7 @@ pub struct ConnectionState {
const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1); const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1);
const WRITE_TIMEOUT: Duration = Duration::from_secs(2); const WRITE_TIMEOUT: Duration = Duration::from_secs(2);
const RECEIVE_TIMEOUT: Duration = Duration::from_secs(30);
impl Peer { impl Peer {
pub fn new() -> Arc<Self> { pub fn new() -> Arc<Self> {
@ -147,14 +148,14 @@ impl Peer {
let keepalive_timer = create_timer(KEEPALIVE_INTERVAL).fuse(); let keepalive_timer = create_timer(KEEPALIVE_INTERVAL).fuse();
futures::pin_mut!(keepalive_timer); futures::pin_mut!(keepalive_timer);
// Disconnect if we don't receive messages at least this frequently.
let receive_timeout = create_timer(RECEIVE_TIMEOUT).fuse();
futures::pin_mut!(receive_timeout);
loop { loop {
let read_message = reader.read().fuse(); let read_message = reader.read().fuse();
futures::pin_mut!(read_message); futures::pin_mut!(read_message);
// Disconnect if we don't receive messages at least this frequently.
let receive_timeout = create_timer(3 * KEEPALIVE_INTERVAL).fuse();
futures::pin_mut!(receive_timeout);
loop { loop {
futures::select_biased! { futures::select_biased! {
outgoing = outgoing_rx.next().fuse() => match outgoing { outgoing = outgoing_rx.next().fuse() => match outgoing {
@ -170,6 +171,7 @@ impl Peer {
}, },
incoming = read_message => { incoming = read_message => {
let incoming = incoming.context("received invalid RPC message")?; let incoming = incoming.context("received invalid RPC message")?;
receive_timeout.set(create_timer(RECEIVE_TIMEOUT).fuse());
if let proto::Message::Envelope(incoming) = incoming { if let proto::Message::Envelope(incoming) = incoming {
if incoming_tx.send(incoming).await.is_err() { if incoming_tx.send(incoming).await.is_err() {
return Ok(()); return Ok(());

View file

@ -146,8 +146,6 @@ messages!(
(BufferReloaded, Foreground), (BufferReloaded, Foreground),
(BufferSaved, Foreground), (BufferSaved, Foreground),
(ChannelMessageSent, Foreground), (ChannelMessageSent, Foreground),
(DiskBasedDiagnosticsUpdated, Background),
(DiskBasedDiagnosticsUpdating, Background),
(Error, Foreground), (Error, Foreground),
(FormatBuffers, Foreground), (FormatBuffers, Foreground),
(FormatBuffersResponse, Foreground), (FormatBuffersResponse, Foreground),
@ -173,6 +171,8 @@ messages!(
(JoinChannelResponse, Foreground), (JoinChannelResponse, Foreground),
(JoinProject, Foreground), (JoinProject, Foreground),
(JoinProjectResponse, Foreground), (JoinProjectResponse, Foreground),
(StartLanguageServer, Foreground),
(UpdateLanguageServer, Foreground),
(LeaveChannel, Foreground), (LeaveChannel, Foreground),
(LeaveProject, Foreground), (LeaveProject, Foreground),
(OpenBuffer, Background), (OpenBuffer, Background),
@ -246,8 +246,6 @@ entity_messages!(
ApplyCompletionAdditionalEdits, ApplyCompletionAdditionalEdits,
BufferReloaded, BufferReloaded,
BufferSaved, BufferSaved,
DiskBasedDiagnosticsUpdated,
DiskBasedDiagnosticsUpdating,
FormatBuffers, FormatBuffers,
GetCodeActions, GetCodeActions,
GetCompletions, GetCompletions,
@ -264,11 +262,13 @@ entity_messages!(
RemoveProjectCollaborator, RemoveProjectCollaborator,
SaveBuffer, SaveBuffer,
SearchProject, SearchProject,
StartLanguageServer,
UnregisterWorktree, UnregisterWorktree,
UnshareProject, UnshareProject,
UpdateBuffer, UpdateBuffer,
UpdateBufferFile, UpdateBufferFile,
UpdateDiagnosticSummary, UpdateDiagnosticSummary,
UpdateLanguageServer,
RegisterWorktree, RegisterWorktree,
UpdateWorktree, UpdateWorktree,
); );

View file

@ -83,9 +83,9 @@ impl Server {
.add_request_handler(Server::register_worktree) .add_request_handler(Server::register_worktree)
.add_message_handler(Server::unregister_worktree) .add_message_handler(Server::unregister_worktree)
.add_request_handler(Server::update_worktree) .add_request_handler(Server::update_worktree)
.add_message_handler(Server::start_language_server)
.add_message_handler(Server::update_language_server)
.add_message_handler(Server::update_diagnostic_summary) .add_message_handler(Server::update_diagnostic_summary)
.add_message_handler(Server::disk_based_diagnostics_updating)
.add_message_handler(Server::disk_based_diagnostics_updated)
.add_request_handler(Server::forward_project_request::<proto::GetDefinition>) .add_request_handler(Server::forward_project_request::<proto::GetDefinition>)
.add_request_handler(Server::forward_project_request::<proto::GetReferences>) .add_request_handler(Server::forward_project_request::<proto::GetReferences>)
.add_request_handler(Server::forward_project_request::<proto::SearchProject>) .add_request_handler(Server::forward_project_request::<proto::SearchProject>)
@ -386,6 +386,7 @@ impl Server {
worktrees, worktrees,
replica_id: joined.replica_id as u32, replica_id: joined.replica_id as u32,
collaborators, collaborators,
language_servers: joined.project.language_servers.clone(),
}; };
let connection_ids = joined.project.connection_ids(); let connection_ids = joined.project.connection_ids();
let contact_user_ids = joined.project.authorized_user_ids(); let contact_user_ids = joined.project.authorized_user_ids();
@ -535,13 +536,19 @@ impl Server {
Ok(()) Ok(())
} }
async fn disk_based_diagnostics_updating( async fn start_language_server(
self: Arc<Server>, mut self: Arc<Server>,
request: TypedEnvelope<proto::DiskBasedDiagnosticsUpdating>, request: TypedEnvelope<proto::StartLanguageServer>,
) -> tide::Result<()> { ) -> tide::Result<()> {
let receiver_ids = self let receiver_ids = self.state_mut().start_language_server(
.state() request.payload.project_id,
.project_connection_ids(request.payload.project_id, request.sender_id)?; request.sender_id,
request
.payload
.server
.clone()
.ok_or_else(|| anyhow!("invalid language server"))?,
)?;
broadcast(request.sender_id, receiver_ids, |connection_id| { broadcast(request.sender_id, receiver_ids, |connection_id| {
self.peer self.peer
.forward_send(request.sender_id, connection_id, request.payload.clone()) .forward_send(request.sender_id, connection_id, request.payload.clone())
@ -549,9 +556,9 @@ impl Server {
Ok(()) Ok(())
} }
async fn disk_based_diagnostics_updated( async fn update_language_server(
self: Arc<Server>, self: Arc<Server>,
request: TypedEnvelope<proto::DiskBasedDiagnosticsUpdated>, request: TypedEnvelope<proto::UpdateLanguageServer>,
) -> tide::Result<()> { ) -> tide::Result<()> {
let receiver_ids = self let receiver_ids = self
.state() .state()

View file

@ -25,6 +25,7 @@ pub struct Project {
pub host_user_id: UserId, pub host_user_id: UserId,
pub share: Option<ProjectShare>, pub share: Option<ProjectShare>,
pub worktrees: HashMap<u64, Worktree>, pub worktrees: HashMap<u64, Worktree>,
pub language_servers: Vec<proto::LanguageServer>,
} }
pub struct Worktree { pub struct Worktree {
@ -240,6 +241,7 @@ impl Store {
host_user_id, host_user_id,
share: None, share: None,
worktrees: Default::default(), worktrees: Default::default(),
language_servers: Default::default(),
}, },
); );
self.next_project_id += 1; self.next_project_id += 1;
@ -438,6 +440,24 @@ impl Store {
Err(anyhow!("no such worktree"))? Err(anyhow!("no such worktree"))?
} }
pub fn start_language_server(
&mut self,
project_id: u64,
connection_id: ConnectionId,
language_server: proto::LanguageServer,
) -> tide::Result<Vec<ConnectionId>> {
let project = self
.projects
.get_mut(&project_id)
.ok_or_else(|| anyhow!("no such project"))?;
if project.host_connection_id == connection_id {
project.language_servers.push(language_server);
return Ok(project.connection_ids());
}
Err(anyhow!("no such project"))?
}
pub fn join_project( pub fn join_project(
&mut self, &mut self,
connection_id: ConnectionId, connection_id: ConnectionId,

View file

@ -26,6 +26,7 @@ parking_lot = "0.11.1"
postage = { version = "0.4.1", features = ["futures-traits"] } postage = { version = "0.4.1", features = ["futures-traits"] }
serde = { version = "1", features = ["derive", "rc"] } serde = { version = "1", features = ["derive", "rc"] }
serde_json = { version = "1", features = ["preserve_order"] } serde_json = { version = "1", features = ["preserve_order"] }
smallvec = { version = "1.6", features = ["union"] }
[dev-dependencies] [dev-dependencies]
client = { path = "../client", features = ["test-support"] } client = { path = "../client", features = ["test-support"] }

View file

@ -1,11 +1,16 @@
use crate::{ItemViewHandle, Settings, StatusItemView}; use crate::{ItemViewHandle, Settings, StatusItemView};
use futures::StreamExt; use futures::StreamExt;
use gpui::AppContext;
use gpui::{ use gpui::{
action, elements::*, platform::CursorStyle, Entity, MutableAppContext, RenderContext, View, action, elements::*, platform::CursorStyle, Entity, ModelHandle, MutableAppContext,
ViewContext, RenderContext, View, ViewContext,
}; };
use language::{LanguageRegistry, LanguageServerBinaryStatus}; use language::{LanguageRegistry, LanguageServerBinaryStatus};
use postage::watch; use postage::watch;
use project::{LanguageServerProgress, Project};
use smallvec::SmallVec;
use std::cmp::Reverse;
use std::fmt::Write;
use std::sync::Arc; use std::sync::Arc;
action!(DismissErrorMessage); action!(DismissErrorMessage);
@ -15,6 +20,7 @@ pub struct LspStatus {
checking_for_update: Vec<String>, checking_for_update: Vec<String>,
downloading: Vec<String>, downloading: Vec<String>,
failed: Vec<String>, failed: Vec<String>,
project: ModelHandle<Project>,
} }
pub fn init(cx: &mut MutableAppContext) { pub fn init(cx: &mut MutableAppContext) {
@ -23,6 +29,7 @@ pub fn init(cx: &mut MutableAppContext) {
impl LspStatus { impl LspStatus {
pub fn new( pub fn new(
project: &ModelHandle<Project>,
languages: Arc<LanguageRegistry>, languages: Arc<LanguageRegistry>,
settings_rx: watch::Receiver<Settings>, settings_rx: watch::Receiver<Settings>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
@ -62,11 +69,14 @@ impl LspStatus {
} }
}) })
.detach(); .detach();
cx.observe(project, |_, _, cx| cx.notify()).detach();
Self { Self {
settings_rx, settings_rx,
checking_for_update: Default::default(), checking_for_update: Default::default(),
downloading: Default::default(), downloading: Default::default(),
failed: Default::default(), failed: Default::default(),
project: project.clone(),
} }
} }
@ -74,6 +84,30 @@ impl LspStatus {
self.failed.clear(); self.failed.clear();
cx.notify(); cx.notify();
} }
fn pending_language_server_work<'a>(
&self,
cx: &'a AppContext,
) -> impl Iterator<Item = (&'a str, &'a str, &'a LanguageServerProgress)> {
self.project
.read(cx)
.language_server_statuses()
.rev()
.filter_map(|status| {
if status.pending_work.is_empty() {
None
} else {
let mut pending_work = status
.pending_work
.iter()
.map(|(token, progress)| (status.name.as_str(), token.as_str(), progress))
.collect::<SmallVec<[_; 4]>>();
pending_work.sort_by_key(|(_, _, progress)| Reverse(progress.last_update_at));
Some(pending_work)
}
})
.flatten()
}
} }
impl Entity for LspStatus { impl Entity for LspStatus {
@ -87,7 +121,29 @@ impl View for LspStatus {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox { fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &self.settings_rx.borrow().theme; let theme = &self.settings_rx.borrow().theme;
if !self.downloading.is_empty() {
let mut pending_work = self.pending_language_server_work(cx);
if let Some((lang_server_name, progress_token, progress)) = pending_work.next() {
let mut message = lang_server_name.to_string();
message.push_str(": ");
if let Some(progress_message) = progress.message.as_ref() {
message.push_str(progress_message);
} else {
message.push_str(progress_token);
}
if let Some(percentage) = progress.percentage {
write!(&mut message, " ({}%)", percentage).unwrap();
}
let additional_work_count = pending_work.count();
if additional_work_count > 0 {
write!(&mut message, " + {} more", additional_work_count).unwrap();
}
Label::new(message, theme.workspace.status_bar.lsp_message.clone()).boxed()
} else if !self.downloading.is_empty() {
Label::new( Label::new(
format!( format!(
"Downloading {} language server{}...", "Downloading {} language server{}...",
@ -112,6 +168,7 @@ impl View for LspStatus {
) )
.boxed() .boxed()
} else if !self.failed.is_empty() { } else if !self.failed.is_empty() {
drop(pending_work);
MouseEventHandler::new::<Self, _, _>(0, cx, |_, _| { MouseEventHandler::new::<Self, _, _>(0, cx, |_, _| {
Label::new( Label::new(
format!( format!(

View file

@ -6,9 +6,9 @@ use gpui::{
elements::*, elements::*,
geometry::{rect::RectF, vector::vec2f}, geometry::{rect::RectF, vector::vec2f},
keymap::Binding, keymap::Binding,
platform::CursorStyle, platform::{CursorStyle, NavigationDirection},
AnyViewHandle, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext, AnyViewHandle, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext,
ViewHandle, ViewHandle, WeakViewHandle,
}; };
use postage::watch; use postage::watch;
use project::ProjectPath; use project::ProjectPath;
@ -27,8 +27,8 @@ action!(ActivateNextItem);
action!(CloseActiveItem); action!(CloseActiveItem);
action!(CloseInactiveItems); action!(CloseInactiveItems);
action!(CloseItem, usize); action!(CloseItem, usize);
action!(GoBack); action!(GoBack, Option<WeakViewHandle<Pane>>);
action!(GoForward); action!(GoForward, Option<WeakViewHandle<Pane>>);
const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
@ -54,11 +54,27 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(|pane: &mut Pane, action: &Split, cx| { cx.add_action(|pane: &mut Pane, action: &Split, cx| {
pane.split(action.0, cx); pane.split(action.0, cx);
}); });
cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| { cx.add_action(|workspace: &mut Workspace, action: &GoBack, cx| {
Pane::go_back(workspace, cx).detach(); Pane::go_back(
workspace,
action
.0
.as_ref()
.and_then(|weak_handle| weak_handle.upgrade(cx)),
cx,
)
.detach();
}); });
cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| { cx.add_action(|workspace: &mut Workspace, action: &GoForward, cx| {
Pane::go_forward(workspace, cx).detach(); Pane::go_forward(
workspace,
action
.0
.as_ref()
.and_then(|weak_handle| weak_handle.upgrade(cx)),
cx,
)
.detach();
}); });
cx.add_bindings(vec![ cx.add_bindings(vec![
@ -70,8 +86,8 @@ pub fn init(cx: &mut MutableAppContext) {
Binding::new("cmd-k down", Split(SplitDirection::Down), Some("Pane")), Binding::new("cmd-k down", Split(SplitDirection::Down), Some("Pane")),
Binding::new("cmd-k left", Split(SplitDirection::Left), Some("Pane")), Binding::new("cmd-k left", Split(SplitDirection::Left), Some("Pane")),
Binding::new("cmd-k right", Split(SplitDirection::Right), Some("Pane")), Binding::new("cmd-k right", Split(SplitDirection::Right), Some("Pane")),
Binding::new("ctrl--", GoBack, Some("Pane")), Binding::new("ctrl--", GoBack(None), Some("Pane")),
Binding::new("shift-ctrl-_", GoForward, Some("Pane")), Binding::new("shift-ctrl-_", GoForward(None), Some("Pane")),
]); ]);
} }
@ -163,19 +179,27 @@ impl Pane {
cx.emit(Event::Activate); cx.emit(Event::Activate);
} }
pub fn go_back(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Task<()> { pub fn go_back(
workspace: &mut Workspace,
pane: Option<ViewHandle<Pane>>,
cx: &mut ViewContext<Workspace>,
) -> Task<()> {
Self::navigate_history( Self::navigate_history(
workspace, workspace,
workspace.active_pane().clone(), pane.unwrap_or_else(|| workspace.active_pane().clone()),
NavigationMode::GoingBack, NavigationMode::GoingBack,
cx, cx,
) )
} }
pub fn go_forward(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Task<()> { pub fn go_forward(
workspace: &mut Workspace,
pane: Option<ViewHandle<Pane>>,
cx: &mut ViewContext<Workspace>,
) -> Task<()> {
Self::navigate_history( Self::navigate_history(
workspace, workspace,
workspace.active_pane().clone(), pane.unwrap_or_else(|| workspace.active_pane().clone()),
NavigationMode::GoingForward, NavigationMode::GoingForward,
cx, cx,
) )
@ -187,6 +211,8 @@ impl Pane {
mode: NavigationMode, mode: NavigationMode,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) -> Task<()> { ) -> Task<()> {
workspace.activate_pane(pane.clone(), cx);
let to_load = pane.update(cx, |pane, cx| { let to_load = pane.update(cx, |pane, cx| {
// Retrieve the weak item handle from the history. // Retrieve the weak item handle from the history.
let entry = pane.nav_history.borrow_mut().pop(mode)?; let entry = pane.nav_history.borrow_mut().pop(mode)?;
@ -634,7 +660,9 @@ impl View for Pane {
} }
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox { fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
if let Some(active_item) = self.active_item() { let this = cx.handle();
EventHandler::new(if let Some(active_item) = self.active_item() {
Flex::column() Flex::column()
.with_child(self.render_tabs(cx)) .with_child(self.render_tabs(cx))
.with_children( .with_children(
@ -643,10 +671,20 @@ impl View for Pane {
.map(|view| ChildView::new(view).boxed()), .map(|view| ChildView::new(view).boxed()),
) )
.with_child(ChildView::new(active_item).flexible(1., true).boxed()) .with_child(ChildView::new(active_item).flexible(1., true).boxed())
.named("pane") .boxed()
} else { } else {
Empty::new().named("pane") Empty::new().boxed()
} })
.on_navigate_mouse_down(move |direction, cx| {
let this = this.clone();
match direction {
NavigationDirection::Back => cx.dispatch_action(GoBack(Some(this))),
NavigationDirection::Forward => cx.dispatch_action(GoForward(Some(this))),
}
true
})
.named("pane")
} }
fn on_focus(&mut self, cx: &mut ViewContext<Self>) { fn on_focus(&mut self, cx: &mut ViewContext<Self>) {

View file

@ -129,6 +129,7 @@ pub fn build_workspace(
}); });
let lsp_status = cx.add_view(|cx| { let lsp_status = cx.add_view(|cx| {
workspace::lsp_status::LspStatus::new( workspace::lsp_status::LspStatus::new(
workspace.project(),
app_state.languages.clone(), app_state.languages.clone(),
app_state.settings.clone(), app_state.settings.clone(),
cx, cx,
@ -775,44 +776,58 @@ mod tests {
(file3.clone(), DisplayPoint::new(15, 0)) (file3.clone(), DisplayPoint::new(15, 0))
); );
workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await; workspace
.update(cx, |w, cx| Pane::go_back(w, None, cx))
.await;
assert_eq!( assert_eq!(
active_location(&workspace, cx), active_location(&workspace, cx),
(file3.clone(), DisplayPoint::new(0, 0)) (file3.clone(), DisplayPoint::new(0, 0))
); );
workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await; workspace
.update(cx, |w, cx| Pane::go_back(w, None, cx))
.await;
assert_eq!( assert_eq!(
active_location(&workspace, cx), active_location(&workspace, cx),
(file2.clone(), DisplayPoint::new(0, 0)) (file2.clone(), DisplayPoint::new(0, 0))
); );
workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await; workspace
.update(cx, |w, cx| Pane::go_back(w, None, cx))
.await;
assert_eq!( assert_eq!(
active_location(&workspace, cx), active_location(&workspace, cx),
(file1.clone(), DisplayPoint::new(10, 0)) (file1.clone(), DisplayPoint::new(10, 0))
); );
workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await; workspace
.update(cx, |w, cx| Pane::go_back(w, None, cx))
.await;
assert_eq!( assert_eq!(
active_location(&workspace, cx), active_location(&workspace, cx),
(file1.clone(), DisplayPoint::new(0, 0)) (file1.clone(), DisplayPoint::new(0, 0))
); );
// Go back one more time and ensure we don't navigate past the first item in the history. // Go back one more time and ensure we don't navigate past the first item in the history.
workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await; workspace
.update(cx, |w, cx| Pane::go_back(w, None, cx))
.await;
assert_eq!( assert_eq!(
active_location(&workspace, cx), active_location(&workspace, cx),
(file1.clone(), DisplayPoint::new(0, 0)) (file1.clone(), DisplayPoint::new(0, 0))
); );
workspace.update(cx, |w, cx| Pane::go_forward(w, cx)).await; workspace
.update(cx, |w, cx| Pane::go_forward(w, None, cx))
.await;
assert_eq!( assert_eq!(
active_location(&workspace, cx), active_location(&workspace, cx),
(file1.clone(), DisplayPoint::new(10, 0)) (file1.clone(), DisplayPoint::new(10, 0))
); );
workspace.update(cx, |w, cx| Pane::go_forward(w, cx)).await; workspace
.update(cx, |w, cx| Pane::go_forward(w, None, cx))
.await;
assert_eq!( assert_eq!(
active_location(&workspace, cx), active_location(&workspace, cx),
(file2.clone(), DisplayPoint::new(0, 0)) (file2.clone(), DisplayPoint::new(0, 0))
@ -826,7 +841,9 @@ mod tests {
.update(cx, |pane, cx| pane.close_item(editor3.id(), cx)); .update(cx, |pane, cx| pane.close_item(editor3.id(), cx));
drop(editor3); drop(editor3);
}); });
workspace.update(cx, |w, cx| Pane::go_forward(w, cx)).await; workspace
.update(cx, |w, cx| Pane::go_forward(w, None, cx))
.await;
assert_eq!( assert_eq!(
active_location(&workspace, cx), active_location(&workspace, cx),
(file3.clone(), DisplayPoint::new(0, 0)) (file3.clone(), DisplayPoint::new(0, 0))
@ -846,12 +863,16 @@ mod tests {
}) })
.await .await
.unwrap(); .unwrap();
workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await; workspace
.update(cx, |w, cx| Pane::go_back(w, None, cx))
.await;
assert_eq!( assert_eq!(
active_location(&workspace, cx), active_location(&workspace, cx),
(file1.clone(), DisplayPoint::new(10, 0)) (file1.clone(), DisplayPoint::new(10, 0))
); );
workspace.update(cx, |w, cx| Pane::go_forward(w, cx)).await; workspace
.update(cx, |w, cx| Pane::go_forward(w, None, cx))
.await;
assert_eq!( assert_eq!(
active_location(&workspace, cx), active_location(&workspace, cx),
(file3.clone(), DisplayPoint::new(0, 0)) (file3.clone(), DisplayPoint::new(0, 0))