Compare commits
14 Commits
v0.202.8
...
v0.82.6-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d21238f5a0 | ||
|
|
1722fc9fe2 | ||
|
|
6ae29d3bb0 | ||
|
|
3afbe51e30 | ||
|
|
79df2a4f3c | ||
|
|
a4c45236e3 | ||
|
|
6dde8a9b59 | ||
|
|
ccde289ed7 | ||
|
|
9a026f3c4b | ||
|
|
301609d595 | ||
|
|
ea8dba625d | ||
|
|
b089be40ba | ||
|
|
b83451ccf6 | ||
|
|
50658077a0 |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -8516,7 +8516,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.82.0"
|
||||
version = "0.82.6"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
||||
@@ -5862,10 +5862,17 @@ async fn test_basic_following(
|
||||
|
||||
// Client A updates their selections in those editors
|
||||
editor_a1.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
|
||||
editor.handle_input("a", cx);
|
||||
editor.handle_input("b", cx);
|
||||
editor.handle_input("c", cx);
|
||||
editor.select_left(&Default::default(), cx);
|
||||
assert_eq!(editor.selections.ranges(cx), vec![3..2]);
|
||||
});
|
||||
editor_a2.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
|
||||
editor.handle_input("d", cx);
|
||||
editor.handle_input("e", cx);
|
||||
editor.select_left(&Default::default(), cx);
|
||||
assert_eq!(editor.selections.ranges(cx), vec![2..1]);
|
||||
});
|
||||
|
||||
// When client B starts following client A, all visible view states are replicated to client B.
|
||||
@@ -5878,6 +5885,27 @@ async fn test_basic_following(
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx_c.foreground().run_until_parked();
|
||||
let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap()
|
||||
});
|
||||
assert_eq!(
|
||||
cx_b.read(|cx| editor_b2.project_path(cx)),
|
||||
Some((worktree_id, "2.txt").into())
|
||||
);
|
||||
assert_eq!(
|
||||
editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
vec![2..1]
|
||||
);
|
||||
assert_eq!(
|
||||
editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
vec![3..2]
|
||||
);
|
||||
|
||||
cx_c.foreground().run_until_parked();
|
||||
let active_call_c = cx_c.read(ActiveCall::global);
|
||||
let project_c = client_c.build_remote_project(project_id, cx_c).await;
|
||||
@@ -6033,26 +6061,6 @@ async fn test_basic_following(
|
||||
});
|
||||
}
|
||||
|
||||
let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap()
|
||||
});
|
||||
assert_eq!(
|
||||
cx_b.read(|cx| editor_b2.project_path(cx)),
|
||||
Some((worktree_id, "2.txt").into())
|
||||
);
|
||||
assert_eq!(
|
||||
editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
vec![2..3]
|
||||
);
|
||||
assert_eq!(
|
||||
editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
vec![0..1]
|
||||
);
|
||||
|
||||
// When client A activates a different editor, client B does so as well.
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace.activate_item(&editor_a1, cx)
|
||||
|
||||
@@ -20,7 +20,7 @@ mod editor_tests;
|
||||
pub mod test;
|
||||
|
||||
use aho_corasick::AhoCorasick;
|
||||
use anyhow::Result;
|
||||
use anyhow::{anyhow, Result};
|
||||
use blink_manager::BlinkManager;
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
||||
@@ -1040,7 +1040,8 @@ impl CopilotState {
|
||||
let completion = self.completions.get(self.active_completion_index)?;
|
||||
let excerpt_id = self.excerpt_id?;
|
||||
let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?;
|
||||
if !completion.range.start.is_valid(completion_buffer)
|
||||
if excerpt_id != cursor.excerpt_id
|
||||
|| !completion.range.start.is_valid(completion_buffer)
|
||||
|| !completion.range.end.is_valid(completion_buffer)
|
||||
{
|
||||
return None;
|
||||
@@ -2351,53 +2352,66 @@ impl Editor {
|
||||
let id = post_inc(&mut self.next_completion_id);
|
||||
let task = cx.spawn_weak(|this, mut cx| {
|
||||
async move {
|
||||
let completions = completions.await?;
|
||||
if completions.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut menu = CompletionsMenu {
|
||||
id,
|
||||
initial_position: position,
|
||||
match_candidates: completions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, completion)| {
|
||||
StringMatchCandidate::new(
|
||||
id,
|
||||
completion.label.text[completion.label.filter_range.clone()].into(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
buffer,
|
||||
completions: completions.into(),
|
||||
matches: Vec::new().into(),
|
||||
selected_item: 0,
|
||||
list: Default::default(),
|
||||
let menu = if let Some(completions) = completions.await.log_err() {
|
||||
let mut menu = CompletionsMenu {
|
||||
id,
|
||||
initial_position: position,
|
||||
match_candidates: completions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, completion)| {
|
||||
StringMatchCandidate::new(
|
||||
id,
|
||||
completion.label.text[completion.label.filter_range.clone()]
|
||||
.into(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
buffer,
|
||||
completions: completions.into(),
|
||||
matches: Vec::new().into(),
|
||||
selected_item: 0,
|
||||
list: Default::default(),
|
||||
};
|
||||
menu.filter(query.as_deref(), cx.background()).await;
|
||||
if menu.matches.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(menu)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
menu.filter(query.as_deref(), cx.background()).await;
|
||||
let this = this
|
||||
.upgrade(&cx)
|
||||
.ok_or_else(|| anyhow!("editor was dropped"))?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.completion_tasks.retain(|(task_id, _)| *task_id > id);
|
||||
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
match this.context_menu.as_ref() {
|
||||
None => {}
|
||||
Some(ContextMenu::Completions(prev_menu)) => {
|
||||
if prev_menu.id > menu.id {
|
||||
return;
|
||||
}
|
||||
match this.context_menu.as_ref() {
|
||||
None => {}
|
||||
Some(ContextMenu::Completions(prev_menu)) => {
|
||||
if prev_menu.id > id {
|
||||
return;
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
|
||||
this.completion_tasks.retain(|(id, _)| *id > menu.id);
|
||||
if this.focused && !menu.matches.is_empty() {
|
||||
this.show_context_menu(ContextMenu::Completions(menu), cx);
|
||||
} else if this.hide_context_menu(cx).is_none() {
|
||||
if this.focused && menu.is_some() {
|
||||
let menu = menu.unwrap();
|
||||
this.show_context_menu(ContextMenu::Completions(menu), cx);
|
||||
} else if this.completion_tasks.is_empty() {
|
||||
// If there are no more completion tasks and the last menu was
|
||||
// empty, we should hide it. If it was already hidden, we should
|
||||
// also show the copilot suggestion when available.
|
||||
if this.hide_context_menu(cx).is_none() {
|
||||
this.update_visible_copilot_suggestion(cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok::<_, anyhow::Error>(())
|
||||
}
|
||||
.log_err()
|
||||
@@ -6619,13 +6633,15 @@ impl Editor {
|
||||
.as_singleton()
|
||||
.and_then(|b| b.read(cx).file()),
|
||||
) {
|
||||
let settings = cx.global::<Settings>();
|
||||
|
||||
let extension = Path::new(file.file_name(cx))
|
||||
.extension()
|
||||
.and_then(|e| e.to_str());
|
||||
project.read(cx).client().report_event(
|
||||
name,
|
||||
json!({ "File Extension": extension }),
|
||||
cx.global::<Settings>().telemetry(),
|
||||
json!({ "File Extension": extension, "Vim Mode": settings.vim_mode }),
|
||||
settings.telemetry(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5897,13 +5897,12 @@ async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppC
|
||||
)
|
||||
.await;
|
||||
|
||||
// When inserting, ensure autocompletion is favored over Copilot suggestions.
|
||||
cx.set_state(indoc! {"
|
||||
oneˇ
|
||||
two
|
||||
three
|
||||
"});
|
||||
|
||||
// When inserting, ensure autocompletion is favored over Copilot suggestions.
|
||||
cx.simulate_keystroke(".");
|
||||
let _ = handle_completion_request(
|
||||
&mut cx,
|
||||
@@ -5917,8 +5916,8 @@ async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppC
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
text: "copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
@@ -5940,13 +5939,45 @@ async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppC
|
||||
assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
// Ensure Copilot suggestions are shown right away if no autocompletion is available.
|
||||
cx.set_state(indoc! {"
|
||||
oneˇ
|
||||
two
|
||||
three
|
||||
"});
|
||||
cx.simulate_keystroke(".");
|
||||
let _ = handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.|<>
|
||||
two
|
||||
three
|
||||
"},
|
||||
vec![],
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(editor.has_active_copilot_suggestion(cx));
|
||||
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
// When inserting, ensure autocompletion is favored over Copilot suggestions.
|
||||
// Reset editor, and ensure autocompletion is still favored over Copilot suggestions.
|
||||
cx.set_state(indoc! {"
|
||||
oneˇ
|
||||
two
|
||||
three
|
||||
"});
|
||||
cx.simulate_keystroke(".");
|
||||
let _ = handle_completion_request(
|
||||
&mut cx,
|
||||
@@ -6163,6 +6194,110 @@ async fn test_copilot_completion_invalidation(
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_copilot_multibuffer(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let (copilot, copilot_lsp) = Copilot::fake(cx);
|
||||
cx.update(|cx| {
|
||||
cx.set_global(Settings::test(cx));
|
||||
cx.set_global(copilot)
|
||||
});
|
||||
|
||||
let buffer_1 = cx.add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx));
|
||||
let buffer_2 = cx.add_model(|cx| Buffer::new(0, "c = 3\nd = 4\n", cx));
|
||||
let multibuffer = cx.add_model(|cx| {
|
||||
let mut multibuffer = MultiBuffer::new(0);
|
||||
multibuffer.push_excerpts(
|
||||
buffer_1.clone(),
|
||||
[ExcerptRange {
|
||||
context: Point::new(0, 0)..Point::new(2, 0),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
multibuffer.push_excerpts(
|
||||
buffer_2.clone(),
|
||||
[ExcerptRange {
|
||||
context: Point::new(0, 0)..Point::new(2, 0),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
multibuffer
|
||||
});
|
||||
let (_, editor) = cx.add_window(|cx| build_editor(multibuffer, cx));
|
||||
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
text: "b = 2 + a".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
editor.update(cx, |editor, cx| {
|
||||
// Ensure copilot suggestions are shown for the first excerpt.
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
|
||||
});
|
||||
editor.next_copilot_suggestion(&Default::default(), cx);
|
||||
});
|
||||
deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert!(editor.has_active_copilot_suggestion(cx));
|
||||
assert_eq!(
|
||||
editor.display_text(cx),
|
||||
"\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n"
|
||||
);
|
||||
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
|
||||
});
|
||||
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
text: "d = 4 + c".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
editor.update(cx, |editor, cx| {
|
||||
// Move to another excerpt, ensuring the suggestion gets cleared.
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
|
||||
});
|
||||
assert!(!editor.has_active_copilot_suggestion(cx));
|
||||
assert_eq!(
|
||||
editor.display_text(cx),
|
||||
"\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n"
|
||||
);
|
||||
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
|
||||
|
||||
// Type a character, ensuring we don't even try to interpolate the previous suggestion.
|
||||
editor.handle_input(" ", cx);
|
||||
assert!(!editor.has_active_copilot_suggestion(cx));
|
||||
assert_eq!(
|
||||
editor.display_text(cx),
|
||||
"\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n"
|
||||
);
|
||||
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
|
||||
});
|
||||
|
||||
// Ensure the new suggestion is displayed when the debounce timeout expires.
|
||||
deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert!(editor.has_active_copilot_suggestion(cx));
|
||||
assert_eq!(
|
||||
editor.display_text(cx),
|
||||
"\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n"
|
||||
);
|
||||
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
|
||||
});
|
||||
}
|
||||
|
||||
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||
let point = DisplayPoint::new(row as u32, column as u32);
|
||||
point..point
|
||||
|
||||
@@ -3,12 +3,12 @@ use crate::{
|
||||
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
|
||||
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
|
||||
};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result};
|
||||
use collections::HashSet;
|
||||
use futures::future::try_join_all;
|
||||
use gpui::{
|
||||
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, RenderContext,
|
||||
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
elements::*, geometry::vector::vec2f, AppContext, AsyncAppContext, Entity, ModelHandle,
|
||||
RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use language::{
|
||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
|
||||
@@ -72,11 +72,11 @@ impl FollowableItem for Editor {
|
||||
let editor = pane.read_with(&cx, |pane, cx| {
|
||||
let mut editors = pane.items_of_type::<Self>();
|
||||
editors.find(|editor| {
|
||||
editor.remote_id(&client, cx) == Some(remote_id)
|
||||
|| state.singleton
|
||||
&& buffers.len() == 1
|
||||
&& editor.read(cx).buffer.read(cx).as_singleton().as_ref()
|
||||
== Some(&buffers[0])
|
||||
let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
|
||||
let singleton_buffer_matches = state.singleton
|
||||
&& buffers.first()
|
||||
== editor.read(cx).buffer.read(cx).as_singleton().as_ref();
|
||||
ids_match || singleton_buffer_matches
|
||||
})
|
||||
});
|
||||
|
||||
@@ -115,46 +115,29 @@ impl FollowableItem for Editor {
|
||||
multibuffer
|
||||
});
|
||||
|
||||
cx.add_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), cx))
|
||||
cx.add_view(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_multibuffer(multibuffer, Some(project.clone()), cx);
|
||||
editor.remote_id = Some(remote_id);
|
||||
editor
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
editor.remote_id = Some(remote_id);
|
||||
let buffer = editor.buffer.read(cx).read(cx);
|
||||
let selections = state
|
||||
.selections
|
||||
.into_iter()
|
||||
.map(|selection| {
|
||||
deserialize_selection(&buffer, selection)
|
||||
.ok_or_else(|| anyhow!("invalid selection"))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let pending_selection = state
|
||||
.pending_selection
|
||||
.map(|selection| deserialize_selection(&buffer, selection))
|
||||
.flatten();
|
||||
let scroll_top_anchor = state
|
||||
.scroll_top_anchor
|
||||
.and_then(|anchor| deserialize_anchor(&buffer, anchor));
|
||||
drop(buffer);
|
||||
|
||||
if !selections.is_empty() || pending_selection.is_some() {
|
||||
editor.set_selections_from_remote(selections, pending_selection, cx);
|
||||
}
|
||||
|
||||
if let Some(scroll_top_anchor) = scroll_top_anchor {
|
||||
editor.set_scroll_anchor_remote(
|
||||
ScrollAnchor {
|
||||
top_anchor: scroll_top_anchor,
|
||||
offset: vec2f(state.scroll_x, state.scroll_y),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})?;
|
||||
update_editor_from_message(
|
||||
editor.clone(),
|
||||
project,
|
||||
proto::update_view::Editor {
|
||||
selections: state.selections,
|
||||
pending_selection: state.pending_selection,
|
||||
scroll_top_anchor: state.scroll_top_anchor,
|
||||
scroll_x: state.scroll_x,
|
||||
scroll_y: state.scroll_y,
|
||||
..Default::default()
|
||||
},
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(editor)
|
||||
}))
|
||||
@@ -299,96 +282,9 @@ impl FollowableItem for Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let update_view::Variant::Editor(message) = message;
|
||||
let multibuffer = self.buffer.read(cx);
|
||||
let multibuffer = multibuffer.read(cx);
|
||||
|
||||
let buffer_ids = message
|
||||
.inserted_excerpts
|
||||
.iter()
|
||||
.filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let mut removals = message
|
||||
.deleted_excerpts
|
||||
.into_iter()
|
||||
.map(ExcerptId::from_proto)
|
||||
.collect::<Vec<_>>();
|
||||
removals.sort_by(|a, b| a.cmp(&b, &multibuffer));
|
||||
|
||||
let selections = message
|
||||
.selections
|
||||
.into_iter()
|
||||
.filter_map(|selection| deserialize_selection(&multibuffer, selection))
|
||||
.collect::<Vec<_>>();
|
||||
let pending_selection = message
|
||||
.pending_selection
|
||||
.and_then(|selection| deserialize_selection(&multibuffer, selection));
|
||||
|
||||
let scroll_top_anchor = message
|
||||
.scroll_top_anchor
|
||||
.and_then(|anchor| deserialize_anchor(&multibuffer, anchor));
|
||||
drop(multibuffer);
|
||||
|
||||
let buffers = project.update(cx, |project, cx| {
|
||||
buffer_ids
|
||||
.into_iter()
|
||||
.map(|id| project.open_buffer_by_id(id, cx))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
let project = project.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let _buffers = try_join_all(buffers).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.buffer.update(cx, |multibuffer, cx| {
|
||||
let mut insertions = message.inserted_excerpts.into_iter().peekable();
|
||||
while let Some(insertion) = insertions.next() {
|
||||
let Some(excerpt) = insertion.excerpt else { continue };
|
||||
let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue };
|
||||
let buffer_id = excerpt.buffer_id;
|
||||
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue };
|
||||
|
||||
let adjacent_excerpts = iter::from_fn(|| {
|
||||
let insertion = insertions.peek()?;
|
||||
if insertion.previous_excerpt_id.is_none()
|
||||
&& insertion.excerpt.as_ref()?.buffer_id == buffer_id
|
||||
{
|
||||
insertions.next()?.excerpt
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
multibuffer.insert_excerpts_with_ids_after(
|
||||
ExcerptId::from_proto(previous_excerpt_id),
|
||||
buffer,
|
||||
[excerpt]
|
||||
.into_iter()
|
||||
.chain(adjacent_excerpts)
|
||||
.filter_map(|excerpt| {
|
||||
Some((
|
||||
ExcerptId::from_proto(excerpt.id),
|
||||
deserialize_excerpt_range(excerpt)?,
|
||||
))
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
multibuffer.remove_excerpts(removals, cx);
|
||||
});
|
||||
|
||||
if !selections.is_empty() || pending_selection.is_some() {
|
||||
this.set_selections_from_remote(selections, pending_selection, cx);
|
||||
this.request_autoscroll_remotely(Autoscroll::newest(), cx);
|
||||
} else if let Some(anchor) = scroll_top_anchor {
|
||||
this.set_scroll_anchor_remote(ScrollAnchor {
|
||||
top_anchor: anchor,
|
||||
offset: vec2f(message.scroll_x, message.scroll_y)
|
||||
}, cx);
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
update_editor_from_message(this, project, message, &mut cx).await
|
||||
})
|
||||
}
|
||||
|
||||
@@ -402,6 +298,128 @@ impl FollowableItem for Editor {
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_editor_from_message(
|
||||
this: ViewHandle<Editor>,
|
||||
project: ModelHandle<Project>,
|
||||
message: proto::update_view::Editor,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
// Open all of the buffers of which excerpts were added to the editor.
|
||||
let inserted_excerpt_buffer_ids = message
|
||||
.inserted_excerpts
|
||||
.iter()
|
||||
.filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
|
||||
.collect::<HashSet<_>>();
|
||||
let inserted_excerpt_buffers = project.update(cx, |project, cx| {
|
||||
inserted_excerpt_buffer_ids
|
||||
.into_iter()
|
||||
.map(|id| project.open_buffer_by_id(id, cx))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?;
|
||||
|
||||
// Update the editor's excerpts.
|
||||
this.update(cx, |editor, cx| {
|
||||
editor.buffer.update(cx, |multibuffer, cx| {
|
||||
let mut removed_excerpt_ids = message
|
||||
.deleted_excerpts
|
||||
.into_iter()
|
||||
.map(ExcerptId::from_proto)
|
||||
.collect::<Vec<_>>();
|
||||
removed_excerpt_ids.sort_by({
|
||||
let multibuffer = multibuffer.read(cx);
|
||||
move |a, b| a.cmp(&b, &multibuffer)
|
||||
});
|
||||
|
||||
let mut insertions = message.inserted_excerpts.into_iter().peekable();
|
||||
while let Some(insertion) = insertions.next() {
|
||||
let Some(excerpt) = insertion.excerpt else { continue };
|
||||
let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue };
|
||||
let buffer_id = excerpt.buffer_id;
|
||||
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue };
|
||||
|
||||
let adjacent_excerpts = iter::from_fn(|| {
|
||||
let insertion = insertions.peek()?;
|
||||
if insertion.previous_excerpt_id.is_none()
|
||||
&& insertion.excerpt.as_ref()?.buffer_id == buffer_id
|
||||
{
|
||||
insertions.next()?.excerpt
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
multibuffer.insert_excerpts_with_ids_after(
|
||||
ExcerptId::from_proto(previous_excerpt_id),
|
||||
buffer,
|
||||
[excerpt]
|
||||
.into_iter()
|
||||
.chain(adjacent_excerpts)
|
||||
.filter_map(|excerpt| {
|
||||
Some((
|
||||
ExcerptId::from_proto(excerpt.id),
|
||||
deserialize_excerpt_range(excerpt)?,
|
||||
))
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
multibuffer.remove_excerpts(removed_excerpt_ids, cx);
|
||||
});
|
||||
});
|
||||
|
||||
// Deserialize the editor state.
|
||||
let (selections, pending_selection, scroll_top_anchor) = this.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer.read(cx).read(cx);
|
||||
let selections = message
|
||||
.selections
|
||||
.into_iter()
|
||||
.filter_map(|selection| deserialize_selection(&buffer, selection))
|
||||
.collect::<Vec<_>>();
|
||||
let pending_selection = message
|
||||
.pending_selection
|
||||
.and_then(|selection| deserialize_selection(&buffer, selection));
|
||||
let scroll_top_anchor = message
|
||||
.scroll_top_anchor
|
||||
.and_then(|anchor| deserialize_anchor(&buffer, anchor));
|
||||
anyhow::Ok((selections, pending_selection, scroll_top_anchor))
|
||||
})?;
|
||||
|
||||
// Wait until the buffer has received all of the operations referenced by
|
||||
// the editor's new state.
|
||||
this.update(cx, |editor, cx| {
|
||||
editor.buffer.update(cx, |buffer, cx| {
|
||||
buffer.wait_for_anchors(
|
||||
selections
|
||||
.iter()
|
||||
.chain(pending_selection.as_ref())
|
||||
.flat_map(|selection| [selection.start, selection.end])
|
||||
.chain(scroll_top_anchor),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Update the editor's state.
|
||||
this.update(cx, |editor, cx| {
|
||||
if !selections.is_empty() || pending_selection.is_some() {
|
||||
editor.set_selections_from_remote(selections, pending_selection, cx);
|
||||
editor.request_autoscroll_remotely(Autoscroll::newest(), cx);
|
||||
} else if let Some(scroll_top_anchor) = scroll_top_anchor {
|
||||
editor.set_scroll_anchor_remote(
|
||||
ScrollAnchor {
|
||||
top_anchor: scroll_top_anchor,
|
||||
offset: vec2f(message.scroll_x, message.scroll_y),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_excerpt(
|
||||
buffer_id: u64,
|
||||
id: &ExcerptId,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod anchor;
|
||||
|
||||
pub use anchor::{Anchor, AnchorRangeExt};
|
||||
use anyhow::{anyhow, Result};
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, Bound, HashMap, HashSet};
|
||||
use futures::{channel::mpsc, SinkExt};
|
||||
@@ -16,7 +17,9 @@ use language::{
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::{Ref, RefCell},
|
||||
cmp, fmt, io,
|
||||
cmp, fmt,
|
||||
future::Future,
|
||||
io,
|
||||
iter::{self, FromIterator},
|
||||
mem,
|
||||
ops::{Range, RangeBounds, Sub},
|
||||
@@ -1238,6 +1241,39 @@ impl MultiBuffer {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn wait_for_anchors<'a>(
|
||||
&self,
|
||||
anchors: impl 'a + Iterator<Item = Anchor>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> impl 'static + Future<Output = Result<()>> {
|
||||
let borrow = self.buffers.borrow();
|
||||
let mut error = None;
|
||||
let mut futures = Vec::new();
|
||||
for anchor in anchors {
|
||||
if let Some(buffer_id) = anchor.buffer_id {
|
||||
if let Some(buffer) = borrow.get(&buffer_id) {
|
||||
buffer.buffer.update(cx, |buffer, _| {
|
||||
futures.push(buffer.wait_for_anchors([anchor.text_anchor]))
|
||||
});
|
||||
} else {
|
||||
error = Some(anyhow!(
|
||||
"buffer {buffer_id} is not part of this multi-buffer"
|
||||
));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
async move {
|
||||
if let Some(error) = error {
|
||||
Err(error)?;
|
||||
}
|
||||
for future in futures {
|
||||
future.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text_anchor_for_position<T: ToOffset>(
|
||||
&self,
|
||||
position: T,
|
||||
|
||||
@@ -523,31 +523,7 @@ impl FakeFs {
|
||||
}
|
||||
|
||||
pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
|
||||
let mut state = self.state.lock();
|
||||
let path = path.as_ref();
|
||||
let inode = state.next_inode;
|
||||
let mtime = state.next_mtime;
|
||||
state.next_inode += 1;
|
||||
state.next_mtime += Duration::from_nanos(1);
|
||||
let file = Arc::new(Mutex::new(FakeFsEntry::File {
|
||||
inode,
|
||||
mtime,
|
||||
content,
|
||||
}));
|
||||
state
|
||||
.write_path(path, move |entry| {
|
||||
match entry {
|
||||
btree_map::Entry::Vacant(e) => {
|
||||
e.insert(file);
|
||||
}
|
||||
btree_map::Entry::Occupied(mut e) => {
|
||||
*e.get_mut() = file;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
state.emit_event(&[path]);
|
||||
self.write_file_internal(path, content).unwrap()
|
||||
}
|
||||
|
||||
pub async fn insert_symlink(&self, path: impl AsRef<Path>, target: PathBuf) {
|
||||
@@ -569,6 +545,33 @@ impl FakeFs {
|
||||
state.emit_event(&[path]);
|
||||
}
|
||||
|
||||
fn write_file_internal(&self, path: impl AsRef<Path>, content: String) -> Result<()> {
|
||||
let mut state = self.state.lock();
|
||||
let path = path.as_ref();
|
||||
let inode = state.next_inode;
|
||||
let mtime = state.next_mtime;
|
||||
state.next_inode += 1;
|
||||
state.next_mtime += Duration::from_nanos(1);
|
||||
let file = Arc::new(Mutex::new(FakeFsEntry::File {
|
||||
inode,
|
||||
mtime,
|
||||
content,
|
||||
}));
|
||||
state.write_path(path, move |entry| {
|
||||
match entry {
|
||||
btree_map::Entry::Vacant(e) => {
|
||||
e.insert(file);
|
||||
}
|
||||
btree_map::Entry::Occupied(mut e) => {
|
||||
*e.get_mut() = file;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
state.emit_event(&[path]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn pause_events(&self) {
|
||||
self.state.lock().events_paused = true;
|
||||
}
|
||||
@@ -952,7 +955,7 @@ impl Fs for FakeFs {
|
||||
async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
|
||||
self.simulate_random_delay().await;
|
||||
let path = normalize_path(path.as_path());
|
||||
self.insert_file(path, data.to_string()).await;
|
||||
self.write_file_internal(path, data.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -961,7 +964,7 @@ impl Fs for FakeFs {
|
||||
self.simulate_random_delay().await;
|
||||
let path = normalize_path(path);
|
||||
let content = chunks(text, line_ending).collect();
|
||||
self.insert_file(path, content).await;
|
||||
self.write_file_internal(path, content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1313,10 +1313,10 @@ impl Buffer {
|
||||
self.text.wait_for_edits(edit_ids)
|
||||
}
|
||||
|
||||
pub fn wait_for_anchors<'a>(
|
||||
pub fn wait_for_anchors(
|
||||
&mut self,
|
||||
anchors: impl IntoIterator<Item = &'a Anchor>,
|
||||
) -> impl Future<Output = Result<()>> {
|
||||
anchors: impl IntoIterator<Item = Anchor>,
|
||||
) -> impl 'static + Future<Output = Result<()>> {
|
||||
self.text.wait_for_anchors(anchors)
|
||||
}
|
||||
|
||||
|
||||
@@ -572,7 +572,7 @@ async fn location_links_from_proto(
|
||||
.and_then(deserialize_anchor)
|
||||
.ok_or_else(|| anyhow!("missing origin end"))?;
|
||||
buffer
|
||||
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
|
||||
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
|
||||
.await?;
|
||||
Some(Location {
|
||||
buffer,
|
||||
@@ -597,7 +597,7 @@ async fn location_links_from_proto(
|
||||
.and_then(deserialize_anchor)
|
||||
.ok_or_else(|| anyhow!("missing target end"))?;
|
||||
buffer
|
||||
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
|
||||
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
|
||||
.await?;
|
||||
let target = Location {
|
||||
buffer,
|
||||
@@ -868,7 +868,7 @@ impl LspCommand for GetReferences {
|
||||
.and_then(deserialize_anchor)
|
||||
.ok_or_else(|| anyhow!("missing target end"))?;
|
||||
target_buffer
|
||||
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
|
||||
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
|
||||
.await?;
|
||||
locations.push(Location {
|
||||
buffer: target_buffer,
|
||||
@@ -1012,7 +1012,7 @@ impl LspCommand for GetDocumentHighlights {
|
||||
.and_then(deserialize_anchor)
|
||||
.ok_or_else(|| anyhow!("missing target end"))?;
|
||||
buffer
|
||||
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
|
||||
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
|
||||
.await?;
|
||||
let kind = match proto::document_highlight::Kind::from_i32(highlight.kind) {
|
||||
Some(proto::document_highlight::Kind::Text) => DocumentHighlightKind::TEXT,
|
||||
|
||||
@@ -1733,13 +1733,19 @@ impl Project {
|
||||
|
||||
async fn send_buffer_messages(
|
||||
this: WeakModelHandle<Self>,
|
||||
mut rx: UnboundedReceiver<BufferMessage>,
|
||||
rx: UnboundedReceiver<BufferMessage>,
|
||||
mut cx: AsyncAppContext,
|
||||
) {
|
||||
) -> Option<()> {
|
||||
const MAX_BATCH_SIZE: usize = 128;
|
||||
|
||||
let mut needs_resync_with_host = false;
|
||||
while let Some(change) = rx.next().await {
|
||||
if let Some(this) = this.upgrade(&mut cx) {
|
||||
let is_local = this.read_with(&cx, |this, _| this.is_local());
|
||||
let mut operations_by_buffer_id = HashMap::default();
|
||||
let mut changes = rx.ready_chunks(MAX_BATCH_SIZE);
|
||||
while let Some(changes) = changes.next().await {
|
||||
let this = this.upgrade(&mut cx)?;
|
||||
let is_local = this.read_with(&cx, |this, _| this.is_local());
|
||||
|
||||
for change in changes {
|
||||
match change {
|
||||
BufferMessage::Operation {
|
||||
buffer_id,
|
||||
@@ -1748,21 +1754,14 @@ impl Project {
|
||||
if needs_resync_with_host {
|
||||
continue;
|
||||
}
|
||||
let request = this.read_with(&cx, |this, _| {
|
||||
let project_id = this.remote_id()?;
|
||||
Some(this.client.request(proto::UpdateBuffer {
|
||||
buffer_id,
|
||||
project_id,
|
||||
operations: vec![operation],
|
||||
}))
|
||||
});
|
||||
if let Some(request) = request {
|
||||
if request.await.is_err() && !is_local {
|
||||
needs_resync_with_host = true;
|
||||
}
|
||||
}
|
||||
|
||||
operations_by_buffer_id
|
||||
.entry(buffer_id)
|
||||
.or_insert(Vec::new())
|
||||
.push(operation);
|
||||
}
|
||||
BufferMessage::Resync => {
|
||||
operations_by_buffer_id.clear();
|
||||
if this
|
||||
.update(&mut cx, |this, cx| this.synchronize_remote_buffers(cx))
|
||||
.await
|
||||
@@ -1772,10 +1771,27 @@ impl Project {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
for (buffer_id, operations) in operations_by_buffer_id.drain() {
|
||||
let request = this.read_with(&cx, |this, _| {
|
||||
let project_id = this.remote_id()?;
|
||||
Some(this.client.request(proto::UpdateBuffer {
|
||||
buffer_id,
|
||||
project_id,
|
||||
operations,
|
||||
}))
|
||||
});
|
||||
if let Some(request) = request {
|
||||
if request.await.is_err() && !is_local {
|
||||
needs_resync_with_host = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn on_buffer_event(
|
||||
|
||||
@@ -2183,7 +2183,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_save_file(cx: &mut gpui::TestAppContext) {
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -154,6 +154,12 @@ impl<K> TreeSet<K>
|
||||
where
|
||||
K: Clone + Debug + Default + Ord,
|
||||
{
|
||||
pub fn from_ordered_entries(entries: impl IntoIterator<Item = K>) -> Self {
|
||||
Self(TreeMap::from_ordered_entries(
|
||||
entries.into_iter().map(|key| (key, ())),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: K) {
|
||||
self.0.insert(key, ());
|
||||
}
|
||||
|
||||
@@ -1331,15 +1331,15 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wait_for_anchors<'a>(
|
||||
pub fn wait_for_anchors(
|
||||
&mut self,
|
||||
anchors: impl IntoIterator<Item = &'a Anchor>,
|
||||
anchors: impl IntoIterator<Item = Anchor>,
|
||||
) -> impl 'static + Future<Output = Result<()>> {
|
||||
let mut futures = Vec::new();
|
||||
for anchor in anchors {
|
||||
if !self.version.observed(anchor.timestamp)
|
||||
&& *anchor != Anchor::MAX
|
||||
&& *anchor != Anchor::MIN
|
||||
&& anchor != Anchor::MAX
|
||||
&& anchor != Anchor::MIN
|
||||
{
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.edit_id_resolvers
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.82.0"
|
||||
version = "0.82.6"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
preview
|
||||
Reference in New Issue
Block a user