There's still a bit more work to do on this, but this PR is compiling (with warnings) after eliminating the key types. When the tasks below are complete, this will be the new narrative for GPUI: - `Entity<T>` - This replaces `View<T>`/`Model<T>`. It represents a unit of state, and if `T` implements `Render`, then `Entity<T>` implements `Element`. - `&mut App` This replaces `AppContext` and represents the app. - `&mut Context<T>` This replaces `ModelContext` and derefs to `App`. It is provided by the framework when updating an entity. - `&mut Window` Broken out of `&mut WindowContext` which no longer exists. Every method that once took `&mut WindowContext` now takes `&mut Window, &mut App` and every method that took `&mut ViewContext<T>` now takes `&mut Window, &mut Context<T>` Not pictured here are the two other failed attempts. It's been quite a month! Tasks: - [x] Remove `View`, `ViewContext`, `WindowContext` and thread through `Window` - [x] [@cole-miller @mikayla-maki] Redraw window when entities change - [x] [@cole-miller @mikayla-maki] Get examples and Zed running - [x] [@cole-miller @mikayla-maki] Fix Zed rendering - [x] [@mikayla-maki] Fix todo! macros and comments - [x] Fix a bug where the editor would not be redrawn because of view caching - [x] remove publicness window.notify() and replace with `AppContext::notify` - [x] remove `observe_new_window_models`, replace with `observe_new_models` with an optional window - [x] Fix a bug where the project panel would not be redrawn because of the wrong refresh() call being used - [x] Fix the tests - [x] Fix warnings by eliminating `Window` params or using `_` - [x] Fix conflicts - [x] Simplify generic code where possible - [x] Rename types - [ ] Update docs ### issues post merge - [x] Issues switching between normal and insert mode - [x] Assistant re-rendering failure - [x] Vim test failures - [x] Mac build issue Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra <me@as-cii.com> Co-authored-by: Cole Miller <cole@zed.dev> Co-authored-by: Mikayla <mikayla@zed.dev> Co-authored-by: Joseph <joseph@zed.dev> Co-authored-by: max <max@zed.dev> Co-authored-by: Michael Sloan <michael@zed.dev> Co-authored-by: Mikayla Maki <mikaylamaki@Mikaylas-MacBook-Pro.local> Co-authored-by: Mikayla <mikayla.c.maki@gmail.com> Co-authored-by: joão <joao@zed.dev>
523 lines
17 KiB
Rust
523 lines
17 KiB
Rust
use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SemanticsProvider};
|
|
use collections::HashSet;
|
|
use futures::{channel::mpsc, future::join_all};
|
|
use gpui::{App, Entity, EventEmitter, Focusable, Render, Subscription, Task};
|
|
use language::{Buffer, BufferEvent, Capability};
|
|
use multi_buffer::{ExcerptRange, MultiBuffer};
|
|
use project::{buffer_store::BufferChangeSet, Project};
|
|
use smol::stream::StreamExt;
|
|
use std::{any::TypeId, ops::Range, rc::Rc, time::Duration};
|
|
use text::ToOffset;
|
|
use ui::{prelude::*, ButtonLike, KeyBinding};
|
|
use workspace::{
|
|
searchable::SearchableItemHandle, Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation,
|
|
ToolbarItemView, Workspace,
|
|
};
|
|
|
|
pub struct ProposedChangesEditor {
|
|
editor: Entity<Editor>,
|
|
multibuffer: Entity<MultiBuffer>,
|
|
title: SharedString,
|
|
buffer_entries: Vec<BufferEntry>,
|
|
_recalculate_diffs_task: Task<Option<()>>,
|
|
recalculate_diffs_tx: mpsc::UnboundedSender<RecalculateDiff>,
|
|
}
|
|
|
|
pub struct ProposedChangeLocation<T> {
|
|
pub buffer: Entity<Buffer>,
|
|
pub ranges: Vec<Range<T>>,
|
|
}
|
|
|
|
struct BufferEntry {
|
|
base: Entity<Buffer>,
|
|
branch: Entity<Buffer>,
|
|
_subscription: Subscription,
|
|
}
|
|
|
|
pub struct ProposedChangesEditorToolbar {
|
|
current_editor: Option<Entity<ProposedChangesEditor>>,
|
|
}
|
|
|
|
struct RecalculateDiff {
|
|
buffer: Entity<Buffer>,
|
|
debounce: bool,
|
|
}
|
|
|
|
/// A provider of code semantics for branch buffers.
|
|
///
|
|
/// Requests in edited regions will return nothing, but requests in unchanged
|
|
/// regions will be translated into the base buffer's coordinates.
|
|
struct BranchBufferSemanticsProvider(Rc<dyn SemanticsProvider>);
|
|
|
|
impl ProposedChangesEditor {
|
|
pub fn new<T: ToOffset>(
|
|
title: impl Into<SharedString>,
|
|
locations: Vec<ProposedChangeLocation<T>>,
|
|
project: Option<Entity<Project>>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Self {
|
|
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
|
let (recalculate_diffs_tx, mut recalculate_diffs_rx) = mpsc::unbounded();
|
|
let mut this = Self {
|
|
editor: cx.new(|cx| {
|
|
let mut editor =
|
|
Editor::for_multibuffer(multibuffer.clone(), project, true, window, cx);
|
|
editor.set_expand_all_diff_hunks(cx);
|
|
editor.set_completion_provider(None);
|
|
editor.clear_code_action_providers();
|
|
editor.set_semantics_provider(
|
|
editor
|
|
.semantics_provider()
|
|
.map(|provider| Rc::new(BranchBufferSemanticsProvider(provider)) as _),
|
|
);
|
|
editor
|
|
}),
|
|
multibuffer,
|
|
title: title.into(),
|
|
buffer_entries: Vec::new(),
|
|
recalculate_diffs_tx,
|
|
_recalculate_diffs_task: cx.spawn_in(window, |this, mut cx| async move {
|
|
let mut buffers_to_diff = HashSet::default();
|
|
while let Some(mut recalculate_diff) = recalculate_diffs_rx.next().await {
|
|
buffers_to_diff.insert(recalculate_diff.buffer);
|
|
|
|
while recalculate_diff.debounce {
|
|
cx.background_executor()
|
|
.timer(Duration::from_millis(50))
|
|
.await;
|
|
let mut had_further_changes = false;
|
|
while let Ok(next_recalculate_diff) = recalculate_diffs_rx.try_next() {
|
|
let next_recalculate_diff = next_recalculate_diff?;
|
|
recalculate_diff.debounce &= next_recalculate_diff.debounce;
|
|
buffers_to_diff.insert(next_recalculate_diff.buffer);
|
|
had_further_changes = true;
|
|
}
|
|
if !had_further_changes {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let recalculate_diff_futures = this
|
|
.update(&mut cx, |this, cx| {
|
|
buffers_to_diff
|
|
.drain()
|
|
.filter_map(|buffer| {
|
|
let buffer = buffer.read(cx);
|
|
let base_buffer = buffer.base_buffer()?;
|
|
let buffer = buffer.text_snapshot();
|
|
let change_set = this
|
|
.multibuffer
|
|
.read(cx)
|
|
.change_set_for(buffer.remote_id())?;
|
|
Some(change_set.update(cx, |change_set, cx| {
|
|
change_set.set_base_text(
|
|
base_buffer.read(cx).text(),
|
|
buffer,
|
|
cx,
|
|
)
|
|
}))
|
|
})
|
|
.collect::<Vec<_>>()
|
|
})
|
|
.ok()?;
|
|
|
|
join_all(recalculate_diff_futures).await;
|
|
}
|
|
None
|
|
}),
|
|
};
|
|
this.reset_locations(locations, window, cx);
|
|
this
|
|
}
|
|
|
|
pub fn branch_buffer_for_base(&self, base_buffer: &Entity<Buffer>) -> Option<Entity<Buffer>> {
|
|
self.buffer_entries.iter().find_map(|entry| {
|
|
if &entry.base == base_buffer {
|
|
Some(entry.branch.clone())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn set_title(&mut self, title: SharedString, cx: &mut Context<Self>) {
|
|
self.title = title;
|
|
cx.notify();
|
|
}
|
|
|
|
pub fn reset_locations<T: ToOffset>(
|
|
&mut self,
|
|
locations: Vec<ProposedChangeLocation<T>>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
// Undo all branch changes
|
|
for entry in &self.buffer_entries {
|
|
let base_version = entry.base.read(cx).version();
|
|
entry.branch.update(cx, |buffer, cx| {
|
|
let undo_counts = buffer
|
|
.operations()
|
|
.iter()
|
|
.filter_map(|(timestamp, _)| {
|
|
if !base_version.observed(*timestamp) {
|
|
Some((*timestamp, u32::MAX))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
buffer.undo_operations(undo_counts, cx);
|
|
});
|
|
}
|
|
|
|
self.multibuffer.update(cx, |multibuffer, cx| {
|
|
multibuffer.clear(cx);
|
|
});
|
|
|
|
let mut buffer_entries = Vec::new();
|
|
let mut new_change_sets = Vec::new();
|
|
for location in locations {
|
|
let branch_buffer;
|
|
if let Some(ix) = self
|
|
.buffer_entries
|
|
.iter()
|
|
.position(|entry| entry.base == location.buffer)
|
|
{
|
|
let entry = self.buffer_entries.remove(ix);
|
|
branch_buffer = entry.branch.clone();
|
|
buffer_entries.push(entry);
|
|
} else {
|
|
branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx));
|
|
new_change_sets.push(cx.new(|cx| {
|
|
let mut change_set = BufferChangeSet::new(&branch_buffer, cx);
|
|
let _ = change_set.set_base_text(
|
|
location.buffer.read(cx).text(),
|
|
branch_buffer.read(cx).text_snapshot(),
|
|
cx,
|
|
);
|
|
change_set
|
|
}));
|
|
buffer_entries.push(BufferEntry {
|
|
branch: branch_buffer.clone(),
|
|
base: location.buffer.clone(),
|
|
_subscription: cx.subscribe(&branch_buffer, Self::on_buffer_event),
|
|
});
|
|
}
|
|
|
|
self.multibuffer.update(cx, |multibuffer, cx| {
|
|
multibuffer.push_excerpts(
|
|
branch_buffer,
|
|
location.ranges.into_iter().map(|range| ExcerptRange {
|
|
context: range,
|
|
primary: None,
|
|
}),
|
|
cx,
|
|
);
|
|
});
|
|
}
|
|
|
|
self.buffer_entries = buffer_entries;
|
|
self.editor.update(cx, |editor, cx| {
|
|
editor.change_selections(None, window, cx, |selections| selections.refresh());
|
|
editor.buffer.update(cx, |buffer, cx| {
|
|
for change_set in new_change_sets {
|
|
buffer.add_change_set(change_set, cx)
|
|
}
|
|
})
|
|
});
|
|
}
|
|
|
|
pub fn recalculate_all_buffer_diffs(&self) {
|
|
for (ix, entry) in self.buffer_entries.iter().enumerate().rev() {
|
|
self.recalculate_diffs_tx
|
|
.unbounded_send(RecalculateDiff {
|
|
buffer: entry.branch.clone(),
|
|
debounce: ix > 0,
|
|
})
|
|
.ok();
|
|
}
|
|
}
|
|
|
|
fn on_buffer_event(
|
|
&mut self,
|
|
buffer: Entity<Buffer>,
|
|
event: &BufferEvent,
|
|
_cx: &mut Context<Self>,
|
|
) {
|
|
match event {
|
|
BufferEvent::Operation { .. } => {
|
|
self.recalculate_diffs_tx
|
|
.unbounded_send(RecalculateDiff {
|
|
buffer,
|
|
debounce: true,
|
|
})
|
|
.ok();
|
|
}
|
|
// BufferEvent::DiffBaseChanged => {
|
|
// self.recalculate_diffs_tx
|
|
// .unbounded_send(RecalculateDiff {
|
|
// buffer,
|
|
// debounce: false,
|
|
// })
|
|
// .ok();
|
|
// }
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Render for ProposedChangesEditor {
|
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
|
div()
|
|
.size_full()
|
|
.key_context("ProposedChangesEditor")
|
|
.child(self.editor.clone())
|
|
}
|
|
}
|
|
|
|
impl Focusable for ProposedChangesEditor {
|
|
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
|
|
self.editor.focus_handle(cx)
|
|
}
|
|
}
|
|
|
|
impl EventEmitter<EditorEvent> for ProposedChangesEditor {}
|
|
|
|
impl Item for ProposedChangesEditor {
|
|
type Event = EditorEvent;
|
|
|
|
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
|
|
Some(Icon::new(IconName::Diff))
|
|
}
|
|
|
|
fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
|
|
Some(self.title.clone())
|
|
}
|
|
|
|
fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
|
Some(Box::new(self.editor.clone()))
|
|
}
|
|
|
|
fn act_as_type<'a>(
|
|
&'a self,
|
|
type_id: TypeId,
|
|
self_handle: &'a Entity<Self>,
|
|
_: &'a App,
|
|
) -> Option<gpui::AnyView> {
|
|
if type_id == TypeId::of::<Self>() {
|
|
Some(self_handle.to_any())
|
|
} else if type_id == TypeId::of::<Editor>() {
|
|
Some(self.editor.to_any())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn added_to_workspace(
|
|
&mut self,
|
|
workspace: &mut Workspace,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.editor.update(cx, |editor, cx| {
|
|
Item::added_to_workspace(editor, workspace, window, cx)
|
|
});
|
|
}
|
|
|
|
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.editor
|
|
.update(cx, |editor, cx| editor.deactivated(window, cx));
|
|
}
|
|
|
|
fn navigate(
|
|
&mut self,
|
|
data: Box<dyn std::any::Any>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> bool {
|
|
self.editor
|
|
.update(cx, |editor, cx| Item::navigate(editor, data, window, cx))
|
|
}
|
|
|
|
fn set_nav_history(
|
|
&mut self,
|
|
nav_history: workspace::ItemNavHistory,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.editor.update(cx, |editor, cx| {
|
|
Item::set_nav_history(editor, nav_history, window, cx)
|
|
});
|
|
}
|
|
|
|
fn can_save(&self, cx: &App) -> bool {
|
|
self.editor.read(cx).can_save(cx)
|
|
}
|
|
|
|
fn save(
|
|
&mut self,
|
|
format: bool,
|
|
project: Entity<Project>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Task<gpui::Result<()>> {
|
|
self.editor.update(cx, |editor, cx| {
|
|
Item::save(editor, format, project, window, cx)
|
|
})
|
|
}
|
|
}
|
|
|
|
impl ProposedChangesEditorToolbar {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
current_editor: None,
|
|
}
|
|
}
|
|
|
|
fn get_toolbar_item_location(&self) -> ToolbarItemLocation {
|
|
if self.current_editor.is_some() {
|
|
ToolbarItemLocation::PrimaryRight
|
|
} else {
|
|
ToolbarItemLocation::Hidden
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Render for ProposedChangesEditorToolbar {
|
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let button_like = ButtonLike::new("apply-changes").child(Label::new("Apply All"));
|
|
|
|
match &self.current_editor {
|
|
Some(editor) => {
|
|
let focus_handle = editor.focus_handle(cx);
|
|
let keybinding =
|
|
KeyBinding::for_action_in(&ApplyAllDiffHunks, &focus_handle, window)
|
|
.map(|binding| binding.into_any_element());
|
|
|
|
button_like.children(keybinding).on_click({
|
|
move |_event, window, cx| {
|
|
focus_handle.dispatch_action(&ApplyAllDiffHunks, window, cx)
|
|
}
|
|
})
|
|
}
|
|
None => button_like.disabled(true),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl EventEmitter<ToolbarItemEvent> for ProposedChangesEditorToolbar {}
|
|
|
|
impl ToolbarItemView for ProposedChangesEditorToolbar {
|
|
fn set_active_pane_item(
|
|
&mut self,
|
|
active_pane_item: Option<&dyn workspace::ItemHandle>,
|
|
_window: &mut Window,
|
|
_cx: &mut Context<Self>,
|
|
) -> workspace::ToolbarItemLocation {
|
|
self.current_editor =
|
|
active_pane_item.and_then(|item| item.downcast::<ProposedChangesEditor>());
|
|
self.get_toolbar_item_location()
|
|
}
|
|
}
|
|
|
|
impl BranchBufferSemanticsProvider {
|
|
fn to_base(
|
|
&self,
|
|
buffer: &Entity<Buffer>,
|
|
positions: &[text::Anchor],
|
|
cx: &App,
|
|
) -> Option<Entity<Buffer>> {
|
|
let base_buffer = buffer.read(cx).base_buffer()?;
|
|
let version = base_buffer.read(cx).version();
|
|
if positions
|
|
.iter()
|
|
.any(|position| !version.observed(position.timestamp))
|
|
{
|
|
return None;
|
|
}
|
|
Some(base_buffer)
|
|
}
|
|
}
|
|
|
|
impl SemanticsProvider for BranchBufferSemanticsProvider {
|
|
fn hover(
|
|
&self,
|
|
buffer: &Entity<Buffer>,
|
|
position: text::Anchor,
|
|
cx: &mut App,
|
|
) -> Option<Task<Vec<project::Hover>>> {
|
|
let buffer = self.to_base(buffer, &[position], cx)?;
|
|
self.0.hover(&buffer, position, cx)
|
|
}
|
|
|
|
fn inlay_hints(
|
|
&self,
|
|
buffer: Entity<Buffer>,
|
|
range: Range<text::Anchor>,
|
|
cx: &mut App,
|
|
) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
|
|
let buffer = self.to_base(&buffer, &[range.start, range.end], cx)?;
|
|
self.0.inlay_hints(buffer, range, cx)
|
|
}
|
|
|
|
fn resolve_inlay_hint(
|
|
&self,
|
|
hint: project::InlayHint,
|
|
buffer: Entity<Buffer>,
|
|
server_id: lsp::LanguageServerId,
|
|
cx: &mut App,
|
|
) -> Option<Task<anyhow::Result<project::InlayHint>>> {
|
|
let buffer = self.to_base(&buffer, &[], cx)?;
|
|
self.0.resolve_inlay_hint(hint, buffer, server_id, cx)
|
|
}
|
|
|
|
fn supports_inlay_hints(&self, buffer: &Entity<Buffer>, cx: &App) -> bool {
|
|
if let Some(buffer) = self.to_base(&buffer, &[], cx) {
|
|
self.0.supports_inlay_hints(&buffer, cx)
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
fn document_highlights(
|
|
&self,
|
|
buffer: &Entity<Buffer>,
|
|
position: text::Anchor,
|
|
cx: &mut App,
|
|
) -> Option<Task<gpui::Result<Vec<project::DocumentHighlight>>>> {
|
|
let buffer = self.to_base(&buffer, &[position], cx)?;
|
|
self.0.document_highlights(&buffer, position, cx)
|
|
}
|
|
|
|
fn definitions(
|
|
&self,
|
|
buffer: &Entity<Buffer>,
|
|
position: text::Anchor,
|
|
kind: crate::GotoDefinitionKind,
|
|
cx: &mut App,
|
|
) -> Option<Task<gpui::Result<Vec<project::LocationLink>>>> {
|
|
let buffer = self.to_base(&buffer, &[position], cx)?;
|
|
self.0.definitions(&buffer, position, kind, cx)
|
|
}
|
|
|
|
fn range_for_rename(
|
|
&self,
|
|
_: &Entity<Buffer>,
|
|
_: text::Anchor,
|
|
_: &mut App,
|
|
) -> Option<Task<gpui::Result<Option<Range<text::Anchor>>>>> {
|
|
None
|
|
}
|
|
|
|
fn perform_rename(
|
|
&self,
|
|
_: &Entity<Buffer>,
|
|
_: text::Anchor,
|
|
_: String,
|
|
_: &mut App,
|
|
) -> Option<Task<gpui::Result<project::ProjectTransaction>>> {
|
|
None
|
|
}
|
|
}
|