Compare commits

...

12 Commits

Author SHA1 Message Date
Zed Bot
9844631b27 Bump to 0.210.2 for @Veykril 2025-10-27 08:50:14 +00:00
Lukas Wirth
1d06e26b92 editor: Fix panics in CursorPosition::update_position (#41237)
Fixes a regression introduced in
https://github.com/zed-industries/zed/pull/39857. As for the exact
reason this causes this issue I am not yet sure will investigate (as per
the todos in code)

Fixes ZED-23R

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-27 09:40:34 +01:00
Lukas Wirth
282b7b4309 settings: Fix out of bounds index 2025-10-26 13:45:27 +01:00
Lukas Wirth
fade12155d gpui: Fix TextLayout::layout producing invalid text runs (#41224)
The issues is that the closure supplied to `request_measured_layout`
could be run multiple times with differing `known_dimensions`. This in
turn will mean we truncate the font runs once, inserting a multibyte
char at the end but then in the next iteration use those truncated runs
for possible the original untruncated string which has multibyte
characters overlapping the now truncated run end resulting in a faulty
utf8 boundary index.

Solution to this is simple, truncate a clone of the runs when needed
instead of modifying the original.

Fixes https://github.com/zed-industries/zed/issues/36925
Fixed ZED-2FF
Fixes ZED-2KM
Fixes ZED-2KK
Fixes ZED-1FF
Fixes ZED-255
Fixes ZED-2JD
Fixes ZED-2FX
Fixes ZED-2K2
Fixes ZED-2JX
Fixes ZED-2GE
Fixes ZED-2FC
Fixes ZED-2GD
Fixes ZED-2HY
Fixes ZED-2HR
Fixes ZED-2FN
Fixes ZED-2GT
Fixes ZED-2FK
Fixes ZED-2EY
Fixes ZED-27E
Fixes ZED-272
Fixes ZED-2EM
Fixes ZED-2CC
Fixes ZED-29V
Fixes ZED-25B
Fixes ZED-257
Fixes ZED-24R
Fixes ZED-24Q
Fixes ZED-23Z
Fixes ZED-227

Release Notes:

- Fixed a crash in text shaping when truncating rendered text
2025-10-26 13:18:16 +01:00
Joseph T. Lyons
18f31e8bf9 zed 0.210.1 2025-10-24 13:34:48 -04:00
Ben Kunkle
780d60bc9d Fix include ignored migration rerunning (#41114)
Closes #ISSUE

Release Notes:

- Fixed an issue where having a correct `file_finder.include_ignored`
setting would result in failed to migrate errors
2025-10-24 13:07:31 -04:00
Agus Zubiaga
08ba1939c6 Revert: Spawn terminal process on background executor (#41060)
Reverts https://github.com/zed-industries/zed/pull/40774 and
https://github.com/zed-industries/zed/pull/40824 since they introduce a
bug where Nushell processes are leaked and Ctrl+C doesn't kill the
current process.

Release Notes:

- Fix a bug where nushell processes wouldn't get killed after closing a
terminal tab
2025-10-24 12:36:27 -04:00
Kirill Bulatov
c4858f8ae6 Revert "Round the scroll offset in editor to fix jumping text (#40401)" (#40982)
This reverts commit 3da4cddce2.

The scrolling is ~30% less for the same gesture, and I'm not using
anything lodpi:


https://github.com/user-attachments/assets/b19521fc-9e29-4bfd-9660-dc1e4c8ae846


Release Notes:

- N/A
2025-10-23 14:18:26 +03:00
Lukas Wirth
f3768cfc40 acp_thread: Fix panic when following acp agents across buffers (#40798)
Fixes ZED-2D7

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-22 23:39:48 +02:00
Ben Kunkle
80d1551671 Don't migrate empty formatter array (#40932)
Follow up for #40409
Fix for
https://github.com/zed-industries/zed/issues/40874#issuecomment-3433759849

Release Notes:

- Fixed an issue where having an empty formatter array in your settings
`"formatter": []` would result in an erroneous prompt to migrate
settings
2025-10-22 16:10:05 -04:00
David Kleingeld
00a14349de Revert "keymaps: Update defaults for inline assist and signature help" (#40903)
Reverts zed-industries/zed#39587
2025-10-22 11:28:21 -04:00
Joseph T. Lyons
aa8128e2ff v0.210.x preview 2025-10-22 10:15:37 -04:00
57 changed files with 1036 additions and 1003 deletions

3
Cargo.lock generated
View File

@@ -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",

View File

@@ -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."],

View File

@@ -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

View File

@@ -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."],

View File

@@ -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 {

View File

@@ -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())

View File

@@ -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| {

View File

@@ -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(

View File

@@ -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>) {

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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>),
}

View File

@@ -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, _, _| {

View File

@@ -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();

View File

@@ -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)
});

View File

@@ -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();

View File

@@ -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,
)
}
}
}

View File

@@ -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(

View File

@@ -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()

View File

@@ -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,
}
})))
}))
}
}

View File

@@ -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 {

View File

@@ -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));
}
}

View File

@@ -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();

View File

@@ -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.

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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)))
}
}

View File

@@ -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
})))
}))
}
}

View File

@@ -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
})))
}))
}
}

View File

@@ -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 {

View File

@@ -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(())

View File

@@ -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]

View File

@@ -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"))]

View File

@@ -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)) {

View File

@@ -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>(

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)]

View File

@@ -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();
}

View File

@@ -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!(

View File

@@ -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');

View File

@@ -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};

View File

@@ -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(&params.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(&params.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| {

View File

@@ -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)]

View File

@@ -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()?

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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)) {

View File

@@ -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)))
}
}

View File

@@ -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!(

View File

@@ -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>"]

View File

@@ -1 +1 @@
dev
preview

View File

@@ -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,

View File

@@ -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)) {