Compare commits
12 Commits
v0.218.x
...
v0.210.2-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9844631b27 | ||
|
|
1d06e26b92 | ||
|
|
282b7b4309 | ||
|
|
fade12155d | ||
|
|
18f31e8bf9 | ||
|
|
780d60bc9d | ||
|
|
08ba1939c6 | ||
|
|
c4858f8ae6 | ||
|
|
f3768cfc40 | ||
|
|
80d1551671 | ||
|
|
00a14349de | ||
|
|
aa8128e2ff |
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -14156,7 +14156,6 @@ dependencies = [
|
||||
"log",
|
||||
"rand 0.9.2",
|
||||
"rayon",
|
||||
"smallvec",
|
||||
"sum_tree",
|
||||
"unicode-segmentation",
|
||||
"util",
|
||||
@@ -20918,7 +20917,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.210.0"
|
||||
version = "0.210.2"
|
||||
dependencies = [
|
||||
"acp_tools",
|
||||
"activity_indicator",
|
||||
|
||||
@@ -539,7 +539,7 @@
|
||||
"ctrl-k ctrl-0": "editor::FoldAll",
|
||||
"ctrl-k ctrl-j": "editor::UnfoldAll",
|
||||
"ctrl-space": "editor::ShowCompletions",
|
||||
"ctrl-shift-space": "editor::ShowSignatureHelp",
|
||||
"ctrl-shift-space": "editor::ShowWordCompletions",
|
||||
"ctrl-.": "editor::ToggleCodeActions",
|
||||
"ctrl-k r": "editor::RevealInFileManager",
|
||||
"ctrl-k p": "editor::CopyPath",
|
||||
@@ -799,7 +799,7 @@
|
||||
"ctrl-shift-e": "pane::RevealInProjectPanel",
|
||||
"ctrl-f8": "editor::GoToHunk",
|
||||
"ctrl-shift-f8": "editor::GoToPreviousHunk",
|
||||
"ctrl-i": "assistant::InlineAssist",
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
"ctrl-:": "editor::ToggleInlayHints"
|
||||
}
|
||||
},
|
||||
@@ -1094,7 +1094,7 @@
|
||||
"paste": "terminal::Paste",
|
||||
"shift-insert": "terminal::Paste",
|
||||
"ctrl-shift-v": "terminal::Paste",
|
||||
"ctrl-i": "assistant::InlineAssist",
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
"alt-b": ["terminal::SendText", "\u001bb"],
|
||||
"alt-f": ["terminal::SendText", "\u001bf"],
|
||||
"alt-.": ["terminal::SendText", "\u001b."],
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
"cmd-\"": "editor::ExpandAllDiffHunks",
|
||||
"cmd-alt-g b": "git::Blame",
|
||||
"cmd-alt-g m": "git::OpenModifiedFiles",
|
||||
"cmd-shift-space": "editor::ShowSignatureHelp",
|
||||
"cmd-i": "editor::ShowSignatureHelp",
|
||||
"f9": "editor::ToggleBreakpoint",
|
||||
"shift-f9": "editor::EditLogBreakpoint",
|
||||
"ctrl-f12": "editor::GoToDeclaration",
|
||||
@@ -864,7 +864,7 @@
|
||||
"cmd-shift-e": "pane::RevealInProjectPanel",
|
||||
"cmd-f8": "editor::GoToHunk",
|
||||
"cmd-shift-f8": "editor::GoToPreviousHunk",
|
||||
"cmd-i": "assistant::InlineAssist",
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
"ctrl-:": "editor::ToggleInlayHints"
|
||||
}
|
||||
},
|
||||
@@ -1168,7 +1168,7 @@
|
||||
"cmd-a": "editor::SelectAll",
|
||||
"cmd-k": "terminal::Clear",
|
||||
"cmd-n": "workspace::NewTerminal",
|
||||
"cmd-i": "assistant::InlineAssist",
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
"ctrl-_": null, // emacs undo
|
||||
// Some nice conveniences
|
||||
"cmd-backspace": ["terminal::SendText", "\u0015"], // ctrl-u: clear line
|
||||
|
||||
@@ -548,7 +548,7 @@
|
||||
"ctrl-k ctrl-0": "editor::FoldAll",
|
||||
"ctrl-k ctrl-j": "editor::UnfoldAll",
|
||||
"ctrl-space": "editor::ShowCompletions",
|
||||
"ctrl-shift-space": "editor::ShowSignatureHelp",
|
||||
"ctrl-shift-space": "editor::ShowWordCompletions",
|
||||
"ctrl-.": "editor::ToggleCodeActions",
|
||||
"ctrl-k r": "editor::RevealInFileManager",
|
||||
"ctrl-k p": "editor::CopyPath",
|
||||
@@ -812,7 +812,7 @@
|
||||
"ctrl-shift-e": "pane::RevealInProjectPanel",
|
||||
"ctrl-f8": "editor::GoToHunk",
|
||||
"ctrl-shift-f8": "editor::GoToPreviousHunk",
|
||||
"ctrl-i": "assistant::InlineAssist",
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
"ctrl-shift-;": "editor::ToggleInlayHints"
|
||||
}
|
||||
},
|
||||
@@ -1120,7 +1120,7 @@
|
||||
"shift-insert": "terminal::Paste",
|
||||
"ctrl-v": "terminal::Paste",
|
||||
"ctrl-shift-v": "terminal::Paste",
|
||||
"ctrl-i": "assistant::InlineAssist",
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
"alt-b": ["terminal::SendText", "\u001bb"],
|
||||
"alt-f": ["terminal::SendText", "\u001bf"],
|
||||
"alt-.": ["terminal::SendText", "\u001b."],
|
||||
|
||||
@@ -581,13 +581,11 @@ impl Item for AgentDiffPane {
|
||||
_workspace_id: Option<workspace::WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Self>>>
|
||||
) -> Option<Entity<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Task::ready(Some(cx.new(|cx| {
|
||||
Self::new(self.thread.clone(), self.workspace.clone(), window, cx)
|
||||
})))
|
||||
Some(cx.new(|cx| Self::new(self.thread.clone(), self.workspace.clone(), window, cx)))
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &App) -> bool {
|
||||
|
||||
@@ -776,30 +776,26 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
.unwrap();
|
||||
|
||||
// Clients A and B follow each other in split panes
|
||||
workspace_a
|
||||
.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.split_and_clone(
|
||||
workspace.active_pane().clone(),
|
||||
SplitDirection::Right,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.split_and_clone(
|
||||
workspace.active_pane().clone(),
|
||||
SplitDirection::Right,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.follow(client_b.peer_id().unwrap(), window, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
workspace_b
|
||||
.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.split_and_clone(
|
||||
workspace.active_pane().clone(),
|
||||
SplitDirection::Right,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.split_and_clone(
|
||||
workspace.active_pane().clone(),
|
||||
SplitDirection::Right,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.follow(client_a.peer_id().unwrap(), window, cx)
|
||||
});
|
||||
@@ -1373,11 +1369,9 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
|
||||
);
|
||||
|
||||
// When client B activates a different pane, it continues following client A in the original pane.
|
||||
workspace_b
|
||||
.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, window, cx)
|
||||
})
|
||||
.await;
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, window, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
Some(leader_id.into())
|
||||
|
||||
@@ -6748,7 +6748,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.split(workspace::SplitDirection::Right, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
let right_pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
pane.update(cx, |pane, cx| {
|
||||
|
||||
@@ -498,8 +498,8 @@ impl Item for ChannelView {
|
||||
_: Option<WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Self>>> {
|
||||
Task::ready(Some(cx.new(|cx| {
|
||||
) -> Option<Entity<Self>> {
|
||||
Some(cx.new(|cx| {
|
||||
Self::new(
|
||||
self.project.clone(),
|
||||
self.workspace.clone(),
|
||||
@@ -508,7 +508,7 @@ impl Item for ChannelView {
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})))
|
||||
}))
|
||||
}
|
||||
|
||||
fn navigate(
|
||||
|
||||
@@ -693,11 +693,11 @@ impl Item for BufferDiagnosticsEditor {
|
||||
_workspace_id: Option<workspace::WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Self>>>
|
||||
) -> Option<Entity<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Task::ready(Some(cx.new(|cx| {
|
||||
Some(cx.new(|cx| {
|
||||
BufferDiagnosticsEditor::new(
|
||||
self.project_path.clone(),
|
||||
self.project.clone(),
|
||||
@@ -706,7 +706,7 @@ impl Item for BufferDiagnosticsEditor {
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})))
|
||||
}))
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
|
||||
@@ -732,11 +732,11 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
_workspace_id: Option<workspace::WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Self>>>
|
||||
) -> Option<Entity<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Task::ready(Some(cx.new(|cx| {
|
||||
Some(cx.new(|cx| {
|
||||
ProjectDiagnosticsEditor::new(
|
||||
self.include_warnings,
|
||||
self.project.clone(),
|
||||
@@ -744,7 +744,7 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})))
|
||||
}))
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &App) -> bool {
|
||||
|
||||
@@ -1427,6 +1427,15 @@ impl DisplaySnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for DisplaySnapshot {
|
||||
type Target = BlockSnapshot;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.block_snapshot
|
||||
}
|
||||
}
|
||||
|
||||
/// A zero-indexed point in a text buffer consisting of a row and column adjusted for inserted blocks.
|
||||
#[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||
pub struct DisplayPoint(BlockPoint);
|
||||
|
||||
|
||||
@@ -69,6 +69,8 @@ impl From<CustomBlockId> for ElementId {
|
||||
}
|
||||
}
|
||||
|
||||
/// A zero-indexed point in a text buffer consisting of a row and column
|
||||
/// adjusted for inserted blocks, wrapped rows, tabs, folds and inlays.
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||
pub struct BlockPoint(pub Point);
|
||||
|
||||
@@ -80,11 +82,16 @@ struct WrapRow(u32);
|
||||
|
||||
pub type RenderBlock = Arc<dyn Send + Sync + Fn(&mut BlockContext) -> AnyElement>;
|
||||
|
||||
/// Where to place a block.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum BlockPlacement<T> {
|
||||
/// Place the block above the given position.
|
||||
Above(T),
|
||||
/// Place the block below the given position.
|
||||
Below(T),
|
||||
/// Place the block next the given position.
|
||||
Near(T),
|
||||
/// Replace the given range of positions with the block.
|
||||
Replace(RangeInclusive<T>),
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@ async fn test_edit_prediction_insert(cx: &mut gpui::TestAppContext) {
|
||||
cx.set_state("let absolute_zero_celsius = ˇ;");
|
||||
|
||||
propose_edits(&provider, vec![(28..28, "-273.15")], &mut cx);
|
||||
cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.update_visible_edit_prediction(&editor.display_snapshot(cx), window, cx)
|
||||
});
|
||||
|
||||
assert_editor_active_edit_completion(&mut cx, |_, edits| {
|
||||
assert_eq!(edits.len(), 1);
|
||||
@@ -41,7 +43,9 @@ async fn test_edit_prediction_modification(cx: &mut gpui::TestAppContext) {
|
||||
cx.set_state("let pi = ˇ\"foo\";");
|
||||
|
||||
propose_edits(&provider, vec![(9..14, "3.14159")], &mut cx);
|
||||
cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.update_visible_edit_prediction(&editor.display_snapshot(cx), window, cx)
|
||||
});
|
||||
|
||||
assert_editor_active_edit_completion(&mut cx, |_, edits| {
|
||||
assert_eq!(edits.len(), 1);
|
||||
@@ -76,7 +80,9 @@ async fn test_edit_prediction_jump_button(cx: &mut gpui::TestAppContext) {
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.update_visible_edit_prediction(&editor.display_snapshot(cx), window, cx)
|
||||
});
|
||||
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
|
||||
assert_eq!(move_target.to_point(&snapshot), Point::new(4, 3));
|
||||
});
|
||||
@@ -106,7 +112,9 @@ async fn test_edit_prediction_jump_button(cx: &mut gpui::TestAppContext) {
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.update_visible_edit_prediction(&editor.display_snapshot(cx), window, cx)
|
||||
});
|
||||
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
|
||||
assert_eq!(move_target.to_point(&snapshot), Point::new(1, 3));
|
||||
});
|
||||
@@ -147,7 +155,9 @@ async fn test_edit_prediction_invalidation_range(cx: &mut gpui::TestAppContext)
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.update_visible_edit_prediction(&editor.display_snapshot(cx), window, cx)
|
||||
});
|
||||
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
|
||||
assert_eq!(move_target.to_point(&snapshot), edit_location);
|
||||
});
|
||||
@@ -195,7 +205,9 @@ async fn test_edit_prediction_invalidation_range(cx: &mut gpui::TestAppContext)
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.update_visible_edit_prediction(&editor.display_snapshot(cx), window, cx)
|
||||
});
|
||||
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
|
||||
assert_eq!(move_target.to_point(&snapshot), edit_location);
|
||||
});
|
||||
@@ -250,7 +262,9 @@ async fn test_edit_prediction_jump_disabled_for_non_zed_providers(cx: &mut gpui:
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.update_visible_edit_prediction(&editor.display_snapshot(cx), window, cx)
|
||||
});
|
||||
|
||||
// For non-Zed providers, there should be no move completion (jump functionality disabled)
|
||||
cx.editor(|editor, _, _| {
|
||||
|
||||
@@ -111,7 +111,6 @@ use gpui::{
|
||||
UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window,
|
||||
div, point, prelude::*, pulsating_between, px, relative, size,
|
||||
};
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file};
|
||||
use hover_popover::{HoverState, hide_hover};
|
||||
use indent_guides::ActiveIndentGuidesState;
|
||||
@@ -164,7 +163,7 @@ use rand::seq::SliceRandom;
|
||||
use rpc::{ErrorCode, ErrorExt, proto::PeerId};
|
||||
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager};
|
||||
use selections_collection::{
|
||||
MutableSelectionsCollection, SelectionsCollection, resolve_selections,
|
||||
MutableSelectionsCollection, SelectionsCollection, resolve_selections_wrapping_blocks,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{GitGutterSetting, Settings, SettingsLocation, SettingsStore, update_settings_file};
|
||||
@@ -2874,7 +2873,7 @@ impl Editor {
|
||||
self.edit_prediction_provider = provider.map(|provider| RegisteredEditPredictionProvider {
|
||||
_subscription: cx.observe_in(&provider, window, |this, _, window, cx| {
|
||||
if this.focus_handle.is_focused(window) {
|
||||
this.update_visible_edit_prediction(window, cx);
|
||||
this.update_visible_edit_prediction(&this.display_snapshot(cx), window, cx);
|
||||
}
|
||||
}),
|
||||
provider: Arc::new(provider),
|
||||
@@ -2963,7 +2962,7 @@ impl Editor {
|
||||
if hidden != self.edit_predictions_hidden_for_vim_mode {
|
||||
self.edit_predictions_hidden_for_vim_mode = hidden;
|
||||
if hidden {
|
||||
self.update_visible_edit_prediction(window, cx);
|
||||
self.update_visible_edit_prediction(&self.display_snapshot(cx), window, cx);
|
||||
} else {
|
||||
self.refresh_edit_prediction(true, false, window, cx);
|
||||
}
|
||||
@@ -3188,9 +3187,9 @@ impl Editor {
|
||||
self.refresh_document_highlights(cx);
|
||||
refresh_linked_ranges(self, window, cx);
|
||||
|
||||
self.refresh_selected_text_highlights(false, window, cx);
|
||||
refresh_matching_bracket_highlights(self, cx);
|
||||
self.update_visible_edit_prediction(window, cx);
|
||||
self.refresh_selected_text_highlights(false, &display_map, window, cx);
|
||||
self.refresh_matching_bracket_highlights(&display_map, cx);
|
||||
self.update_visible_edit_prediction(&display_map, window, cx);
|
||||
self.edit_prediction_requires_modifier_in_indent_conflict = true;
|
||||
self.inline_blame_popover.take();
|
||||
if self.git_blame_inline_enabled {
|
||||
@@ -4373,16 +4372,17 @@ impl Editor {
|
||||
let new_anchor_selections = new_selections.iter().map(|e| &e.0);
|
||||
let new_selection_deltas = new_selections.iter().map(|e| e.1);
|
||||
let map = this.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let new_selections = resolve_selections::<usize, _>(new_anchor_selections, &map)
|
||||
.zip(new_selection_deltas)
|
||||
.map(|(selection, delta)| Selection {
|
||||
id: selection.id,
|
||||
start: selection.start + delta,
|
||||
end: selection.end + delta,
|
||||
reversed: selection.reversed,
|
||||
goal: SelectionGoal::None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let new_selections =
|
||||
resolve_selections_wrapping_blocks::<usize, _>(new_anchor_selections, &map)
|
||||
.zip(new_selection_deltas)
|
||||
.map(|(selection, delta)| Selection {
|
||||
id: selection.id,
|
||||
start: selection.start + delta,
|
||||
end: selection.end + delta,
|
||||
reversed: selection.reversed,
|
||||
goal: SelectionGoal::None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut i = 0;
|
||||
for (position, delta, selection_id, pair) in new_autoclose_regions {
|
||||
@@ -5894,7 +5894,11 @@ impl Editor {
|
||||
|
||||
crate::hover_popover::hide_hover(editor, cx);
|
||||
if editor.show_edit_predictions_in_menu() {
|
||||
editor.update_visible_edit_prediction(window, cx);
|
||||
editor.update_visible_edit_prediction(
|
||||
&editor.display_snapshot(cx),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
editor.discard_edit_prediction(false, cx);
|
||||
}
|
||||
@@ -5909,7 +5913,11 @@ impl Editor {
|
||||
// If it was already hidden and we don't show edit predictions in the menu,
|
||||
// we should also show the edit prediction when available.
|
||||
if was_hidden && editor.show_edit_predictions_in_menu() {
|
||||
editor.update_visible_edit_prediction(window, cx);
|
||||
editor.update_visible_edit_prediction(
|
||||
&editor.display_snapshot(cx),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -6974,6 +6982,7 @@ impl Editor {
|
||||
|
||||
fn prepare_highlight_query_from_selection(
|
||||
&mut self,
|
||||
window: &Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Option<(String, Range<Anchor>)> {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
@@ -6985,24 +6994,23 @@ impl Editor {
|
||||
if self.selections.count() != 1 || self.selections.line_mode() {
|
||||
return None;
|
||||
}
|
||||
let selection = self.selections.newest_anchor();
|
||||
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
let selection_point_range = selection.start.to_point(&multi_buffer_snapshot)
|
||||
..selection.end.to_point(&multi_buffer_snapshot);
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
let selection = self.selections.newest::<Point>(&snapshot);
|
||||
// If the selection spans multiple rows OR it is empty
|
||||
if selection_point_range.start.row != selection_point_range.end.row
|
||||
|| selection_point_range.start.column == selection_point_range.end.column
|
||||
if selection.start.row != selection.end.row
|
||||
|| selection.start.column == selection.end.column
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let query = multi_buffer_snapshot
|
||||
.text_for_range(selection.range())
|
||||
let selection_anchor_range = selection.range().to_anchors(snapshot.buffer_snapshot());
|
||||
let query = snapshot
|
||||
.buffer_snapshot()
|
||||
.text_for_range(selection_anchor_range.clone())
|
||||
.collect::<String>();
|
||||
if query.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some((query, selection.range()))
|
||||
Some((query, selection_anchor_range))
|
||||
}
|
||||
|
||||
fn update_selection_occurrence_highlights(
|
||||
@@ -7078,32 +7086,36 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
fn refresh_single_line_folds(&mut self, window: &mut Window, cx: &mut Context<Editor>) {
|
||||
fn refresh_single_line_folds(
|
||||
&mut self,
|
||||
display_snapshot: &DisplaySnapshot,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
struct NewlineFold;
|
||||
let type_id = std::any::TypeId::of::<NewlineFold>();
|
||||
if !self.mode.is_single_line() {
|
||||
return;
|
||||
}
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
if snapshot.buffer_snapshot().max_point().row == 0 {
|
||||
let display_snapshot = display_snapshot.clone();
|
||||
if display_snapshot.buffer_snapshot().max_point().row == 0 {
|
||||
return;
|
||||
}
|
||||
let task = cx.background_spawn(async move {
|
||||
let new_newlines = snapshot
|
||||
let new_newlines = display_snapshot
|
||||
.buffer_chars_at(0)
|
||||
.filter_map(|(c, i)| {
|
||||
if c == '\n' {
|
||||
Some(
|
||||
snapshot.buffer_snapshot().anchor_after(i)
|
||||
..snapshot.buffer_snapshot().anchor_before(i + 1),
|
||||
display_snapshot.buffer_snapshot().anchor_after(i)
|
||||
..display_snapshot.buffer_snapshot().anchor_before(i + 1),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let existing_newlines = snapshot
|
||||
.folds_in_range(0..snapshot.buffer_snapshot().len())
|
||||
let existing_newlines = display_snapshot
|
||||
.folds_in_range(0..display_snapshot.buffer_snapshot().len())
|
||||
.filter_map(|fold| {
|
||||
if fold.placeholder.type_tag == Some(type_id) {
|
||||
Some(fold.range.start..fold.range.end)
|
||||
@@ -7152,17 +7164,19 @@ impl Editor {
|
||||
fn refresh_selected_text_highlights(
|
||||
&mut self,
|
||||
on_buffer_edit: bool,
|
||||
display_snapshot: &DisplaySnapshot,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
let Some((query_text, query_range)) = self.prepare_highlight_query_from_selection(cx)
|
||||
let Some((query_text, query_range)) =
|
||||
self.prepare_highlight_query_from_selection(window, cx)
|
||||
else {
|
||||
self.clear_background_highlights::<SelectedTextHighlight>(cx);
|
||||
self.quick_selection_highlight_task.take();
|
||||
self.debounced_selection_highlight_task.take();
|
||||
return;
|
||||
};
|
||||
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
let multi_buffer_snapshot = display_snapshot.buffer_snapshot();
|
||||
if on_buffer_edit
|
||||
|| self
|
||||
.quick_selection_highlight_task
|
||||
@@ -7240,7 +7254,7 @@ impl Editor {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.update_visible_edit_prediction(window, cx);
|
||||
self.update_visible_edit_prediction(&self.display_snapshot(cx), window, cx);
|
||||
|
||||
if !user_requested
|
||||
&& (!self.should_show_edit_predictions()
|
||||
@@ -7402,7 +7416,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
provider.cycle(buffer, cursor_buffer_position, direction, cx);
|
||||
self.update_visible_edit_prediction(window, cx);
|
||||
self.update_visible_edit_prediction(&self.display_snapshot(cx), window, cx);
|
||||
|
||||
Some(())
|
||||
}
|
||||
@@ -7418,7 +7432,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
self.update_visible_edit_prediction(window, cx);
|
||||
self.update_visible_edit_prediction(&self.display_snapshot(cx), window, cx);
|
||||
}
|
||||
|
||||
pub fn display_cursor_names(
|
||||
@@ -7557,8 +7571,13 @@ impl Editor {
|
||||
// Store the transaction ID and selections before applying the edit
|
||||
let transaction_id_prev = self.buffer.read(cx).last_transaction_id(cx);
|
||||
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let last_edit_end = edits.last().unwrap().0.end.bias_right(&snapshot);
|
||||
let snapshot = self.display_snapshot(cx);
|
||||
let last_edit_end = edits
|
||||
.last()
|
||||
.unwrap()
|
||||
.0
|
||||
.end
|
||||
.bias_right(snapshot.buffer_snapshot());
|
||||
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(edits.iter().cloned(), None, cx)
|
||||
@@ -7577,7 +7596,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
self.update_visible_edit_prediction(window, cx);
|
||||
self.update_visible_edit_prediction(&snapshot, window, cx);
|
||||
if self.active_edit_prediction.is_none() {
|
||||
self.refresh_edit_prediction(true, true, window, cx);
|
||||
}
|
||||
@@ -7910,7 +7929,7 @@ impl Editor {
|
||||
since: Instant::now(),
|
||||
};
|
||||
|
||||
self.update_visible_edit_prediction(window, cx);
|
||||
self.update_visible_edit_prediction(&self.display_snapshot(cx), window, cx);
|
||||
cx.notify();
|
||||
}
|
||||
} else if let EditPredictionPreview::Active {
|
||||
@@ -7933,13 +7952,14 @@ impl Editor {
|
||||
released_too_fast: since.elapsed() < Duration::from_millis(200),
|
||||
};
|
||||
self.clear_row_highlights::<EditPredictionPreview>();
|
||||
self.update_visible_edit_prediction(window, cx);
|
||||
self.update_visible_edit_prediction(&self.display_snapshot(cx), window, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn update_visible_edit_prediction(
|
||||
&mut self,
|
||||
display_snapshot: &DisplaySnapshot,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<()> {
|
||||
@@ -7954,7 +7974,7 @@ impl Editor {
|
||||
|
||||
let selection = self.selections.newest_anchor();
|
||||
let cursor = selection.head();
|
||||
let multibuffer = self.buffer.read(cx).snapshot(cx);
|
||||
let multibuffer = display_snapshot.buffer_snapshot();
|
||||
let offset_selection = selection.map(|endpoint| endpoint.to_offset(&multibuffer));
|
||||
let excerpt_id = cursor.excerpt_id;
|
||||
|
||||
@@ -9787,7 +9807,7 @@ impl Editor {
|
||||
self.completion_tasks.clear();
|
||||
let context_menu = self.context_menu.borrow_mut().take();
|
||||
self.stale_edit_prediction_in_menu.take();
|
||||
self.update_visible_edit_prediction(window, cx);
|
||||
self.update_visible_edit_prediction(&self.display_snapshot(cx), window, cx);
|
||||
if let Some(CodeContextMenu::Completions(_)) = &context_menu
|
||||
&& let Some(completion_provider) = &self.completion_provider
|
||||
{
|
||||
@@ -17651,13 +17671,17 @@ impl Editor {
|
||||
window.show_character_palette();
|
||||
}
|
||||
|
||||
fn refresh_active_diagnostics(&mut self, cx: &mut Context<Editor>) {
|
||||
fn refresh_active_diagnostics(
|
||||
&mut self,
|
||||
display_snapshot: &DisplaySnapshot,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
if !self.diagnostics_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let ActiveDiagnostic::Group(active_diagnostics) = &mut self.active_diagnostics {
|
||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||
let buffer = display_snapshot.buffer_snapshot();
|
||||
let primary_range_start = active_diagnostics.active_range.start.to_offset(&buffer);
|
||||
let primary_range_end = active_diagnostics.active_range.end.to_offset(&buffer);
|
||||
let is_valid = buffer
|
||||
@@ -20959,15 +20983,16 @@ impl Editor {
|
||||
) {
|
||||
match event {
|
||||
multi_buffer::Event::Edited { edited_buffer } => {
|
||||
let display_snapshot = self.display_snapshot(cx);
|
||||
self.scrollbar_marker_state.dirty = true;
|
||||
self.active_indent_guides_state.dirty = true;
|
||||
self.refresh_active_diagnostics(cx);
|
||||
self.refresh_active_diagnostics(&display_snapshot, cx);
|
||||
self.refresh_code_actions(window, cx);
|
||||
self.refresh_selected_text_highlights(true, window, cx);
|
||||
self.refresh_single_line_folds(window, cx);
|
||||
refresh_matching_bracket_highlights(self, cx);
|
||||
self.refresh_selected_text_highlights(true, &display_snapshot, window, cx);
|
||||
self.refresh_single_line_folds(&display_snapshot, cx);
|
||||
self.refresh_matching_bracket_highlights(&display_snapshot, cx);
|
||||
if self.has_active_edit_prediction() {
|
||||
self.update_visible_edit_prediction(window, cx);
|
||||
self.update_visible_edit_prediction(&display_snapshot, window, cx);
|
||||
}
|
||||
|
||||
if let Some(edited_buffer) = edited_buffer {
|
||||
@@ -21105,7 +21130,7 @@ impl Editor {
|
||||
if !self.diagnostics_enabled() {
|
||||
return;
|
||||
}
|
||||
self.refresh_active_diagnostics(cx);
|
||||
self.refresh_active_diagnostics(&self.display_snapshot(cx), cx);
|
||||
self.refresh_inline_diagnostics(true, window, cx);
|
||||
self.scrollbar_marker_state.dirty = true;
|
||||
cx.notify();
|
||||
|
||||
@@ -8533,7 +8533,9 @@ async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext)
|
||||
})
|
||||
});
|
||||
|
||||
cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.update_visible_edit_prediction(&editor.display_snapshot(cx), window, cx)
|
||||
});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
|
||||
});
|
||||
|
||||
@@ -7233,16 +7233,9 @@ impl EditorElement {
|
||||
* ScrollPixelOffset::from(max_glyph_advance)
|
||||
- ScrollPixelOffset::from(delta.x * scroll_sensitivity))
|
||||
/ ScrollPixelOffset::from(max_glyph_advance);
|
||||
|
||||
let scale_factor = window.scale_factor();
|
||||
let y = (current_scroll_position.y
|
||||
* ScrollPixelOffset::from(line_height)
|
||||
* ScrollPixelOffset::from(scale_factor)
|
||||
let y = (current_scroll_position.y * ScrollPixelOffset::from(line_height)
|
||||
- ScrollPixelOffset::from(delta.y * scroll_sensitivity))
|
||||
.round()
|
||||
/ ScrollPixelOffset::from(line_height)
|
||||
/ ScrollPixelOffset::from(scale_factor);
|
||||
|
||||
/ ScrollPixelOffset::from(line_height);
|
||||
let mut scroll_position =
|
||||
point(x, y).clamp(&point(0., 0.), &position_map.scroll_max);
|
||||
let forbid_vertical_scroll = editor.scroll_manager.forbid_vertical_scroll();
|
||||
|
||||
@@ -1,57 +1,59 @@
|
||||
use crate::{Editor, RangeToAnchorExt};
|
||||
use crate::{Editor, RangeToAnchorExt, display_map::DisplaySnapshot};
|
||||
use gpui::{Context, HighlightStyle};
|
||||
use language::CursorShape;
|
||||
use multi_buffer::ToOffset;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
enum MatchingBracketHighlight {}
|
||||
|
||||
pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut Context<Editor>) {
|
||||
editor.clear_highlights::<MatchingBracketHighlight>(cx);
|
||||
impl Editor {
|
||||
pub fn refresh_matching_bracket_highlights(
|
||||
&mut self,
|
||||
snapshot: &DisplaySnapshot,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
self.clear_highlights::<MatchingBracketHighlight>(cx);
|
||||
|
||||
let buffer_snapshot = editor.buffer.read(cx).snapshot(cx);
|
||||
let newest_selection = editor
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.map(|anchor| anchor.to_offset(&buffer_snapshot));
|
||||
// Don't highlight brackets if the selection isn't empty
|
||||
if !newest_selection.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let head = newest_selection.head();
|
||||
if head > buffer_snapshot.len() {
|
||||
log::error!("bug: cursor offset is out of range while refreshing bracket highlights");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut tail = head;
|
||||
if (editor.cursor_shape == CursorShape::Block || editor.cursor_shape == CursorShape::Hollow)
|
||||
&& head < buffer_snapshot.len()
|
||||
{
|
||||
if let Some(tail_ch) = buffer_snapshot.chars_at(tail).next() {
|
||||
tail += tail_ch.len_utf8();
|
||||
let buffer_snapshot = snapshot.buffer_snapshot();
|
||||
let newest_selection = self.selections.newest::<usize>(snapshot);
|
||||
// Don't highlight brackets if the selection isn't empty
|
||||
if !newest_selection.is_empty() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((opening_range, closing_range)) =
|
||||
buffer_snapshot.innermost_enclosing_bracket_ranges(head..tail, None)
|
||||
{
|
||||
editor.highlight_text::<MatchingBracketHighlight>(
|
||||
vec![
|
||||
opening_range.to_anchors(&buffer_snapshot),
|
||||
closing_range.to_anchors(&buffer_snapshot),
|
||||
],
|
||||
HighlightStyle {
|
||||
background_color: Some(
|
||||
cx.theme()
|
||||
.colors()
|
||||
.editor_document_highlight_bracket_background,
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
let head = newest_selection.head();
|
||||
if head > buffer_snapshot.len() {
|
||||
log::error!("bug: cursor offset is out of range while refreshing bracket highlights");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut tail = head;
|
||||
if (self.cursor_shape == CursorShape::Block || self.cursor_shape == CursorShape::Hollow)
|
||||
&& head < buffer_snapshot.len()
|
||||
{
|
||||
if let Some(tail_ch) = buffer_snapshot.chars_at(tail).next() {
|
||||
tail += tail_ch.len_utf8();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((opening_range, closing_range)) =
|
||||
buffer_snapshot.innermost_enclosing_bracket_ranges(head..tail, None)
|
||||
{
|
||||
self.highlight_text::<MatchingBracketHighlight>(
|
||||
vec![
|
||||
opening_range.to_anchors(&buffer_snapshot),
|
||||
closing_range.to_anchors(&buffer_snapshot),
|
||||
],
|
||||
HighlightStyle {
|
||||
background_color: Some(
|
||||
cx.theme()
|
||||
.colors()
|
||||
.editor_document_highlight_bracket_background,
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -762,11 +762,11 @@ impl Item for Editor {
|
||||
_workspace_id: Option<WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Editor>>>
|
||||
) -> Option<Entity<Editor>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Task::ready(Some(cx.new(|cx| self.clone(window, cx))))
|
||||
Some(cx.new(|cx| self.clone(window, cx)))
|
||||
}
|
||||
|
||||
fn set_nav_history(
|
||||
|
||||
@@ -131,7 +131,7 @@ impl SelectionsCollection {
|
||||
&self,
|
||||
snapshot: &DisplaySnapshot,
|
||||
) -> Option<Selection<D>> {
|
||||
resolve_selections(self.pending_anchor(), &snapshot).next()
|
||||
resolve_selections_wrapping_blocks(self.pending_anchor(), &snapshot).next()
|
||||
}
|
||||
|
||||
pub(crate) fn pending_mode(&self) -> Option<SelectMode> {
|
||||
@@ -144,7 +144,8 @@ impl SelectionsCollection {
|
||||
{
|
||||
let disjoint_anchors = &self.disjoint;
|
||||
let mut disjoint =
|
||||
resolve_selections::<D, _>(disjoint_anchors.iter(), &snapshot).peekable();
|
||||
resolve_selections_wrapping_blocks::<D, _>(disjoint_anchors.iter(), &snapshot)
|
||||
.peekable();
|
||||
let mut pending_opt = self.pending::<D>(&snapshot);
|
||||
iter::from_fn(move || {
|
||||
if let Some(pending) = pending_opt.as_mut() {
|
||||
@@ -185,27 +186,6 @@ impl SelectionsCollection {
|
||||
selections
|
||||
}
|
||||
|
||||
/// Returns all of the selections, adjusted to take into account the selection line_mode. Uses a provided snapshot to resolve selections.
|
||||
pub fn all_adjusted_with_snapshot(
|
||||
&self,
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
) -> Vec<Selection<Point>> {
|
||||
let mut selections = self
|
||||
.disjoint
|
||||
.iter()
|
||||
.chain(self.pending_anchor())
|
||||
.map(|anchor| anchor.map(|anchor| anchor.to_point(&snapshot)))
|
||||
.collect::<Vec<_>>();
|
||||
if self.line_mode {
|
||||
for selection in &mut selections {
|
||||
let new_range = snapshot.expand_to_line(selection.range());
|
||||
selection.start = new_range.start;
|
||||
selection.end = new_range.end;
|
||||
}
|
||||
}
|
||||
selections
|
||||
}
|
||||
|
||||
/// Returns the newest selection, adjusted to take into account the selection line_mode
|
||||
pub fn newest_adjusted(&self, snapshot: &DisplaySnapshot) -> Selection<Point> {
|
||||
let mut selection = self.newest::<Point>(&snapshot);
|
||||
@@ -259,7 +239,7 @@ impl SelectionsCollection {
|
||||
Ok(ix) => ix + 1,
|
||||
Err(ix) => ix,
|
||||
};
|
||||
resolve_selections(&self.disjoint[start_ix..end_ix], snapshot).collect()
|
||||
resolve_selections_wrapping_blocks(&self.disjoint[start_ix..end_ix], snapshot).collect()
|
||||
}
|
||||
|
||||
pub fn all_display(&self, snapshot: &DisplaySnapshot) -> Vec<Selection<DisplayPoint>> {
|
||||
@@ -305,7 +285,7 @@ impl SelectionsCollection {
|
||||
&self,
|
||||
snapshot: &DisplaySnapshot,
|
||||
) -> Selection<D> {
|
||||
resolve_selections([self.newest_anchor()], &snapshot)
|
||||
resolve_selections_wrapping_blocks([self.newest_anchor()], &snapshot)
|
||||
.next()
|
||||
.unwrap()
|
||||
}
|
||||
@@ -328,7 +308,7 @@ impl SelectionsCollection {
|
||||
&self,
|
||||
snapshot: &DisplaySnapshot,
|
||||
) -> Selection<D> {
|
||||
resolve_selections([self.oldest_anchor()], &snapshot)
|
||||
resolve_selections_wrapping_blocks([self.oldest_anchor()], &snapshot)
|
||||
.next()
|
||||
.unwrap()
|
||||
}
|
||||
@@ -658,7 +638,7 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
pub fn select_anchors(&mut self, selections: Vec<Selection<Anchor>>) {
|
||||
let map = self.display_map();
|
||||
let resolved_selections =
|
||||
resolve_selections::<usize, _>(&selections, &map).collect::<Vec<_>>();
|
||||
resolve_selections_wrapping_blocks::<usize, _>(&selections, &map).collect::<Vec<_>>();
|
||||
self.select(resolved_selections);
|
||||
}
|
||||
|
||||
@@ -940,7 +920,8 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
|
||||
if !adjusted_disjoint.is_empty() {
|
||||
let map = self.display_map();
|
||||
let resolved_selections = resolve_selections(adjusted_disjoint.iter(), &map).collect();
|
||||
let resolved_selections =
|
||||
resolve_selections_wrapping_blocks(adjusted_disjoint.iter(), &map).collect();
|
||||
self.select::<usize>(resolved_selections);
|
||||
}
|
||||
|
||||
@@ -1025,6 +1006,7 @@ fn resolve_selections_point<'a>(
|
||||
}
|
||||
|
||||
/// Panics if passed selections are not in order
|
||||
/// Resolves the anchors to display positions
|
||||
fn resolve_selections_display<'a>(
|
||||
selections: impl 'a + IntoIterator<Item = &'a Selection<Anchor>>,
|
||||
map: &'a DisplaySnapshot,
|
||||
@@ -1056,8 +1038,13 @@ fn resolve_selections_display<'a>(
|
||||
coalesce_selections(selections)
|
||||
}
|
||||
|
||||
/// Resolves the passed in anchors to [`TextDimension`]s `D`
|
||||
/// wrapping around blocks inbetween.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if passed selections are not in order
|
||||
pub(crate) fn resolve_selections<'a, D, I>(
|
||||
pub(crate) fn resolve_selections_wrapping_blocks<'a, D, I>(
|
||||
selections: I,
|
||||
map: &'a DisplaySnapshot,
|
||||
) -> impl 'a + Iterator<Item = Selection<D>>
|
||||
@@ -1065,6 +1052,8 @@ where
|
||||
D: TextDimension + Ord + Sub<D, Output = D>,
|
||||
I: 'a + IntoIterator<Item = &'a Selection<Anchor>>,
|
||||
{
|
||||
// Transforms `Anchor -> DisplayPoint -> Point -> DisplayPoint -> D`
|
||||
// todo(lw): We should be able to short circuit the `Anchor -> DisplayPoint -> Point` to `Anchor -> Point`
|
||||
let (to_convert, selections) = resolve_selections_display(selections, map).tee();
|
||||
let mut converted_endpoints =
|
||||
map.buffer_snapshot()
|
||||
|
||||
@@ -4,8 +4,8 @@ use editor::{Editor, EditorEvent, MultiBuffer, SelectionEffects, multibuffer_con
|
||||
use git::repository::{CommitDetails, CommitDiff, RepoPath};
|
||||
use gpui::{
|
||||
Action, AnyElement, AnyView, App, AppContext as _, AsyncApp, AsyncWindowContext, Context,
|
||||
Entity, EventEmitter, FocusHandle, Focusable, IntoElement, PromptLevel, Render, Task,
|
||||
WeakEntity, Window, actions,
|
||||
Entity, EventEmitter, FocusHandle, Focusable, IntoElement, PromptLevel, Render, WeakEntity,
|
||||
Window, actions,
|
||||
};
|
||||
use language::{
|
||||
Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _,
|
||||
@@ -561,11 +561,11 @@ impl Item for CommitView {
|
||||
_workspace_id: Option<workspace::WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Self>>>
|
||||
) -> Option<Entity<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Task::ready(Some(cx.new(|cx| {
|
||||
Some(cx.new(|cx| {
|
||||
let editor = cx.new(|cx| {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.clone(window, cx))
|
||||
@@ -577,7 +577,7 @@ impl Item for CommitView {
|
||||
commit: self.commit.clone(),
|
||||
stash: self.stash,
|
||||
}
|
||||
})))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -625,16 +625,12 @@ impl Item for ProjectDiff {
|
||||
_workspace_id: Option<workspace::WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Self>>>
|
||||
) -> Option<Entity<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return Task::ready(None);
|
||||
};
|
||||
Task::ready(Some(cx.new(|cx| {
|
||||
ProjectDiff::new(self.project.clone(), workspace, window, cx)
|
||||
})))
|
||||
let workspace = self.workspace.upgrade()?;
|
||||
Some(cx.new(|cx| ProjectDiff::new(self.project.clone(), workspace, window, cx)))
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &App) -> bool {
|
||||
|
||||
@@ -111,15 +111,14 @@ impl CursorPosition {
|
||||
}
|
||||
editor::EditorMode::Full { .. } => {
|
||||
let mut last_selection = None::<Selection<Point>>;
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
if snapshot.excerpts().count() > 0 {
|
||||
for selection in
|
||||
editor.selections.all_adjusted_with_snapshot(&snapshot)
|
||||
{
|
||||
let snapshot = editor.display_snapshot(cx);
|
||||
if snapshot.buffer_snapshot().excerpts().count() > 0 {
|
||||
for selection in editor.selections.all_adjusted(&snapshot) {
|
||||
let selection_summary = snapshot
|
||||
.buffer_snapshot()
|
||||
.text_summary_for_range::<text::TextSummary, _>(
|
||||
selection.start..selection.end,
|
||||
);
|
||||
selection.start..selection.end,
|
||||
);
|
||||
cursor_position.selected_count.characters +=
|
||||
selection_summary.chars;
|
||||
if selection.end != selection.start {
|
||||
@@ -136,8 +135,12 @@ impl CursorPosition {
|
||||
}
|
||||
}
|
||||
}
|
||||
cursor_position.position = last_selection
|
||||
.map(|s| UserCaretPosition::at_selection_end(&s, &snapshot));
|
||||
cursor_position.position = last_selection.map(|s| {
|
||||
UserCaretPosition::at_selection_end(
|
||||
&s,
|
||||
snapshot.buffer_snapshot(),
|
||||
)
|
||||
});
|
||||
cursor_position.context = Some(editor.focus_handle(cx));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::{
|
||||
use anyhow::Context as _;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::{Cell, RefCell},
|
||||
mem,
|
||||
ops::Range,
|
||||
@@ -334,12 +335,11 @@ impl TextLayout {
|
||||
.line_height
|
||||
.to_pixels(font_size.into(), window.rem_size());
|
||||
|
||||
let mut runs = if let Some(runs) = runs {
|
||||
let runs = if let Some(runs) = runs {
|
||||
runs
|
||||
} else {
|
||||
vec![text_style.to_run(text.len())]
|
||||
};
|
||||
|
||||
window.request_measured_layout(Default::default(), {
|
||||
let element_state = self.clone();
|
||||
|
||||
@@ -378,15 +378,15 @@ impl TextLayout {
|
||||
}
|
||||
|
||||
let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
|
||||
let text = if let Some(truncate_width) = truncate_width {
|
||||
let (text, runs) = if let Some(truncate_width) = truncate_width {
|
||||
line_wrapper.truncate_line(
|
||||
text.clone(),
|
||||
truncate_width,
|
||||
&truncation_suffix,
|
||||
&mut runs,
|
||||
&runs,
|
||||
)
|
||||
} else {
|
||||
text.clone()
|
||||
(text.clone(), Cow::Borrowed(&*runs))
|
||||
};
|
||||
let len = text.len();
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ pub use gpui_macros::{
|
||||
overflow_style_methods, padding_style_methods, position_style_methods,
|
||||
visibility_style_methods,
|
||||
};
|
||||
|
||||
const ELLIPSIS: SharedString = SharedString::new_static("…");
|
||||
|
||||
/// A trait for elements that can be styled.
|
||||
|
||||
@@ -418,22 +418,21 @@ impl WindowTextSystem {
|
||||
let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
|
||||
|
||||
let mut lines = SmallVec::new();
|
||||
let mut line_start = 0;
|
||||
let mut max_wrap_lines = line_clamp.unwrap_or(usize::MAX);
|
||||
let mut max_wrap_lines = line_clamp;
|
||||
let mut wrapped_lines = 0;
|
||||
|
||||
let mut process_line = |line_text: SharedString| {
|
||||
let mut process_line = |line_text: SharedString, line_start, line_end| {
|
||||
font_runs.clear();
|
||||
let line_end = line_start + line_text.len();
|
||||
|
||||
let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
|
||||
let mut run_start = line_start;
|
||||
while run_start < line_end {
|
||||
let Some(run) = runs.peek_mut() else {
|
||||
log::warn!("`TextRun`s do not cover the entire to be shaped text");
|
||||
break;
|
||||
};
|
||||
|
||||
let run_len_within_line = cmp::min(line_end, run_start + run.len) - run_start;
|
||||
let run_len_within_line = cmp::min(line_end - run_start, run.len);
|
||||
|
||||
let decoration_changed = if let Some(last_run) = decoration_runs.last_mut()
|
||||
&& last_run.color == run.color
|
||||
@@ -467,11 +466,10 @@ impl WindowTextSystem {
|
||||
});
|
||||
}
|
||||
|
||||
if run_len_within_line == run.len {
|
||||
// Preserve the remainder of the run for the next line
|
||||
run.len -= run_len_within_line;
|
||||
if run.len == 0 {
|
||||
runs.next();
|
||||
} else {
|
||||
// Preserve the remainder of the run for the next line
|
||||
run.len -= run_len_within_line;
|
||||
}
|
||||
run_start += run_len_within_line;
|
||||
}
|
||||
@@ -481,7 +479,7 @@ impl WindowTextSystem {
|
||||
font_size,
|
||||
&font_runs,
|
||||
wrap_width,
|
||||
Some(max_wrap_lines - wrapped_lines),
|
||||
max_wrap_lines.map(|max| max.saturating_sub(wrapped_lines)),
|
||||
);
|
||||
wrapped_lines += layout.wrap_boundaries.len();
|
||||
|
||||
@@ -492,7 +490,6 @@ impl WindowTextSystem {
|
||||
});
|
||||
|
||||
// Skip `\n` character.
|
||||
line_start = line_end + 1;
|
||||
if let Some(run) = runs.peek_mut() {
|
||||
run.len -= 1;
|
||||
if run.len == 0 {
|
||||
@@ -502,21 +499,34 @@ impl WindowTextSystem {
|
||||
};
|
||||
|
||||
let mut split_lines = text.split('\n');
|
||||
let mut processed = false;
|
||||
|
||||
// Special case single lines to prevent allocating a sharedstring
|
||||
if let Some(first_line) = split_lines.next()
|
||||
&& let Some(second_line) = split_lines.next()
|
||||
{
|
||||
processed = true;
|
||||
process_line(first_line.to_string().into());
|
||||
process_line(second_line.to_string().into());
|
||||
let mut line_start = 0;
|
||||
process_line(
|
||||
SharedString::new(first_line),
|
||||
line_start,
|
||||
line_start + first_line.len(),
|
||||
);
|
||||
line_start += first_line.len() + '\n'.len_utf8();
|
||||
process_line(
|
||||
SharedString::new(second_line),
|
||||
line_start,
|
||||
line_start + second_line.len(),
|
||||
);
|
||||
for line_text in split_lines {
|
||||
process_line(line_text.to_string().into());
|
||||
line_start += line_text.len() + '\n'.len_utf8();
|
||||
process_line(
|
||||
SharedString::new(line_text),
|
||||
line_start,
|
||||
line_start + line_text.len(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !processed {
|
||||
process_line(text);
|
||||
} else {
|
||||
let end = text.len();
|
||||
process_line(text, 0, end);
|
||||
}
|
||||
|
||||
self.font_runs_pool.lock().push(font_runs);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{FontId, FontRun, Pixels, PlatformTextSystem, SharedString, TextRun, px};
|
||||
use collections::HashMap;
|
||||
use std::{iter, sync::Arc};
|
||||
use std::{borrow::Cow, iter, sync::Arc};
|
||||
|
||||
/// The GPUI line wrapper, used to wrap lines of text to a given width.
|
||||
pub struct LineWrapper {
|
||||
@@ -129,13 +129,13 @@ impl LineWrapper {
|
||||
}
|
||||
|
||||
/// Truncate a line of text to the given width with this wrapper's font and font size.
|
||||
pub fn truncate_line(
|
||||
pub fn truncate_line<'a>(
|
||||
&mut self,
|
||||
line: SharedString,
|
||||
truncate_width: Pixels,
|
||||
truncation_suffix: &str,
|
||||
runs: &mut Vec<TextRun>,
|
||||
) -> SharedString {
|
||||
runs: &'a [TextRun],
|
||||
) -> (SharedString, Cow<'a, [TextRun]>) {
|
||||
let mut width = px(0.);
|
||||
let mut suffix_width = truncation_suffix
|
||||
.chars()
|
||||
@@ -154,13 +154,14 @@ impl LineWrapper {
|
||||
if width.floor() > truncate_width {
|
||||
let result =
|
||||
SharedString::from(format!("{}{}", &line[..truncate_ix], truncation_suffix));
|
||||
update_runs_after_truncation(&result, truncation_suffix, runs);
|
||||
let mut runs = runs.to_vec();
|
||||
update_runs_after_truncation(&result, truncation_suffix, &mut runs);
|
||||
|
||||
return result;
|
||||
return (result, Cow::Owned(runs));
|
||||
}
|
||||
}
|
||||
|
||||
line
|
||||
(line, Cow::Borrowed(runs))
|
||||
}
|
||||
|
||||
/// Any character in this list should be treated as a word character,
|
||||
@@ -493,15 +494,14 @@ mod tests {
|
||||
fn perform_test(
|
||||
wrapper: &mut LineWrapper,
|
||||
text: &'static str,
|
||||
result: &'static str,
|
||||
expected: &'static str,
|
||||
ellipsis: &str,
|
||||
) {
|
||||
let dummy_run_lens = vec![text.len()];
|
||||
let mut dummy_runs = generate_test_runs(&dummy_run_lens);
|
||||
assert_eq!(
|
||||
wrapper.truncate_line(text.into(), px(220.), ellipsis, &mut dummy_runs),
|
||||
result
|
||||
);
|
||||
let dummy_runs = generate_test_runs(&dummy_run_lens);
|
||||
let (result, dummy_runs) =
|
||||
wrapper.truncate_line(text.into(), px(220.), ellipsis, &dummy_runs);
|
||||
assert_eq!(result, expected);
|
||||
assert_eq!(dummy_runs.first().unwrap().len, result.len());
|
||||
}
|
||||
|
||||
@@ -532,16 +532,15 @@ mod tests {
|
||||
fn perform_test(
|
||||
wrapper: &mut LineWrapper,
|
||||
text: &'static str,
|
||||
result: &str,
|
||||
expected: &str,
|
||||
run_lens: &[usize],
|
||||
result_run_len: &[usize],
|
||||
line_width: Pixels,
|
||||
) {
|
||||
let mut dummy_runs = generate_test_runs(run_lens);
|
||||
assert_eq!(
|
||||
wrapper.truncate_line(text.into(), line_width, "…", &mut dummy_runs),
|
||||
result
|
||||
);
|
||||
let dummy_runs = generate_test_runs(run_lens);
|
||||
let (result, dummy_runs) =
|
||||
wrapper.truncate_line(text.into(), line_width, "…", &dummy_runs);
|
||||
assert_eq!(result, expected);
|
||||
for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
|
||||
assert_eq!(run.len, *result_len);
|
||||
}
|
||||
|
||||
@@ -3243,14 +3243,11 @@ impl Window {
|
||||
/// returns a `Size`.
|
||||
///
|
||||
/// This method should only be called as part of the request_layout or prepaint phase of element drawing.
|
||||
pub fn request_measured_layout<
|
||||
F: FnMut(Size<Option<Pixels>>, Size<AvailableSpace>, &mut Window, &mut App) -> Size<Pixels>
|
||||
pub fn request_measured_layout<F>(&mut self, style: Style, measure: F) -> LayoutId
|
||||
where
|
||||
F: Fn(Size<Option<Pixels>>, Size<AvailableSpace>, &mut Window, &mut App) -> Size<Pixels>
|
||||
+ 'static,
|
||||
>(
|
||||
&mut self,
|
||||
style: Style,
|
||||
measure: F,
|
||||
) -> LayoutId {
|
||||
{
|
||||
self.invalidator.debug_assert_prepaint();
|
||||
|
||||
let rem_size = self.rem_size();
|
||||
|
||||
@@ -179,15 +179,15 @@ impl Item for ImageView {
|
||||
_workspace_id: Option<WorkspaceId>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Self>>>
|
||||
) -> Option<Entity<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Task::ready(Some(cx.new(|cx| Self {
|
||||
Some(cx.new(|cx| Self {
|
||||
image_item: self.image_item.clone(),
|
||||
project: self.project.clone(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
})))
|
||||
}))
|
||||
}
|
||||
|
||||
fn has_deleted_file(&self, cx: &App) -> bool {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use gpui::{
|
||||
Action, App, AppContext as _, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
KeyBindingContextPredicate, KeyContext, Keystroke, MouseButton, Render, Subscription, Task,
|
||||
actions,
|
||||
KeyBindingContextPredicate, KeyContext, Keystroke, MouseButton, Render, Subscription, actions,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use serde_json::json;
|
||||
@@ -158,11 +157,11 @@ impl Item for KeyContextView {
|
||||
_workspace_id: Option<workspace::WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Self>>>
|
||||
) -> Option<Entity<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Task::ready(Some(cx.new(|cx| KeyContextView::new(window, cx))))
|
||||
Some(cx.new(|cx| KeyContextView::new(window, cx)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use copilot::Copilot;
|
||||
use editor::{Editor, EditorEvent, actions::MoveToEnd, scroll::Autoscroll};
|
||||
use gpui::{
|
||||
AnyView, App, Context, Corner, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
|
||||
ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window, actions, div,
|
||||
ParentElement, Render, Styled, Subscription, WeakEntity, Window, actions, div,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{LanguageServerId, language_settings::SoftWrap};
|
||||
@@ -763,11 +763,11 @@ impl Item for LspLogView {
|
||||
_workspace_id: Option<WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Self>>>
|
||||
) -> Option<Entity<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Task::ready(Some(cx.new(|cx| {
|
||||
Some(cx.new(|cx| {
|
||||
let mut new_view = Self::new(self.project.clone(), self.log_store.clone(), window, cx);
|
||||
if let Some(server_id) = self.current_server_id {
|
||||
match self.active_entry_kind {
|
||||
@@ -778,7 +778,7 @@ impl Item for LspLogView {
|
||||
}
|
||||
}
|
||||
new_view
|
||||
})))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use editor::{Anchor, Editor, ExcerptId, SelectionEffects, scroll::Autoscroll};
|
||||
use gpui::{
|
||||
App, AppContext as _, Context, Div, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
|
||||
Hsla, InteractiveElement, IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||
ParentElement, Render, ScrollStrategy, SharedString, Styled, Task, UniformListScrollHandle,
|
||||
ParentElement, Render, ScrollStrategy, SharedString, Styled, UniformListScrollHandle,
|
||||
WeakEntity, Window, actions, div, rems, uniform_list,
|
||||
};
|
||||
use language::{Buffer, OwnedSyntaxLayer};
|
||||
@@ -573,17 +573,17 @@ impl Item for SyntaxTreeView {
|
||||
_: Option<workspace::WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Self>>>
|
||||
) -> Option<Entity<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Task::ready(Some(cx.new(|cx| {
|
||||
Some(cx.new(|cx| {
|
||||
let mut clone = Self::new(self.workspace_handle.clone(), None, window, cx);
|
||||
if let Some(editor) = &self.editor {
|
||||
clone.set_editor(editor.editor.clone(), window, cx)
|
||||
}
|
||||
clone
|
||||
})))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,9 @@ fn restore_code_actions_on_format_inner(value: &mut Value, path: &[&str]) -> Res
|
||||
} else {
|
||||
vec![formatter.clone()]
|
||||
};
|
||||
if formatter_array.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut code_action_formatters = Vec::new();
|
||||
for formatter in formatter_array {
|
||||
let Some(code_action) = formatter.get("code_action") else {
|
||||
|
||||
@@ -17,6 +17,7 @@ pub fn make_file_finder_include_ignored_an_enum(value: &mut Value) -> Result<()>
|
||||
Value::Bool(true) => Value::String("all".to_string()),
|
||||
Value::Bool(false) => Value::String("indexed".to_string()),
|
||||
Value::Null => Value::String("smart".to_string()),
|
||||
Value::String(s) if s == "all" || s == "indexed" || s == "smart" => return Ok(()),
|
||||
_ => anyhow::bail!("Expected include_ignored to be a boolean or null"),
|
||||
};
|
||||
Ok(())
|
||||
|
||||
@@ -366,7 +366,13 @@ mod tests {
|
||||
#[track_caller]
|
||||
fn assert_migrate_settings(input: &str, output: Option<&str>) {
|
||||
let migrated = migrate_settings(input).unwrap();
|
||||
assert_migrated_correctly(migrated, output);
|
||||
assert_migrated_correctly(migrated.clone(), output);
|
||||
|
||||
// expect that rerunning the migration does not result in another migration
|
||||
if let Some(migrated) = migrated {
|
||||
let rerun = migrate_settings(&migrated).unwrap();
|
||||
assert_migrated_correctly(rerun, None);
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
@@ -376,7 +382,13 @@ mod tests {
|
||||
output: Option<&str>,
|
||||
) {
|
||||
let migrated = run_migrations(input, migrations).unwrap();
|
||||
assert_migrated_correctly(migrated, output);
|
||||
assert_migrated_correctly(migrated.clone(), output);
|
||||
|
||||
// expect that rerunning the migration does not result in another migration
|
||||
if let Some(migrated) = migrated {
|
||||
let rerun = run_migrations(&migrated, migrations).unwrap();
|
||||
assert_migrated_correctly(rerun, None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2090,6 +2102,21 @@ mod tests {
|
||||
.unindent(),
|
||||
),
|
||||
);
|
||||
|
||||
assert_migrate_settings_with_migrations(
|
||||
&[MigrationType::Json(
|
||||
migrations::m_2025_10_16::restore_code_actions_on_format,
|
||||
)],
|
||||
&r#"{
|
||||
"formatter": [],
|
||||
"code_actions_on_format": {
|
||||
"bar": true,
|
||||
"baz": false
|
||||
}
|
||||
}"#
|
||||
.unindent(),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -6389,17 +6389,6 @@ impl MultiBufferSnapshot {
|
||||
debug_ranges.insert(key, text_ranges, format!("{value:?}").into())
|
||||
});
|
||||
}
|
||||
|
||||
// used by line_mode selections and tries to match vim behavior
|
||||
pub fn expand_to_line(&self, range: Range<Point>) -> Range<Point> {
|
||||
let new_start = MultiBufferPoint::new(range.start.row, 0);
|
||||
let new_end = if range.end.column > 0 {
|
||||
MultiBufferPoint::new(range.end.row, self.line_len(MultiBufferRow(range.end.row)))
|
||||
} else {
|
||||
range.end
|
||||
};
|
||||
new_start..new_end
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
||||
@@ -384,14 +384,14 @@ impl Item for Onboarding {
|
||||
_workspace_id: Option<WorkspaceId>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Self>>> {
|
||||
Task::ready(Some(cx.new(|cx| Onboarding {
|
||||
) -> Option<Entity<Self>> {
|
||||
Some(cx.new(|cx| Onboarding {
|
||||
workspace: self.workspace.clone(),
|
||||
user_store: self.user_store.clone(),
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
_settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
|
||||
})))
|
||||
}))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
|
||||
|
||||
@@ -139,146 +139,141 @@ impl Project {
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let builder = project
|
||||
.update(cx, move |_, cx| {
|
||||
let format_to_run = || {
|
||||
if let Some(command) = &spawn_task.command {
|
||||
let mut command: Option<Cow<str>> = shell_kind.try_quote(command);
|
||||
if let Some(command) = &mut command
|
||||
&& command.starts_with('"')
|
||||
&& let Some(prefix) = shell_kind.command_prefix()
|
||||
{
|
||||
*command = Cow::Owned(format!("{prefix}{command}"));
|
||||
}
|
||||
|
||||
let args = spawn_task
|
||||
.args
|
||||
.iter()
|
||||
.filter_map(|arg| shell_kind.try_quote(&arg));
|
||||
|
||||
command.into_iter().chain(args).join(" ")
|
||||
} else {
|
||||
// todo: this breaks for remotes to windows
|
||||
format!("exec {shell} -l")
|
||||
project.update(cx, move |this, cx| {
|
||||
let format_to_run = || {
|
||||
if let Some(command) = &spawn_task.command {
|
||||
let mut command: Option<Cow<str>> = shell_kind.try_quote(command);
|
||||
if let Some(command) = &mut command
|
||||
&& command.starts_with('"')
|
||||
&& let Some(prefix) = shell_kind.command_prefix()
|
||||
{
|
||||
*command = Cow::Owned(format!("{prefix}{command}"));
|
||||
}
|
||||
};
|
||||
|
||||
let (shell, env) = {
|
||||
env.extend(spawn_task.env);
|
||||
match remote_client {
|
||||
Some(remote_client) => match activation_script.clone() {
|
||||
activation_script if !activation_script.is_empty() => {
|
||||
let activation_script = activation_script.join("; ");
|
||||
let to_run = format_to_run();
|
||||
let args = vec![
|
||||
"-c".to_owned(),
|
||||
format!("{activation_script}; {to_run}"),
|
||||
];
|
||||
create_remote_shell(
|
||||
Some((
|
||||
&remote_client
|
||||
.read(cx)
|
||||
.shell()
|
||||
.unwrap_or_else(get_default_system_shell),
|
||||
&args,
|
||||
)),
|
||||
env,
|
||||
path,
|
||||
remote_client,
|
||||
cx,
|
||||
)?
|
||||
}
|
||||
_ => create_remote_shell(
|
||||
spawn_task
|
||||
.command
|
||||
.as_ref()
|
||||
.map(|command| (command, &spawn_task.args)),
|
||||
let args = spawn_task
|
||||
.args
|
||||
.iter()
|
||||
.filter_map(|arg| shell_kind.try_quote(&arg));
|
||||
|
||||
command.into_iter().chain(args).join(" ")
|
||||
} else {
|
||||
// todo: this breaks for remotes to windows
|
||||
format!("exec {shell} -l")
|
||||
}
|
||||
};
|
||||
|
||||
let (shell, env) = {
|
||||
env.extend(spawn_task.env);
|
||||
match remote_client {
|
||||
Some(remote_client) => match activation_script.clone() {
|
||||
activation_script if !activation_script.is_empty() => {
|
||||
let activation_script = activation_script.join("; ");
|
||||
let to_run = format_to_run();
|
||||
let args =
|
||||
vec!["-c".to_owned(), format!("{activation_script}; {to_run}")];
|
||||
create_remote_shell(
|
||||
Some((
|
||||
&remote_client
|
||||
.read(cx)
|
||||
.shell()
|
||||
.unwrap_or_else(get_default_system_shell),
|
||||
&args,
|
||||
)),
|
||||
env,
|
||||
path,
|
||||
remote_client,
|
||||
cx,
|
||||
)?,
|
||||
},
|
||||
None => match activation_script.clone() {
|
||||
activation_script if !activation_script.is_empty() => {
|
||||
let separator = shell_kind.sequential_commands_separator();
|
||||
let activation_script =
|
||||
activation_script.join(&format!("{separator} "));
|
||||
let to_run = format_to_run();
|
||||
)?
|
||||
}
|
||||
_ => create_remote_shell(
|
||||
spawn_task
|
||||
.command
|
||||
.as_ref()
|
||||
.map(|command| (command, &spawn_task.args)),
|
||||
env,
|
||||
path,
|
||||
remote_client,
|
||||
cx,
|
||||
)?,
|
||||
},
|
||||
None => match activation_script.clone() {
|
||||
activation_script if !activation_script.is_empty() => {
|
||||
let separator = shell_kind.sequential_commands_separator();
|
||||
let activation_script =
|
||||
activation_script.join(&format!("{separator} "));
|
||||
let to_run = format_to_run();
|
||||
|
||||
let mut arg =
|
||||
format!("{activation_script}{separator} {to_run}");
|
||||
if shell_kind == ShellKind::Cmd {
|
||||
// We need to put the entire command in quotes since otherwise CMD tries to execute them
|
||||
// as separate commands rather than chaining one after another.
|
||||
arg = format!("\"{arg}\"");
|
||||
}
|
||||
|
||||
let args = shell_kind.args_for_shell(false, arg);
|
||||
|
||||
(
|
||||
Shell::WithArguments {
|
||||
program: shell,
|
||||
args,
|
||||
title_override: None,
|
||||
},
|
||||
env,
|
||||
)
|
||||
let mut arg = format!("{activation_script}{separator} {to_run}");
|
||||
if shell_kind == ShellKind::Cmd {
|
||||
// We need to put the entire command in quotes since otherwise CMD tries to execute them
|
||||
// as separate commands rather than chaining one after another.
|
||||
arg = format!("\"{arg}\"");
|
||||
}
|
||||
_ => (
|
||||
if let Some(program) = spawn_task.command {
|
||||
Shell::WithArguments {
|
||||
program,
|
||||
args: spawn_task.args,
|
||||
title_override: None,
|
||||
}
|
||||
} else {
|
||||
Shell::System
|
||||
|
||||
let args = shell_kind.args_for_shell(false, arg);
|
||||
|
||||
(
|
||||
Shell::WithArguments {
|
||||
program: shell,
|
||||
args,
|
||||
title_override: None,
|
||||
},
|
||||
env,
|
||||
),
|
||||
},
|
||||
}
|
||||
};
|
||||
anyhow::Ok(TerminalBuilder::new(
|
||||
local_path.map(|path| path.to_path_buf()),
|
||||
task_state,
|
||||
shell,
|
||||
env,
|
||||
settings.cursor_shape,
|
||||
settings.alternate_scroll,
|
||||
settings.max_scroll_history_lines,
|
||||
is_via_remote,
|
||||
cx.entity_id().as_u64(),
|
||||
Some(completion_tx),
|
||||
cx,
|
||||
activation_script,
|
||||
))
|
||||
})??
|
||||
.await?;
|
||||
project.update(cx, move |this, cx| {
|
||||
let terminal_handle = cx.new(|cx| builder.subscribe(cx));
|
||||
|
||||
this.terminals
|
||||
.local_handles
|
||||
.push(terminal_handle.downgrade());
|
||||
|
||||
let id = terminal_handle.entity_id();
|
||||
cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
|
||||
let handles = &mut project.terminals.local_handles;
|
||||
|
||||
if let Some(index) = handles
|
||||
.iter()
|
||||
.position(|terminal| terminal.entity_id() == id)
|
||||
{
|
||||
handles.remove(index);
|
||||
cx.notify();
|
||||
)
|
||||
}
|
||||
_ => (
|
||||
if let Some(program) = spawn_task.command {
|
||||
Shell::WithArguments {
|
||||
program,
|
||||
args: spawn_task.args,
|
||||
title_override: None,
|
||||
}
|
||||
} else {
|
||||
Shell::System
|
||||
},
|
||||
env,
|
||||
),
|
||||
},
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
};
|
||||
TerminalBuilder::new(
|
||||
local_path.map(|path| path.to_path_buf()),
|
||||
task_state,
|
||||
shell,
|
||||
env,
|
||||
settings.cursor_shape,
|
||||
settings.alternate_scroll,
|
||||
settings.max_scroll_history_lines,
|
||||
is_via_remote,
|
||||
cx.entity_id().as_u64(),
|
||||
Some(completion_tx),
|
||||
cx,
|
||||
activation_script,
|
||||
)
|
||||
.map(|builder| {
|
||||
let terminal_handle = cx.new(|cx| builder.subscribe(cx));
|
||||
|
||||
terminal_handle
|
||||
})
|
||||
this.terminals
|
||||
.local_handles
|
||||
.push(terminal_handle.downgrade());
|
||||
|
||||
let id = terminal_handle.entity_id();
|
||||
cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
|
||||
let handles = &mut project.terminals.local_handles;
|
||||
|
||||
if let Some(index) = handles
|
||||
.iter()
|
||||
.position(|terminal| terminal.entity_id() == id)
|
||||
{
|
||||
handles.remove(index);
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
terminal_handle
|
||||
})
|
||||
})?
|
||||
})
|
||||
}
|
||||
|
||||
@@ -359,55 +354,53 @@ impl Project {
|
||||
})
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let builder = project
|
||||
.update(cx, move |_, cx| {
|
||||
let (shell, env) = {
|
||||
match remote_client {
|
||||
Some(remote_client) => {
|
||||
create_remote_shell(None, env, path, remote_client, cx)?
|
||||
}
|
||||
None => (settings.shell, env),
|
||||
}
|
||||
};
|
||||
anyhow::Ok(TerminalBuilder::new(
|
||||
local_path.map(|path| path.to_path_buf()),
|
||||
None,
|
||||
shell,
|
||||
env,
|
||||
settings.cursor_shape,
|
||||
settings.alternate_scroll,
|
||||
settings.max_scroll_history_lines,
|
||||
is_via_remote,
|
||||
cx.entity_id().as_u64(),
|
||||
None,
|
||||
cx,
|
||||
activation_script,
|
||||
))
|
||||
})??
|
||||
.await?;
|
||||
project.update(cx, move |this, cx| {
|
||||
let terminal_handle = cx.new(|cx| builder.subscribe(cx));
|
||||
|
||||
this.terminals
|
||||
.local_handles
|
||||
.push(terminal_handle.downgrade());
|
||||
|
||||
let id = terminal_handle.entity_id();
|
||||
cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
|
||||
let handles = &mut project.terminals.local_handles;
|
||||
|
||||
if let Some(index) = handles
|
||||
.iter()
|
||||
.position(|terminal| terminal.entity_id() == id)
|
||||
{
|
||||
handles.remove(index);
|
||||
cx.notify();
|
||||
let (shell, env) = {
|
||||
match remote_client {
|
||||
Some(remote_client) => {
|
||||
create_remote_shell(None, env, path, remote_client, cx)?
|
||||
}
|
||||
None => (settings.shell, env),
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
};
|
||||
TerminalBuilder::new(
|
||||
local_path.map(|path| path.to_path_buf()),
|
||||
None,
|
||||
shell,
|
||||
env,
|
||||
settings.cursor_shape,
|
||||
settings.alternate_scroll,
|
||||
settings.max_scroll_history_lines,
|
||||
is_via_remote,
|
||||
cx.entity_id().as_u64(),
|
||||
None,
|
||||
cx,
|
||||
activation_script,
|
||||
)
|
||||
.map(|builder| {
|
||||
let terminal_handle = cx.new(|cx| builder.subscribe(cx));
|
||||
|
||||
terminal_handle
|
||||
})
|
||||
this.terminals
|
||||
.local_handles
|
||||
.push(terminal_handle.downgrade());
|
||||
|
||||
let id = terminal_handle.entity_id();
|
||||
cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
|
||||
let handles = &mut project.terminals.local_handles;
|
||||
|
||||
if let Some(index) = handles
|
||||
.iter()
|
||||
.position(|terminal| terminal.entity_id() == id)
|
||||
{
|
||||
handles.remove(index);
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
terminal_handle
|
||||
})
|
||||
})?
|
||||
})
|
||||
}
|
||||
|
||||
@@ -416,27 +409,20 @@ impl Project {
|
||||
terminal: &Entity<Terminal>,
|
||||
cx: &mut Context<'_, Project>,
|
||||
cwd: Option<PathBuf>,
|
||||
) -> Task<Result<Entity<Terminal>>> {
|
||||
// We cannot clone the task's terminal, as it will effectively re-spawn the task, which might not be desirable.
|
||||
// For now, create a new shell instead.
|
||||
if terminal.read(cx).task().is_some() {
|
||||
return self.create_terminal_shell(cwd, cx);
|
||||
}
|
||||
|
||||
) -> Result<Entity<Terminal>> {
|
||||
let local_path = if self.is_via_remote_server() {
|
||||
None
|
||||
} else {
|
||||
cwd
|
||||
};
|
||||
|
||||
let builder = terminal.read(cx).clone_builder(cx, local_path);
|
||||
cx.spawn(async |project, cx| {
|
||||
let terminal = builder.await?;
|
||||
project.update(cx, |project, cx| {
|
||||
let terminal_handle = cx.new(|cx| terminal.subscribe(cx));
|
||||
terminal
|
||||
.read(cx)
|
||||
.clone_builder(cx, local_path)
|
||||
.map(|builder| {
|
||||
let terminal_handle = cx.new(|cx| builder.subscribe(cx));
|
||||
|
||||
project
|
||||
.terminals
|
||||
self.terminals
|
||||
.local_handles
|
||||
.push(terminal_handle.downgrade());
|
||||
|
||||
@@ -456,7 +442,6 @@ impl Project {
|
||||
|
||||
terminal_handle
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn terminal_settings<'a>(
|
||||
|
||||
@@ -709,13 +709,11 @@ impl Item for NotebookEditor {
|
||||
_workspace_id: Option<workspace::WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Self>>>
|
||||
) -> Option<Entity<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Task::ready(Some(cx.new(|cx| {
|
||||
Self::new(self.project.clone(), self.notebook_item.clone(), window, cx)
|
||||
})))
|
||||
Some(cx.new(|cx| Self::new(self.project.clone(), self.notebook_item.clone(), window, cx)))
|
||||
}
|
||||
|
||||
fn buffer_kind(&self, _: &App) -> workspace::item::ItemBufferKind {
|
||||
|
||||
@@ -15,7 +15,6 @@ path = "src/rope.rs"
|
||||
arrayvec = "0.7.1"
|
||||
log.workspace = true
|
||||
rayon.workspace = true
|
||||
smallvec.workspace = true
|
||||
sum_tree.workspace = true
|
||||
unicode-segmentation.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -93,6 +93,71 @@ impl Chunk {
|
||||
pub fn tabs(&self) -> Bitmap {
|
||||
self.tabs
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_char_boundary(&self, offset: usize) -> bool {
|
||||
(1 as Bitmap).unbounded_shl(offset as u32) & self.chars != 0 || offset == self.text.len()
|
||||
}
|
||||
|
||||
pub fn floor_char_boundary(&self, index: usize) -> usize {
|
||||
#[inline]
|
||||
pub(crate) const fn is_utf8_char_boundary(u8: u8) -> bool {
|
||||
// This is bit magic equivalent to: b < 128 || b >= 192
|
||||
(u8 as i8) >= -0x40
|
||||
}
|
||||
|
||||
if index >= self.text.len() {
|
||||
self.text.len()
|
||||
} else {
|
||||
let mut i = index;
|
||||
while i > 0 {
|
||||
if is_utf8_char_boundary(self.text.as_bytes()[i]) {
|
||||
break;
|
||||
}
|
||||
i -= 1;
|
||||
}
|
||||
|
||||
i
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn assert_char_boundary(&self, offset: usize) {
|
||||
if self.is_char_boundary(offset) {
|
||||
return;
|
||||
}
|
||||
panic_char_boundary(self, offset);
|
||||
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
#[track_caller]
|
||||
fn panic_char_boundary(chunk: &Chunk, offset: usize) {
|
||||
if offset > chunk.text.len() {
|
||||
panic!(
|
||||
"byte index {} is out of bounds of `{:?}` (length: {})",
|
||||
offset,
|
||||
chunk.text,
|
||||
chunk.text.len()
|
||||
);
|
||||
}
|
||||
// find the character
|
||||
let char_start = chunk.floor_char_boundary(offset);
|
||||
// `char_start` must be less than len and a char boundary
|
||||
let ch = chunk
|
||||
.text
|
||||
.get(char_start..)
|
||||
.unwrap()
|
||||
.chars()
|
||||
.next()
|
||||
.unwrap();
|
||||
let char_range = char_start..char_start + ch.len_utf8();
|
||||
panic!(
|
||||
"byte index {} is not a char boundary; it is inside {:?} (bytes {:?})",
|
||||
offset, ch, char_range,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
|
||||
@@ -4,8 +4,8 @@ mod point;
|
||||
mod point_utf16;
|
||||
mod unclipped;
|
||||
|
||||
use arrayvec::ArrayVec;
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator as _};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cmp, fmt, io, mem,
|
||||
ops::{self, AddAssign, Range},
|
||||
@@ -192,10 +192,19 @@ impl Rope {
|
||||
(),
|
||||
);
|
||||
|
||||
if text.len() > 2048 {
|
||||
#[cfg(not(test))]
|
||||
const NUM_CHUNKS: usize = 16;
|
||||
#[cfg(test)]
|
||||
const NUM_CHUNKS: usize = 4;
|
||||
|
||||
// We accommodate for NUM_CHUNKS chunks of size MAX_BASE
|
||||
// but given the chunk boundary can land within a character
|
||||
// we need to accommodate for the worst case where every chunk gets cut short by up to 4 bytes
|
||||
if text.len() > NUM_CHUNKS * chunk::MAX_BASE - NUM_CHUNKS * 4 {
|
||||
return self.push_large(text);
|
||||
}
|
||||
let mut new_chunks = SmallVec::<[_; 16]>::new();
|
||||
// 16 is enough as otherwise we will hit the branch above
|
||||
let mut new_chunks = ArrayVec::<_, NUM_CHUNKS>::new();
|
||||
|
||||
while !text.is_empty() {
|
||||
let mut split_ix = cmp::min(chunk::MAX_BASE, text.len());
|
||||
@@ -206,19 +215,8 @@ impl Rope {
|
||||
new_chunks.push(chunk);
|
||||
text = remainder;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
const PARALLEL_THRESHOLD: usize = 4;
|
||||
#[cfg(not(test))]
|
||||
const PARALLEL_THRESHOLD: usize = 4 * (2 * sum_tree::TREE_BASE);
|
||||
|
||||
if new_chunks.len() >= PARALLEL_THRESHOLD {
|
||||
self.chunks
|
||||
.par_extend(new_chunks.into_vec().into_par_iter().map(Chunk::new), ());
|
||||
} else {
|
||||
self.chunks
|
||||
.extend(new_chunks.into_iter().map(Chunk::new), ());
|
||||
}
|
||||
self.chunks
|
||||
.extend(new_chunks.into_iter().map(Chunk::new), ());
|
||||
|
||||
self.check_invariants();
|
||||
}
|
||||
|
||||
@@ -572,14 +572,12 @@ impl Item for ProjectSearchView {
|
||||
_workspace_id: Option<WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Self>>>
|
||||
) -> Option<Entity<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let model = self.entity.update(cx, |model, cx| model.clone(cx));
|
||||
Task::ready(Some(cx.new(|cx| {
|
||||
Self::new(self.workspace.clone(), model, window, cx, None)
|
||||
})))
|
||||
Some(cx.new(|cx| Self::new(self.workspace.clone(), model, window, cx, None)))
|
||||
}
|
||||
|
||||
fn added_to_workspace(
|
||||
@@ -3696,7 +3694,6 @@ pub mod tests {
|
||||
)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1);
|
||||
|
||||
@@ -3892,7 +3889,6 @@ pub mod tests {
|
||||
)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1);
|
||||
assert!(
|
||||
|
||||
@@ -727,15 +727,11 @@ pub fn to_pretty_json(
|
||||
indent_size: usize,
|
||||
indent_prefix_len: usize,
|
||||
) -> String {
|
||||
const SPACES: [u8; 32] = [b' '; 32];
|
||||
|
||||
debug_assert!(indent_size <= SPACES.len());
|
||||
debug_assert!(indent_prefix_len <= SPACES.len());
|
||||
|
||||
let mut output = Vec::new();
|
||||
let indent = " ".repeat(indent_size);
|
||||
let mut ser = serde_json::Serializer::with_formatter(
|
||||
&mut output,
|
||||
serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]),
|
||||
serde_json::ser::PrettyFormatter::with_indent(indent.as_bytes()),
|
||||
);
|
||||
|
||||
value.serialize(&mut ser).unwrap();
|
||||
@@ -744,7 +740,7 @@ pub fn to_pretty_json(
|
||||
let mut adjusted_text = String::new();
|
||||
for (i, line) in text.split('\n').enumerate() {
|
||||
if i > 0 {
|
||||
adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap());
|
||||
adjusted_text.extend(std::iter::repeat(' ').take(indent_prefix_len));
|
||||
}
|
||||
adjusted_text.push_str(line);
|
||||
adjusted_text.push('\n');
|
||||
|
||||
@@ -3,7 +3,7 @@ mod tree_map;
|
||||
|
||||
use arrayvec::ArrayVec;
|
||||
pub use cursor::{Cursor, FilterCursor, Iter};
|
||||
use rayon::prelude::*;
|
||||
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator as _};
|
||||
use std::marker::PhantomData;
|
||||
use std::mem;
|
||||
use std::{cmp::Ordering, fmt, iter::FromIterator, sync::Arc};
|
||||
|
||||
@@ -423,233 +423,232 @@ impl TerminalBuilder {
|
||||
completion_tx: Option<Sender<Option<ExitStatus>>>,
|
||||
cx: &App,
|
||||
activation_script: Vec<String>,
|
||||
) -> Task<Result<TerminalBuilder>> {
|
||||
let version = release_channel::AppVersion::global(cx);
|
||||
cx.background_spawn(async move {
|
||||
// If the parent environment doesn't have a locale set
|
||||
// (As is the case when launched from a .app on MacOS),
|
||||
// and the Project doesn't have a locale set, then
|
||||
// set a fallback for our child environment to use.
|
||||
if std::env::var("LANG").is_err() {
|
||||
env.entry("LANG".to_string())
|
||||
.or_insert_with(|| "en_US.UTF-8".to_string());
|
||||
}
|
||||
) -> Result<TerminalBuilder> {
|
||||
// If the parent environment doesn't have a locale set
|
||||
// (As is the case when launched from a .app on MacOS),
|
||||
// and the Project doesn't have a locale set, then
|
||||
// set a fallback for our child environment to use.
|
||||
if std::env::var("LANG").is_err() {
|
||||
env.entry("LANG".to_string())
|
||||
.or_insert_with(|| "en_US.UTF-8".to_string());
|
||||
}
|
||||
|
||||
env.insert("ZED_TERM".to_string(), "true".to_string());
|
||||
env.insert("TERM_PROGRAM".to_string(), "zed".to_string());
|
||||
env.insert("TERM".to_string(), "xterm-256color".to_string());
|
||||
env.insert("COLORTERM".to_string(), "truecolor".to_string());
|
||||
env.insert("TERM_PROGRAM_VERSION".to_string(), version.to_string());
|
||||
env.insert("ZED_TERM".to_string(), "true".to_string());
|
||||
env.insert("TERM_PROGRAM".to_string(), "zed".to_string());
|
||||
env.insert("TERM".to_string(), "xterm-256color".to_string());
|
||||
env.insert("COLORTERM".to_string(), "truecolor".to_string());
|
||||
env.insert(
|
||||
"TERM_PROGRAM_VERSION".to_string(),
|
||||
release_channel::AppVersion::global(cx).to_string(),
|
||||
);
|
||||
|
||||
#[derive(Default)]
|
||||
struct ShellParams {
|
||||
#[derive(Default)]
|
||||
struct ShellParams {
|
||||
program: String,
|
||||
args: Option<Vec<String>>,
|
||||
title_override: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl ShellParams {
|
||||
fn new(
|
||||
program: String,
|
||||
args: Option<Vec<String>>,
|
||||
title_override: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl ShellParams {
|
||||
fn new(
|
||||
program: String,
|
||||
args: Option<Vec<String>>,
|
||||
title_override: Option<SharedString>,
|
||||
) -> Self {
|
||||
log::debug!("Using {program} as shell");
|
||||
Self {
|
||||
program,
|
||||
args,
|
||||
title_override,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let shell_params = match shell.clone() {
|
||||
Shell::System => {
|
||||
if cfg!(windows) {
|
||||
Some(ShellParams::new(
|
||||
util::shell::get_windows_system_shell(),
|
||||
None,
|
||||
None,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Shell::Program(program) => Some(ShellParams::new(program, None, None)),
|
||||
Shell::WithArguments {
|
||||
) -> Self {
|
||||
log::info!("Using {program} as shell");
|
||||
Self {
|
||||
program,
|
||||
args,
|
||||
title_override,
|
||||
} => Some(ShellParams::new(program, Some(args), title_override)),
|
||||
};
|
||||
let terminal_title_override =
|
||||
shell_params.as_ref().and_then(|e| e.title_override.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
let shell_program = shell_params.as_ref().map(|params| {
|
||||
use util::ResultExt;
|
||||
let shell_params = match shell.clone() {
|
||||
Shell::System => {
|
||||
if cfg!(windows) {
|
||||
Some(ShellParams::new(
|
||||
util::shell::get_windows_system_shell(),
|
||||
None,
|
||||
None,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Shell::Program(program) => Some(ShellParams::new(program, None, None)),
|
||||
Shell::WithArguments {
|
||||
program,
|
||||
args,
|
||||
title_override,
|
||||
} => Some(ShellParams::new(program, Some(args), title_override)),
|
||||
};
|
||||
let terminal_title_override = shell_params.as_ref().and_then(|e| e.title_override.clone());
|
||||
|
||||
Self::resolve_path(¶ms.program)
|
||||
.log_err()
|
||||
.unwrap_or(params.program.clone())
|
||||
#[cfg(windows)]
|
||||
let shell_program = shell_params.as_ref().map(|params| {
|
||||
use util::ResultExt;
|
||||
|
||||
Self::resolve_path(¶ms.program)
|
||||
.log_err()
|
||||
.unwrap_or(params.program.clone())
|
||||
});
|
||||
|
||||
// Note: when remoting, this shell_kind will scrutinize `ssh` or
|
||||
// `wsl.exe` as a shell and fall back to posix or powershell based on
|
||||
// the compilation target. This is fine right now due to the restricted
|
||||
// way we use the return value, but would become incorrect if we
|
||||
// supported remoting into windows.
|
||||
let shell_kind = shell.shell_kind(cfg!(windows));
|
||||
|
||||
let pty_options = {
|
||||
let alac_shell = shell_params.as_ref().map(|params| {
|
||||
alacritty_terminal::tty::Shell::new(
|
||||
params.program.clone(),
|
||||
params.args.clone().unwrap_or_default(),
|
||||
)
|
||||
});
|
||||
|
||||
// Note: when remoting, this shell_kind will scrutinize `ssh` or
|
||||
// `wsl.exe` as a shell and fall back to posix or powershell based on
|
||||
// the compilation target. This is fine right now due to the restricted
|
||||
// way we use the return value, but would become incorrect if we
|
||||
// supported remoting into windows.
|
||||
let shell_kind = shell.shell_kind(cfg!(windows));
|
||||
|
||||
let pty_options = {
|
||||
let alac_shell = shell_params.as_ref().map(|params| {
|
||||
alacritty_terminal::tty::Shell::new(
|
||||
params.program.clone(),
|
||||
params.args.clone().unwrap_or_default(),
|
||||
)
|
||||
});
|
||||
|
||||
alacritty_terminal::tty::Options {
|
||||
shell: alac_shell,
|
||||
working_directory: working_directory.clone(),
|
||||
drain_on_exit: true,
|
||||
env: env.clone().into_iter().collect(),
|
||||
// We do not want to escape arguments if we are using CMD as our shell.
|
||||
// If we do we end up with too many quotes/escaped quotes for CMD to handle.
|
||||
#[cfg(windows)]
|
||||
escape_args: shell_kind != util::shell::ShellKind::Cmd,
|
||||
}
|
||||
};
|
||||
|
||||
let default_cursor_style = AlacCursorStyle::from(cursor_shape);
|
||||
let scrolling_history = if task.is_some() {
|
||||
// Tasks like `cargo build --all` may produce a lot of output, ergo allow maximum scrolling.
|
||||
// After the task finishes, we do not allow appending to that terminal, so small tasks output should not
|
||||
// cause excessive memory usage over time.
|
||||
MAX_SCROLL_HISTORY_LINES
|
||||
} else {
|
||||
max_scroll_history_lines
|
||||
.unwrap_or(DEFAULT_SCROLL_HISTORY_LINES)
|
||||
.min(MAX_SCROLL_HISTORY_LINES)
|
||||
};
|
||||
let config = Config {
|
||||
scrolling_history,
|
||||
default_cursor_style,
|
||||
..Config::default()
|
||||
};
|
||||
|
||||
//Spawn a task so the Alacritty EventLoop can communicate with us
|
||||
//TODO: Remove with a bounded sender which can be dispatched on &self
|
||||
let (events_tx, events_rx) = unbounded();
|
||||
//Set up the terminal...
|
||||
let mut term = Term::new(
|
||||
config.clone(),
|
||||
&TerminalBounds::default(),
|
||||
ZedListener(events_tx.clone()),
|
||||
);
|
||||
|
||||
//Alacritty defaults to alternate scrolling being on, so we just need to turn it off.
|
||||
if let AlternateScroll::Off = alternate_scroll {
|
||||
term.unset_private_mode(PrivateMode::Named(NamedPrivateMode::AlternateScroll));
|
||||
}
|
||||
|
||||
let term = Arc::new(FairMutex::new(term));
|
||||
|
||||
//Setup the pty...
|
||||
let pty = match tty::new(&pty_options, TerminalBounds::default().into(), window_id) {
|
||||
Ok(pty) => pty,
|
||||
Err(error) => {
|
||||
bail!(TerminalError {
|
||||
directory: working_directory,
|
||||
program: shell_params.as_ref().map(|params| params.program.clone()),
|
||||
args: shell_params.as_ref().and_then(|params| params.args.clone()),
|
||||
title_override: terminal_title_override,
|
||||
source: error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let pty_info = PtyProcessInfo::new(&pty);
|
||||
|
||||
//And connect them together
|
||||
let event_loop = EventLoop::new(
|
||||
term.clone(),
|
||||
ZedListener(events_tx),
|
||||
pty,
|
||||
pty_options.drain_on_exit,
|
||||
false,
|
||||
)
|
||||
.context("failed to create event loop")?;
|
||||
|
||||
//Kick things off
|
||||
let pty_tx = event_loop.channel();
|
||||
let _io_thread = event_loop.spawn(); // DANGER
|
||||
|
||||
let no_task = task.is_none();
|
||||
|
||||
let terminal = Terminal {
|
||||
task,
|
||||
terminal_type: TerminalType::Pty {
|
||||
pty_tx: Notifier(pty_tx),
|
||||
info: pty_info,
|
||||
},
|
||||
completion_tx,
|
||||
term,
|
||||
term_config: config,
|
||||
title_override: terminal_title_override,
|
||||
events: VecDeque::with_capacity(10), //Should never get this high.
|
||||
last_content: Default::default(),
|
||||
last_mouse: None,
|
||||
matches: Vec::new(),
|
||||
selection_head: None,
|
||||
breadcrumb_text: String::new(),
|
||||
scroll_px: px(0.),
|
||||
next_link_id: 0,
|
||||
selection_phase: SelectionPhase::Ended,
|
||||
hyperlink_regex_searches: RegexSearches::new(),
|
||||
vi_mode_enabled: false,
|
||||
is_ssh_terminal,
|
||||
last_mouse_move_time: Instant::now(),
|
||||
last_hyperlink_search_position: None,
|
||||
alacritty_terminal::tty::Options {
|
||||
shell: alac_shell,
|
||||
working_directory: working_directory.clone(),
|
||||
drain_on_exit: true,
|
||||
env: env.clone().into_iter().collect(),
|
||||
// We do not want to escape arguments if we are using CMD as our shell.
|
||||
// If we do we end up with too many quotes/escaped quotes for CMD to handle.
|
||||
#[cfg(windows)]
|
||||
shell_program,
|
||||
activation_script: activation_script.clone(),
|
||||
template: CopyTemplate {
|
||||
shell,
|
||||
env,
|
||||
cursor_shape,
|
||||
alternate_scroll,
|
||||
max_scroll_history_lines,
|
||||
window_id,
|
||||
},
|
||||
child_exited: None,
|
||||
};
|
||||
escape_args: shell_kind != util::shell::ShellKind::Cmd,
|
||||
}
|
||||
};
|
||||
|
||||
if !activation_script.is_empty() && no_task {
|
||||
for activation_script in activation_script {
|
||||
terminal.write_to_pty(activation_script.into_bytes());
|
||||
// Simulate enter key press
|
||||
// NOTE(PowerShell): using `\r\n` will put PowerShell in a continuation mode (infamous >> character)
|
||||
// and generally mess up the rendering.
|
||||
terminal.write_to_pty(b"\x0d");
|
||||
}
|
||||
// In order to clear the screen at this point, we have two options:
|
||||
// 1. We can send a shell-specific command such as "clear" or "cls"
|
||||
// 2. We can "echo" a marker message that we will then catch when handling a Wakeup event
|
||||
// and clear the screen using `terminal.clear()` method
|
||||
// We cannot issue a `terminal.clear()` command at this point as alacritty is evented
|
||||
// and while we have sent the activation script to the pty, it will be executed asynchronously.
|
||||
// Therefore, we somehow need to wait for the activation script to finish executing before we
|
||||
// can proceed with clearing the screen.
|
||||
terminal.write_to_pty(shell_kind.clear_screen_command().as_bytes());
|
||||
let default_cursor_style = AlacCursorStyle::from(cursor_shape);
|
||||
let scrolling_history = if task.is_some() {
|
||||
// Tasks like `cargo build --all` may produce a lot of output, ergo allow maximum scrolling.
|
||||
// After the task finishes, we do not allow appending to that terminal, so small tasks output should not
|
||||
// cause excessive memory usage over time.
|
||||
MAX_SCROLL_HISTORY_LINES
|
||||
} else {
|
||||
max_scroll_history_lines
|
||||
.unwrap_or(DEFAULT_SCROLL_HISTORY_LINES)
|
||||
.min(MAX_SCROLL_HISTORY_LINES)
|
||||
};
|
||||
let config = Config {
|
||||
scrolling_history,
|
||||
default_cursor_style,
|
||||
..Config::default()
|
||||
};
|
||||
|
||||
//Spawn a task so the Alacritty EventLoop can communicate with us
|
||||
//TODO: Remove with a bounded sender which can be dispatched on &self
|
||||
let (events_tx, events_rx) = unbounded();
|
||||
//Set up the terminal...
|
||||
let mut term = Term::new(
|
||||
config.clone(),
|
||||
&TerminalBounds::default(),
|
||||
ZedListener(events_tx.clone()),
|
||||
);
|
||||
|
||||
//Alacritty defaults to alternate scrolling being on, so we just need to turn it off.
|
||||
if let AlternateScroll::Off = alternate_scroll {
|
||||
term.unset_private_mode(PrivateMode::Named(NamedPrivateMode::AlternateScroll));
|
||||
}
|
||||
|
||||
let term = Arc::new(FairMutex::new(term));
|
||||
|
||||
//Setup the pty...
|
||||
let pty = match tty::new(&pty_options, TerminalBounds::default().into(), window_id) {
|
||||
Ok(pty) => pty,
|
||||
Err(error) => {
|
||||
bail!(TerminalError {
|
||||
directory: working_directory,
|
||||
program: shell_params.as_ref().map(|params| params.program.clone()),
|
||||
args: shell_params.as_ref().and_then(|params| params.args.clone()),
|
||||
title_override: terminal_title_override,
|
||||
source: error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let pty_info = PtyProcessInfo::new(&pty);
|
||||
|
||||
//And connect them together
|
||||
let event_loop = EventLoop::new(
|
||||
term.clone(),
|
||||
ZedListener(events_tx),
|
||||
pty,
|
||||
pty_options.drain_on_exit,
|
||||
false,
|
||||
)
|
||||
.context("failed to create event loop")?;
|
||||
|
||||
//Kick things off
|
||||
let pty_tx = event_loop.channel();
|
||||
let _io_thread = event_loop.spawn(); // DANGER
|
||||
|
||||
let no_task = task.is_none();
|
||||
|
||||
let terminal = Terminal {
|
||||
task,
|
||||
terminal_type: TerminalType::Pty {
|
||||
pty_tx: Notifier(pty_tx),
|
||||
info: pty_info,
|
||||
},
|
||||
completion_tx,
|
||||
term,
|
||||
term_config: config,
|
||||
title_override: terminal_title_override,
|
||||
events: VecDeque::with_capacity(10), //Should never get this high.
|
||||
last_content: Default::default(),
|
||||
last_mouse: None,
|
||||
matches: Vec::new(),
|
||||
selection_head: None,
|
||||
breadcrumb_text: String::new(),
|
||||
scroll_px: px(0.),
|
||||
next_link_id: 0,
|
||||
selection_phase: SelectionPhase::Ended,
|
||||
hyperlink_regex_searches: RegexSearches::new(),
|
||||
vi_mode_enabled: false,
|
||||
is_ssh_terminal,
|
||||
last_mouse_move_time: Instant::now(),
|
||||
last_hyperlink_search_position: None,
|
||||
#[cfg(windows)]
|
||||
shell_program,
|
||||
activation_script: activation_script.clone(),
|
||||
template: CopyTemplate {
|
||||
shell,
|
||||
env,
|
||||
cursor_shape,
|
||||
alternate_scroll,
|
||||
max_scroll_history_lines,
|
||||
window_id,
|
||||
},
|
||||
child_exited: None,
|
||||
};
|
||||
|
||||
if !activation_script.is_empty() && no_task {
|
||||
for activation_script in activation_script {
|
||||
terminal.write_to_pty(activation_script.into_bytes());
|
||||
// Simulate enter key press
|
||||
// NOTE(PowerShell): using `\r\n` will put PowerShell in a continuation mode (infamous >> character)
|
||||
// and generally mess up the rendering.
|
||||
terminal.write_to_pty(b"\x0d");
|
||||
}
|
||||
// In order to clear the screen at this point, we have two options:
|
||||
// 1. We can send a shell-specific command such as "clear" or "cls"
|
||||
// 2. We can "echo" a marker message that we will then catch when handling a Wakeup event
|
||||
// and clear the screen using `terminal.clear()` method
|
||||
// We cannot issue a `terminal.clear()` command at this point as alacritty is evented
|
||||
// and while we have sent the activation script to the pty, it will be executed asynchronously.
|
||||
// Therefore, we somehow need to wait for the activation script to finish executing before we
|
||||
// can proceed with clearing the screen.
|
||||
terminal.write_to_pty(shell_kind.clear_screen_command().as_bytes());
|
||||
// Simulate enter key press
|
||||
terminal.write_to_pty(b"\x0d");
|
||||
}
|
||||
|
||||
Ok(TerminalBuilder {
|
||||
terminal,
|
||||
events_rx,
|
||||
})
|
||||
Ok(TerminalBuilder {
|
||||
terminal,
|
||||
events_rx,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2154,7 +2153,7 @@ impl Terminal {
|
||||
self.vi_mode_enabled
|
||||
}
|
||||
|
||||
pub fn clone_builder(&self, cx: &App, cwd: Option<PathBuf>) -> Task<Result<TerminalBuilder>> {
|
||||
pub fn clone_builder(&self, cx: &App, cwd: Option<PathBuf>) -> Result<TerminalBuilder> {
|
||||
let working_directory = self.working_directory().or_else(|| cwd);
|
||||
TerminalBuilder::new(
|
||||
working_directory,
|
||||
@@ -2390,30 +2389,28 @@ mod tests {
|
||||
let (completion_tx, completion_rx) = smol::channel::unbounded();
|
||||
let (program, args) = ShellBuilder::new(&Shell::System, false)
|
||||
.build(Some("echo".to_owned()), &["hello".to_owned()]);
|
||||
let builder = cx
|
||||
.update(|cx| {
|
||||
TerminalBuilder::new(
|
||||
None,
|
||||
None,
|
||||
task::Shell::WithArguments {
|
||||
program,
|
||||
args,
|
||||
title_override: None,
|
||||
},
|
||||
HashMap::default(),
|
||||
CursorShape::default(),
|
||||
AlternateScroll::On,
|
||||
None,
|
||||
false,
|
||||
0,
|
||||
Some(completion_tx),
|
||||
cx,
|
||||
vec![],
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let terminal = cx.new(|cx| builder.subscribe(cx));
|
||||
let terminal = cx.new(|cx| {
|
||||
TerminalBuilder::new(
|
||||
None,
|
||||
None,
|
||||
task::Shell::WithArguments {
|
||||
program,
|
||||
args,
|
||||
title_override: None,
|
||||
},
|
||||
HashMap::default(),
|
||||
CursorShape::default(),
|
||||
AlternateScroll::On,
|
||||
None,
|
||||
false,
|
||||
0,
|
||||
Some(completion_tx),
|
||||
cx,
|
||||
vec![],
|
||||
)
|
||||
.unwrap()
|
||||
.subscribe(cx)
|
||||
});
|
||||
assert_eq!(
|
||||
completion_rx.recv().await.unwrap(),
|
||||
Some(ExitStatus::default())
|
||||
@@ -2442,27 +2439,25 @@ mod tests {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let (completion_tx, completion_rx) = smol::channel::unbounded();
|
||||
let builder = cx
|
||||
.update(|cx| {
|
||||
TerminalBuilder::new(
|
||||
None,
|
||||
None,
|
||||
task::Shell::System,
|
||||
HashMap::default(),
|
||||
CursorShape::default(),
|
||||
AlternateScroll::On,
|
||||
None,
|
||||
false,
|
||||
0,
|
||||
Some(completion_tx),
|
||||
cx,
|
||||
Vec::new(),
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
// Build an empty command, which will result in a tty shell spawned.
|
||||
let terminal = cx.new(|cx| builder.subscribe(cx));
|
||||
let terminal = cx.new(|cx| {
|
||||
TerminalBuilder::new(
|
||||
None,
|
||||
None,
|
||||
task::Shell::System,
|
||||
HashMap::default(),
|
||||
CursorShape::default(),
|
||||
AlternateScroll::On,
|
||||
None,
|
||||
false,
|
||||
0,
|
||||
Some(completion_tx),
|
||||
cx,
|
||||
Vec::new(),
|
||||
)
|
||||
.unwrap()
|
||||
.subscribe(cx)
|
||||
});
|
||||
|
||||
let (event_tx, event_rx) = smol::channel::unbounded::<Event>();
|
||||
cx.update(|cx| {
|
||||
@@ -2513,30 +2508,28 @@ mod tests {
|
||||
let (completion_tx, completion_rx) = smol::channel::unbounded();
|
||||
let (program, args) = ShellBuilder::new(&Shell::System, false)
|
||||
.build(Some("asdasdasdasd".to_owned()), &["@@@@@".to_owned()]);
|
||||
let builder = cx
|
||||
.update(|cx| {
|
||||
TerminalBuilder::new(
|
||||
None,
|
||||
None,
|
||||
task::Shell::WithArguments {
|
||||
program,
|
||||
args,
|
||||
title_override: None,
|
||||
},
|
||||
HashMap::default(),
|
||||
CursorShape::default(),
|
||||
AlternateScroll::On,
|
||||
None,
|
||||
false,
|
||||
0,
|
||||
Some(completion_tx),
|
||||
cx,
|
||||
Vec::new(),
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let terminal = cx.new(|cx| builder.subscribe(cx));
|
||||
let terminal = cx.new(|cx| {
|
||||
TerminalBuilder::new(
|
||||
None,
|
||||
None,
|
||||
task::Shell::WithArguments {
|
||||
program,
|
||||
args,
|
||||
title_override: None,
|
||||
},
|
||||
HashMap::default(),
|
||||
CursorShape::default(),
|
||||
AlternateScroll::On,
|
||||
None,
|
||||
false,
|
||||
0,
|
||||
Some(completion_tx),
|
||||
cx,
|
||||
Vec::new(),
|
||||
)
|
||||
.unwrap()
|
||||
.subscribe(cx)
|
||||
});
|
||||
|
||||
let (event_tx, event_rx) = smol::channel::unbounded::<Event>();
|
||||
cx.update(|cx| {
|
||||
|
||||
@@ -214,6 +214,14 @@ async fn deserialize_pane_group(
|
||||
}
|
||||
SerializedPaneGroup::Pane(serialized_pane) => {
|
||||
let active = serialized_pane.active;
|
||||
let new_items = deserialize_terminal_views(
|
||||
workspace_id,
|
||||
project.clone(),
|
||||
workspace.clone(),
|
||||
serialized_pane.children.as_slice(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let pane = panel
|
||||
.update_in(cx, |terminal_panel, window, cx| {
|
||||
@@ -228,71 +236,56 @@ async fn deserialize_pane_group(
|
||||
.log_err()?;
|
||||
let active_item = serialized_pane.active_item;
|
||||
let pinned_count = serialized_pane.pinned_count;
|
||||
let new_items = deserialize_terminal_views(
|
||||
workspace_id,
|
||||
project.clone(),
|
||||
workspace.clone(),
|
||||
serialized_pane.children.as_slice(),
|
||||
cx,
|
||||
);
|
||||
cx.spawn({
|
||||
let pane = pane.downgrade();
|
||||
async move |cx| {
|
||||
let new_items = new_items.await;
|
||||
|
||||
let items = pane.update_in(cx, |pane, window, cx| {
|
||||
populate_pane_items(pane, new_items, active_item, window, cx);
|
||||
pane.set_pinned_count(pinned_count);
|
||||
pane.items_len()
|
||||
});
|
||||
let terminal = pane
|
||||
.update_in(cx, |pane, window, cx| {
|
||||
populate_pane_items(pane, new_items, active_item, window, cx);
|
||||
pane.set_pinned_count(pinned_count);
|
||||
// Avoid blank panes in splits
|
||||
if items.is_ok_and(|items| items == 0) {
|
||||
if pane.items_len() == 0 {
|
||||
let working_directory = workspace
|
||||
.update(cx, |workspace, cx| default_working_directory(workspace, cx))
|
||||
.ok()
|
||||
.flatten();
|
||||
let Some(terminal) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_terminal_shell(working_directory, cx)
|
||||
})
|
||||
.log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let terminal = terminal.await.log_err();
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
if let Some(terminal) = terminal {
|
||||
let terminal_view = Box::new(cx.new(|cx| {
|
||||
TerminalView::new(
|
||||
terminal,
|
||||
workspace.clone(),
|
||||
Some(workspace_id),
|
||||
project.downgrade(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
pane.add_item(terminal_view, true, false, None, window, cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
let terminal = project.update(cx, |project, cx| {
|
||||
project.create_terminal_shell(working_directory, cx)
|
||||
});
|
||||
Some(Some(terminal))
|
||||
} else {
|
||||
Some(None)
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.ok()
|
||||
.flatten()?;
|
||||
if let Some(terminal) = terminal {
|
||||
let terminal = terminal.await.ok()?;
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
let terminal_view = Box::new(cx.new(|cx| {
|
||||
TerminalView::new(
|
||||
terminal,
|
||||
workspace.clone(),
|
||||
Some(workspace_id),
|
||||
project.downgrade(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
pane.add_item(terminal_view, true, false, None, window, cx);
|
||||
})
|
||||
.ok()?;
|
||||
}
|
||||
Some((Member::Pane(pane.clone()), active.then_some(pane)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_terminal_views(
|
||||
async fn deserialize_terminal_views(
|
||||
workspace_id: WorkspaceId,
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
item_ids: &[u64],
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> impl Future<Output = Vec<Entity<TerminalView>>> + use<> {
|
||||
) -> Vec<Entity<TerminalView>> {
|
||||
let mut items = Vec::with_capacity(item_ids.len());
|
||||
let mut deserialized_items = item_ids
|
||||
.iter()
|
||||
.map(|item_id| {
|
||||
@@ -309,15 +302,12 @@ fn deserialize_terminal_views(
|
||||
.unwrap_or_else(|e| Task::ready(Err(e.context("no window present"))))
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>();
|
||||
async move {
|
||||
let mut items = Vec::with_capacity(deserialized_items.len());
|
||||
while let Some(item) = deserialized_items.next().await {
|
||||
if let Some(item) = item.log_err() {
|
||||
items.push(item);
|
||||
}
|
||||
while let Some(item) = deserialized_items.next().await {
|
||||
if let Some(item) = item.log_err() {
|
||||
items.push(item);
|
||||
}
|
||||
items
|
||||
}
|
||||
items
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
||||
@@ -462,11 +462,11 @@ impl TerminalPanel {
|
||||
cx.spawn_in(window, async move |panel, cx| {
|
||||
let terminal = project
|
||||
.update(cx, |project, cx| match terminal_view {
|
||||
Some(view) => project.clone_terminal(
|
||||
Some(view) => Task::ready(project.clone_terminal(
|
||||
&view.read(cx).terminal.clone(),
|
||||
cx,
|
||||
working_directory,
|
||||
),
|
||||
)),
|
||||
None => project.create_terminal_shell(working_directory, cx),
|
||||
})
|
||||
.ok()?
|
||||
|
||||
@@ -1220,31 +1220,28 @@ impl Item for TerminalView {
|
||||
workspace_id: Option<WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Self>>> {
|
||||
let Ok(terminal) = self.project.update(cx, |project, cx| {
|
||||
let cwd = project
|
||||
.active_project_directory(cx)
|
||||
.map(|it| it.to_path_buf());
|
||||
project.clone_terminal(self.terminal(), cx, cwd)
|
||||
}) else {
|
||||
return Task::ready(None);
|
||||
};
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let terminal = terminal.await.log_err()?;
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
cx.new(|cx| {
|
||||
TerminalView::new(
|
||||
terminal,
|
||||
this.workspace.clone(),
|
||||
workspace_id,
|
||||
this.project.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
) -> Option<Entity<Self>> {
|
||||
let terminal = self
|
||||
.project
|
||||
.update(cx, |project, cx| {
|
||||
let cwd = project
|
||||
.active_project_directory(cx)
|
||||
.map(|it| it.to_path_buf());
|
||||
project.clone_terminal(self.terminal(), cx, cwd)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.ok()?
|
||||
.log_err()?;
|
||||
|
||||
Some(cx.new(|cx| {
|
||||
TerminalView::new(
|
||||
terminal,
|
||||
self.workspace.clone(),
|
||||
workspace_id,
|
||||
self.project.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &gpui::App) -> bool {
|
||||
|
||||
@@ -11,9 +11,8 @@ use anyhow::Result;
|
||||
use client::{Client, proto};
|
||||
use futures::{StreamExt, channel::mpsc};
|
||||
use gpui::{
|
||||
Action, AnyElement, AnyView, App, AppContext, Context, Entity, EntityId, EventEmitter,
|
||||
FocusHandle, Focusable, Font, HighlightStyle, Pixels, Point, Render, SharedString, Task,
|
||||
WeakEntity, Window,
|
||||
Action, AnyElement, AnyView, App, Context, Entity, EntityId, EventEmitter, FocusHandle,
|
||||
Focusable, Font, HighlightStyle, Pixels, Point, Render, SharedString, Task, WeakEntity, Window,
|
||||
};
|
||||
use project::{Project, ProjectEntryId, ProjectPath};
|
||||
pub use settings::{
|
||||
@@ -218,11 +217,11 @@ pub trait Item: Focusable + EventEmitter<Self::Event> + Render + Sized {
|
||||
_workspace_id: Option<WorkspaceId>,
|
||||
_window: &mut Window,
|
||||
_: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Self>>>
|
||||
) -> Option<Entity<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Task::ready(None)
|
||||
None
|
||||
}
|
||||
fn is_dirty(&self, _: &App) -> bool {
|
||||
false
|
||||
@@ -423,7 +422,7 @@ pub trait ItemHandle: 'static + Send {
|
||||
workspace_id: Option<WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Option<Box<dyn ItemHandle>>>;
|
||||
) -> Option<Box<dyn ItemHandle>>;
|
||||
fn added_to_pane(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
@@ -636,12 +635,9 @@ impl<T: Item> ItemHandle for Entity<T> {
|
||||
workspace_id: Option<WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Option<Box<dyn ItemHandle>>> {
|
||||
let task = self.update(cx, |item, cx| item.clone_on_split(workspace_id, window, cx));
|
||||
cx.background_spawn(async move {
|
||||
task.await
|
||||
.map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
|
||||
})
|
||||
) -> Option<Box<dyn ItemHandle>> {
|
||||
self.update(cx, |item, cx| item.clone_on_split(workspace_id, window, cx))
|
||||
.map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
|
||||
}
|
||||
|
||||
fn added_to_pane(
|
||||
@@ -1508,11 +1504,11 @@ pub mod test {
|
||||
_workspace_id: Option<WorkspaceId>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Self>>>
|
||||
) -> Option<Entity<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Task::ready(Some(cx.new(|cx| Self {
|
||||
Some(cx.new(|cx| Self {
|
||||
state: self.state.clone(),
|
||||
label: self.label.clone(),
|
||||
save_count: self.save_count,
|
||||
@@ -1529,7 +1525,7 @@ pub mod test {
|
||||
workspace_id: self.workspace_id,
|
||||
focus_handle: cx.focus_handle(),
|
||||
serialize: None,
|
||||
})))
|
||||
}))
|
||||
}
|
||||
|
||||
fn is_dirty(&self, _: &App) -> bool {
|
||||
|
||||
@@ -3295,18 +3295,11 @@ impl Pane {
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let task = item.clone_on_split(database_id, window, cx);
|
||||
let to_pane = to_pane.downgrade();
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
if let Some(item) = task.await {
|
||||
to_pane
|
||||
.update_in(cx, |pane, window, cx| {
|
||||
pane.add_item(item, true, true, None, window, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
if let Some(item) = item.clone_on_split(database_id, window, cx) {
|
||||
to_pane.update(cx, |pane, cx| {
|
||||
pane.add_item(item, true, true, None, window, cx);
|
||||
})
|
||||
}
|
||||
} else {
|
||||
move_item(&from_pane, &to_pane, item_id, ix, true, window, cx);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use call::{RemoteVideoTrack, RemoteVideoTrackView, Room};
|
||||
use client::{User, proto::PeerId};
|
||||
use gpui::{
|
||||
AppContext as _, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
|
||||
ParentElement, Render, SharedString, Styled, Task, div,
|
||||
ParentElement, Render, SharedString, Styled, div,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use ui::{Icon, IconName, prelude::*};
|
||||
@@ -114,14 +114,14 @@ impl Item for SharedScreen {
|
||||
_workspace_id: Option<WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Self>>> {
|
||||
Task::ready(Some(cx.new(|cx| Self {
|
||||
) -> Option<Entity<Self>> {
|
||||
Some(cx.new(|cx| Self {
|
||||
view: self.view.update(cx, |view, cx| view.clone(window, cx)),
|
||||
peer_id: self.peer_id,
|
||||
user: self.user.clone(),
|
||||
nav_history: Default::default(),
|
||||
focus: cx.focus_handle(),
|
||||
})))
|
||||
}))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#![allow(unused, dead_code)]
|
||||
use gpui::{
|
||||
AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Hsla, Task, actions, hsla,
|
||||
};
|
||||
use gpui::{AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Hsla, actions, hsla};
|
||||
use strum::IntoEnumIterator;
|
||||
use theme::all_theme_colors;
|
||||
use ui::{
|
||||
@@ -102,11 +100,11 @@ impl Item for ThemePreview {
|
||||
_workspace_id: Option<crate::WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Self>>>
|
||||
) -> Option<Entity<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Task::ready(Some(cx.new(|cx| Self::new(window, cx))))
|
||||
Some(cx.new(|cx| Self::new(window, cx)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3627,8 +3627,7 @@ impl Workspace {
|
||||
if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
|
||||
window.focus(&pane.focus_handle(cx));
|
||||
} else {
|
||||
self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx)
|
||||
.detach();
|
||||
self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3995,8 +3994,7 @@ impl Workspace {
|
||||
clone_active_item,
|
||||
} => {
|
||||
if *clone_active_item {
|
||||
self.split_and_clone(pane.clone(), *direction, window, cx)
|
||||
.detach();
|
||||
self.split_and_clone(pane.clone(), *direction, window, cx);
|
||||
} else {
|
||||
self.split_and_move(pane.clone(), *direction, window, cx);
|
||||
}
|
||||
@@ -4137,27 +4135,21 @@ impl Workspace {
|
||||
direction: SplitDirection,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Pane>>> {
|
||||
let Some(item) = pane.read(cx).active_item() else {
|
||||
return Task::ready(None);
|
||||
};
|
||||
let task = item.clone_on_split(self.database_id(), window, cx);
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Some(clone) = task.await {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
let new_pane = this.add_pane(window, cx);
|
||||
new_pane.update(cx, |pane, cx| {
|
||||
pane.add_item(clone, true, true, None, window, cx)
|
||||
});
|
||||
this.center.split(&pane, &new_pane, direction).unwrap();
|
||||
cx.notify();
|
||||
new_pane
|
||||
})
|
||||
.ok()
|
||||
) -> Option<Entity<Pane>> {
|
||||
let item = pane.read(cx).active_item()?;
|
||||
let maybe_pane_handle =
|
||||
if let Some(clone) = item.clone_on_split(self.database_id(), window, cx) {
|
||||
let new_pane = self.add_pane(window, cx);
|
||||
new_pane.update(cx, |pane, cx| {
|
||||
pane.add_item(clone, true, true, None, window, cx)
|
||||
});
|
||||
self.center.split(&pane, &new_pane, direction).unwrap();
|
||||
cx.notify();
|
||||
Some(new_pane)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
};
|
||||
maybe_pane_handle
|
||||
}
|
||||
|
||||
pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -8191,27 +8183,19 @@ pub fn clone_active_item(
|
||||
let Some(active_item) = source.read(cx).active_item() else {
|
||||
return;
|
||||
};
|
||||
let destination = destination.downgrade();
|
||||
let task = active_item.clone_on_split(workspace_id, window, cx);
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let Some(clone) = task.await else {
|
||||
return;
|
||||
};
|
||||
destination
|
||||
.update_in(cx, |target_pane, window, cx| {
|
||||
target_pane.add_item(
|
||||
clone,
|
||||
focus_destination,
|
||||
focus_destination,
|
||||
Some(target_pane.items_len()),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
destination.update(cx, |target_pane, cx| {
|
||||
let Some(clone) = active_item.clone_on_split(workspace_id, window, cx) else {
|
||||
return;
|
||||
};
|
||||
target_pane.add_item(
|
||||
clone,
|
||||
focus_destination,
|
||||
focus_destination,
|
||||
Some(target_pane.items_len()),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -8718,24 +8702,25 @@ mod tests {
|
||||
cx,
|
||||
);
|
||||
|
||||
let right_pane =
|
||||
workspace.split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx);
|
||||
let right_pane = workspace
|
||||
.split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
|
||||
.unwrap();
|
||||
|
||||
let boxed_clone = single_entry_items[1].boxed_clone();
|
||||
let right_pane = window.spawn(cx, async move |cx| {
|
||||
right_pane.await.inspect(|right_pane| {
|
||||
right_pane
|
||||
.update_in(cx, |pane, window, cx| {
|
||||
pane.add_item(boxed_clone, true, true, None, window, cx);
|
||||
pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
|
||||
})
|
||||
.unwrap();
|
||||
})
|
||||
right_pane.update(cx, |pane, cx| {
|
||||
pane.add_item(
|
||||
single_entry_items[1].boxed_clone(),
|
||||
true,
|
||||
true,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
|
||||
});
|
||||
|
||||
(left_pane, right_pane)
|
||||
});
|
||||
let right_pane = right_pane.await.unwrap();
|
||||
|
||||
cx.focus(&right_pane);
|
||||
|
||||
let mut close = right_pane.update_in(cx, |pane, window, cx| {
|
||||
@@ -10552,10 +10537,7 @@ mod tests {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
assert_eq!(workspace.panes.len(), 3, "Two new panes were created");
|
||||
for pane in workspace.panes() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.210.0"
|
||||
version = "0.210.2"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
preview
|
||||
@@ -2839,16 +2839,14 @@ mod tests {
|
||||
});
|
||||
|
||||
// Split the pane with the first entry, then open the second entry again.
|
||||
let (task1, task2) = window
|
||||
window
|
||||
.update(cx, |w, window, cx| {
|
||||
(
|
||||
w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx),
|
||||
w.open_path(file2.clone(), None, true, window, cx),
|
||||
)
|
||||
w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx);
|
||||
w.open_path(file2.clone(), None, true, window, cx)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
task1.await.unwrap();
|
||||
task2.await.unwrap();
|
||||
|
||||
window
|
||||
.read_with(cx, |w, cx| {
|
||||
@@ -3471,13 +3469,7 @@ mod tests {
|
||||
SplitDirection::Right,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
window
|
||||
.update(cx, |workspace, window, cx| {
|
||||
);
|
||||
workspace.open_path(
|
||||
(worktree.read(cx).id(), rel_path("the-new-name.rs")),
|
||||
None,
|
||||
|
||||
@@ -720,7 +720,7 @@ impl Item for ComponentPreview {
|
||||
_workspace_id: Option<WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<gpui::Entity<Self>>>
|
||||
) -> Option<gpui::Entity<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -742,13 +742,13 @@ impl Item for ComponentPreview {
|
||||
cx,
|
||||
);
|
||||
|
||||
Task::ready(match self_result {
|
||||
match self_result {
|
||||
Ok(preview) => Some(cx.new(|_cx| preview)),
|
||||
Err(e) => {
|
||||
log::error!("Failed to clone component preview: {}", e);
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
|
||||
|
||||
Reference in New Issue
Block a user