Compare commits

...

17 Commits

Author SHA1 Message Date
Zed Bot
a16d556398 Bump to 0.210.4 for @osiewicz 2025-10-27 14:25:44 +00:00
Piotr Osiewicz
cbb803c7b5 lsp: Support tracking multiple registrations of diagnostic providers (#41096)
Closes #40966
Closes #41195
Closes #40980 

Release Notes:

- Fixed diagnostics not working with basedpyright/pyright beyond an
initial version of the document

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: dino <dinojoaocosta@gmail.com>
2025-10-27 15:01:07 +01:00
Zed Bot
64ae3bfccc Bump to 0.210.3 for @Veykril 2025-10-27 10:55:55 +00:00
Lukas Wirth
2be70b7cdb fix: Edit predictions using stale snapshot
Co-authored-by: David Kleingeld <davidsk@zed.dev>
2025-10-27 11:50:29 +01:00
Ben Brandt
9a01c7e7d1 acp: Start sending Client Info to the Agent (#41265)
Updates to acp crate 0.7, which allows us to send information about the
client to the Agent.
In the future, we can also use the AgentInfo on the response for
internal metrics.

Release Notes:

- N/A
2025-10-27 11:11:16 +01:00
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
63 changed files with 1301 additions and 1126 deletions

12
Cargo.lock generated
View File

@@ -210,9 +210,9 @@ dependencies = [
[[package]]
name = "agent-client-protocol"
version = "0.5.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f655394a107cd601bd2e5375c2d909ea83adc65678a0e0e8d77613d3c848a7d"
checksum = "525705e39c11cd73f7bc784e3681a9386aa30c8d0630808d3dc2237eb4f9cb1b"
dependencies = [
"agent-client-protocol-schema",
"anyhow",
@@ -228,9 +228,9 @@ dependencies = [
[[package]]
name = "agent-client-protocol-schema"
version = "0.4.11"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61be4454304d7df1a5b44c4ae55e707ffe72eac4dfb1ef8762510ce8d8f6d924"
checksum = "ecf16c18fea41282d6bbadd1549a06be6836bddb1893f44a6235f340fa24e2af"
dependencies = [
"anyhow",
"derive_more 2.0.1",
@@ -266,6 +266,7 @@ dependencies = [
"log",
"nix 0.29.0",
"project",
"release_channel",
"reqwest_client",
"serde",
"serde_json",
@@ -14156,7 +14157,6 @@ dependencies = [
"log",
"rand 0.9.2",
"rayon",
"smallvec",
"sum_tree",
"unicode-segmentation",
"util",
@@ -20918,7 +20918,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.210.0"
version = "0.210.4"
dependencies = [
"acp_tools",
"activity_indicator",

View File

@@ -438,7 +438,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
agent-client-protocol = { version = "0.5.0", features = ["unstable"] }
agent-client-protocol = { version = "0.7.0", features = ["unstable"] }
aho-corasick = "1.1"
alacritty_terminal = "0.25.1-rc1"
any_vec = "0.14"

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

@@ -1105,13 +1105,13 @@ impl AcpThread {
cx: &mut Context<Self>,
) -> Result<(), acp::Error> {
match update {
acp::SessionUpdate::UserMessageChunk { content } => {
acp::SessionUpdate::UserMessageChunk(acp::ContentChunk { content, .. }) => {
self.push_user_content_block(None, content, cx);
}
acp::SessionUpdate::AgentMessageChunk { content } => {
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk { content, .. }) => {
self.push_assistant_content_block(content, false, cx);
}
acp::SessionUpdate::AgentThoughtChunk { content } => {
acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk { content, .. }) => {
self.push_assistant_content_block(content, true, cx);
}
acp::SessionUpdate::ToolCall(tool_call) => {
@@ -1123,12 +1123,14 @@ impl AcpThread {
acp::SessionUpdate::Plan(plan) => {
self.update_plan(plan, cx);
}
acp::SessionUpdate::AvailableCommandsUpdate { available_commands } => {
cx.emit(AcpThreadEvent::AvailableCommandsUpdated(available_commands))
}
acp::SessionUpdate::CurrentModeUpdate { current_mode_id } => {
cx.emit(AcpThreadEvent::ModeUpdated(current_mode_id))
}
acp::SessionUpdate::AvailableCommandsUpdate(acp::AvailableCommandsUpdate {
available_commands,
..
}) => cx.emit(AcpThreadEvent::AvailableCommandsUpdated(available_commands)),
acp::SessionUpdate::CurrentModeUpdate(acp::CurrentModeUpdate {
current_mode_id,
..
}) => cx.emit(AcpThreadEvent::ModeUpdated(current_mode_id)),
}
Ok(())
}
@@ -2586,17 +2588,19 @@ mod tests {
thread.update(&mut cx, |thread, cx| {
thread
.handle_session_update(
acp::SessionUpdate::AgentThoughtChunk {
acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk {
content: "Thinking ".into(),
},
meta: None,
}),
cx,
)
.unwrap();
thread
.handle_session_update(
acp::SessionUpdate::AgentThoughtChunk {
acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk {
content: "hard!".into(),
},
meta: None,
}),
cx,
)
.unwrap();
@@ -3095,9 +3099,10 @@ mod tests {
thread.update(&mut cx, |thread, cx| {
thread
.handle_session_update(
acp::SessionUpdate::AgentMessageChunk {
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
content: content.text.to_uppercase().into(),
},
meta: None,
}),
cx,
)
.unwrap();
@@ -3454,9 +3459,10 @@ mod tests {
thread.update(&mut cx, |thread, cx| {
thread
.handle_session_update(
acp::SessionUpdate::AgentMessageChunk {
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
content: content.text.to_uppercase().into(),
},
meta: None,
}),
cx,
)
.unwrap();

View File

@@ -38,6 +38,7 @@ language_model.workspace = true
language_models.workspace = true
log.workspace = true
project.workspace = true
release_channel.workspace = true
reqwest_client = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true

View File

@@ -107,6 +107,14 @@ impl AcpConnection {
let sessions = Rc::new(RefCell::new(HashMap::default()));
let (release_channel, version) = cx.update(|cx| {
(
release_channel::ReleaseChannel::try_global(cx)
.map(|release_channel| release_channel.display_name()),
release_channel::AppVersion::global(cx).to_string(),
)
})?;
let client = ClientDelegate {
sessions: sessions.clone(),
cx: cx.clone(),
@@ -174,6 +182,11 @@ impl AcpConnection {
"terminal_output": true,
})),
},
client_info: Some(acp::Implementation {
name: "zed".to_owned(),
title: release_channel.map(|c| c.to_owned()),
version,
}),
meta: None,
})
.await?;
@@ -702,7 +715,11 @@ impl acp::Client for ClientDelegate {
.get(&notification.session_id)
.context("Failed to get session")?;
if let acp::SessionUpdate::CurrentModeUpdate { current_mode_id } = &notification.update {
if let acp::SessionUpdate::CurrentModeUpdate(acp::CurrentModeUpdate {
current_mode_id,
..
}) = &notification.update
{
if let Some(session_modes) = &session.session_modes {
session_modes.borrow_mut().current_mode_id = current_mode_id.clone();
} else {

View File

@@ -6005,9 +6005,12 @@ pub(crate) mod tests {
impl StubAgentServer<StubAgentConnection> {
fn default_response() -> Self {
let conn = StubAgentConnection::new();
conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk {
content: "Default response".into(),
}]);
conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk {
content: "Default response".into(),
meta: None,
},
)]);
Self::new(conn)
}
}
@@ -6358,13 +6361,16 @@ pub(crate) mod tests {
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
meta: None,
}),
}]);
},
)]);
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
add_to_workspace(thread_view.clone(), cx);
@@ -6448,13 +6454,16 @@ pub(crate) mod tests {
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
meta: None,
}),
}]);
},
)]);
let (thread_view, cx) =
setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
@@ -6492,13 +6501,16 @@ pub(crate) mod tests {
});
// Send
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "New Response".into(),
annotations: None,
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "New Response".into(),
annotations: None,
meta: None,
}),
meta: None,
}),
}]);
},
)]);
user_message_editor.update_in(cx, |_editor, window, cx| {
window.dispatch_action(Box::new(Chat), cx);
@@ -6585,13 +6597,14 @@ pub(crate) mod tests {
cx.update(|_, cx| {
connection.send_update(
session_id.clone(),
acp::SessionUpdate::AgentMessageChunk {
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
},
meta: None,
}),
cx,
);
connection.end_turn(session_id, acp::StopReason::EndTurn);
@@ -6643,9 +6656,10 @@ pub(crate) mod tests {
cx.update(|_, cx| {
connection.send_update(
session_id.clone(),
acp::SessionUpdate::AgentMessageChunk {
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
content: "Message 1 resp".into(),
},
meta: None,
}),
cx,
);
});
@@ -6679,9 +6693,10 @@ pub(crate) mod tests {
// Simulate a response sent after beginning to cancel
connection.send_update(
session_id.clone(),
acp::SessionUpdate::AgentMessageChunk {
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
content: "onse".into(),
},
meta: None,
}),
cx,
);
});
@@ -6712,9 +6727,10 @@ pub(crate) mod tests {
cx.update(|_, cx| {
connection.send_update(
session_id.clone(),
acp::SessionUpdate::AgentMessageChunk {
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
content: "Message 2 response".into(),
},
meta: None,
}),
cx,
);
connection.end_turn(session_id.clone(), acp::StopReason::EndTurn);
@@ -6752,13 +6768,16 @@ pub(crate) mod tests {
init_test(cx);
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
meta: None,
}),
}]);
},
)]);
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
add_to_workspace(thread_view.clone(), cx);
@@ -6835,13 +6854,16 @@ pub(crate) mod tests {
init_test(cx);
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
meta: None,
}),
}]);
},
)]);
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
add_to_workspace(thread_view.clone(), cx);

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

@@ -2600,7 +2600,7 @@ async fn test_lsp_pull_diagnostics(
capabilities: capabilities.clone(),
initializer: Some(Box::new(move |fake_language_server| {
let expected_workspace_diagnostic_token = lsp::ProgressToken::String(format!(
"workspace/diagnostic-{}-1",
"workspace/diagnostic/{}/1",
fake_language_server.server.server_id()
));
let closure_workspace_diagnostics_pulls_result_ids = closure_workspace_diagnostics_pulls_result_ids.clone();

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

@@ -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;
@@ -163,9 +162,7 @@ use project::{
use rand::seq::SliceRandom;
use rpc::{ErrorCode, ErrorExt, proto::PeerId};
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager};
use selections_collection::{
MutableSelectionsCollection, SelectionsCollection, resolve_selections,
};
use selections_collection::{MutableSelectionsCollection, SelectionsCollection};
use serde::{Deserialize, Serialize};
use settings::{GitGutterSetting, Settings, SettingsLocation, SettingsStore, update_settings_file};
use smallvec::{SmallVec, smallvec};
@@ -209,6 +206,7 @@ use crate::{
editor_settings::MultiCursorModifier,
hover_links::{find_url, find_url_from_range},
scroll::{ScrollOffset, ScrollPixelOffset},
selections_collection::resolve_selections_wrapping_blocks,
signature_help::{SignatureHelpHiddenBy, SignatureHelpState},
};
@@ -3189,7 +3187,7 @@ impl Editor {
refresh_linked_ranges(self, window, cx);
self.refresh_selected_text_highlights(false, window, cx);
refresh_matching_bracket_highlights(self, cx);
self.refresh_matching_bracket_highlights(window, cx);
self.update_visible_edit_prediction(window, cx);
self.edit_prediction_requires_modifier_in_indent_conflict = true;
self.inline_blame_popover.take();
@@ -4373,16 +4371,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 {
@@ -6974,6 +6973,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 +6985,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(
@@ -7155,7 +7154,8 @@ impl Editor {
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();
@@ -20965,7 +20965,7 @@ impl Editor {
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_matching_bracket_highlights(window, cx);
if self.has_active_edit_prediction() {
self.update_visible_edit_prediction(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,60 @@
use crate::{Editor, RangeToAnchorExt};
use gpui::{Context, HighlightStyle};
use gpui::{Context, HighlightStyle, Window};
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,
window: &Window,
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 snapshot = self.snapshot(window, cx);
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

@@ -26,8 +26,8 @@ use language::{
use lsp::{
AdapterServerCapabilities, CodeActionKind, CodeActionOptions, CodeDescription,
CompletionContext, CompletionListItemDefaultsEditRange, CompletionTriggerKind,
DocumentHighlightKind, LanguageServer, LanguageServerId, LinkedEditingRangeServerCapabilities,
OneOf, RenameOptions, ServerCapabilities,
DiagnosticServerCapabilities, DocumentHighlightKind, LanguageServer, LanguageServerId,
LinkedEditingRangeServerCapabilities, OneOf, RenameOptions, ServerCapabilities,
};
use serde_json::Value;
use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature};
@@ -262,6 +262,9 @@ pub(crate) struct LinkedEditingRange {
#[derive(Clone, Debug)]
pub(crate) struct GetDocumentDiagnostics {
/// We cannot blindly rely on server's capabilities.diagnostic_provider, as they're a singular field, whereas
/// a server can register multiple diagnostic providers post-mortem.
pub dynamic_caps: DiagnosticServerCapabilities,
pub previous_result_id: Option<String>,
}
@@ -4031,26 +4034,22 @@ impl LspCommand for GetDocumentDiagnostics {
"Get diagnostics"
}
fn check_capabilities(&self, server_capabilities: AdapterServerCapabilities) -> bool {
server_capabilities
.server_capabilities
.diagnostic_provider
.is_some()
fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool {
true
}
fn to_lsp(
&self,
path: &Path,
_: &Buffer,
language_server: &Arc<LanguageServer>,
_: &Arc<LanguageServer>,
_: &App,
) -> Result<lsp::DocumentDiagnosticParams> {
let identifier = match language_server.capabilities().diagnostic_provider {
Some(lsp::DiagnosticServerCapabilities::Options(options)) => options.identifier,
Some(lsp::DiagnosticServerCapabilities::RegistrationOptions(options)) => {
options.diagnostic_options.identifier
let identifier = match &self.dynamic_caps {
lsp::DiagnosticServerCapabilities::Options(options) => options.identifier.clone(),
lsp::DiagnosticServerCapabilities::RegistrationOptions(options) => {
options.diagnostic_options.identifier.clone()
}
None => None,
};
Ok(lsp::DocumentDiagnosticParams {

View File

@@ -71,12 +71,12 @@ use language::{
range_from_lsp, range_to_lsp,
};
use lsp::{
AdapterServerCapabilities, CodeActionKind, CompletionContext, DiagnosticSeverity,
DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter,
FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher,
LSP_REQUEST_TIMEOUT, LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions,
LanguageServerId, LanguageServerName, LanguageServerSelector, LspRequestFuture,
MessageActionItem, MessageType, OneOf, RenameFilesParams, SymbolKind,
AdapterServerCapabilities, CodeActionKind, CompletionContext, DiagnosticServerCapabilities,
DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, Edit,
FileOperationFilter, FileOperationPatternKind, FileOperationRegistrationOptions, FileRename,
FileSystemWatcher, LSP_REQUEST_TIMEOUT, LanguageServer, LanguageServerBinary,
LanguageServerBinaryOptions, LanguageServerId, LanguageServerName, LanguageServerSelector,
LspRequestFuture, MessageActionItem, MessageType, OneOf, RenameFilesParams, SymbolKind,
TextDocumentSyncSaveOptions, TextEdit, Uri, WillRenameFiles, WorkDoneProgressCancelParams,
WorkspaceFolder, notification::DidRenameFiles,
};
@@ -182,6 +182,12 @@ pub struct DocumentDiagnostics {
version: Option<i32>,
}
#[derive(Default)]
struct DynamicRegistrations {
did_change_watched_files: HashMap<String, Vec<FileSystemWatcher>>,
diagnostics: HashMap<Option<String>, DiagnosticServerCapabilities>,
}
pub struct LocalLspStore {
weak: WeakEntity<LspStore>,
worktree_store: Entity<WorktreeStore>,
@@ -199,8 +205,7 @@ pub struct LocalLspStore {
watched_manifest_filenames: HashSet<ManifestName>,
language_server_paths_watched_for_rename:
HashMap<LanguageServerId, RenamePathsWatchedForServer>,
language_server_watcher_registrations:
HashMap<LanguageServerId, HashMap<String, Vec<FileSystemWatcher>>>,
language_server_dynamic_registrations: HashMap<LanguageServerId, DynamicRegistrations>,
supplementary_language_servers:
HashMap<LanguageServerId, (LanguageServerName, Arc<LanguageServer>)>,
prettier_store: Entity<PrettierStore>,
@@ -3174,7 +3179,7 @@ impl LocalLspStore {
for watcher in watchers {
if let Some((worktree, literal_prefix, pattern)) =
self.worktree_and_path_for_file_watcher(&worktrees, watcher, cx)
Self::worktree_and_path_for_file_watcher(&worktrees, watcher, cx)
{
worktree.update(cx, |worktree, _| {
if let Some((tree, glob)) =
@@ -3272,7 +3277,6 @@ impl LocalLspStore {
}
fn worktree_and_path_for_file_watcher(
&self,
worktrees: &[Entity<Worktree>],
watcher: &FileSystemWatcher,
cx: &App,
@@ -3320,15 +3324,18 @@ impl LocalLspStore {
language_server_id: LanguageServerId,
cx: &mut Context<LspStore>,
) {
let Some(watchers) = self
.language_server_watcher_registrations
let Some(registrations) = self
.language_server_dynamic_registrations
.get(&language_server_id)
else {
return;
};
let watch_builder =
self.rebuild_watched_paths_inner(language_server_id, watchers.values().flatten(), cx);
let watch_builder = self.rebuild_watched_paths_inner(
language_server_id,
registrations.did_change_watched_files.values().flatten(),
cx,
);
let watcher = watch_builder.build(self.fs.clone(), language_server_id, cx);
self.language_server_watched_paths
.insert(language_server_id, watcher);
@@ -3344,11 +3351,13 @@ impl LocalLspStore {
cx: &mut Context<LspStore>,
) {
let registrations = self
.language_server_watcher_registrations
.language_server_dynamic_registrations
.entry(language_server_id)
.or_default();
registrations.insert(registration_id.to_string(), params.watchers);
registrations
.did_change_watched_files
.insert(registration_id.to_string(), params.watchers);
self.rebuild_watched_paths(language_server_id, cx);
}
@@ -3360,11 +3369,15 @@ impl LocalLspStore {
cx: &mut Context<LspStore>,
) {
let registrations = self
.language_server_watcher_registrations
.language_server_dynamic_registrations
.entry(language_server_id)
.or_default();
if registrations.remove(registration_id).is_some() {
if registrations
.did_change_watched_files
.remove(registration_id)
.is_some()
{
log::info!(
"language server {}: unregistered workspace/DidChangeWatchedFiles capability with id {}",
language_server_id,
@@ -3729,7 +3742,7 @@ impl LspStore {
last_workspace_edits_by_language_server: Default::default(),
language_server_watched_paths: Default::default(),
language_server_paths_watched_for_rename: Default::default(),
language_server_watcher_registrations: Default::default(),
language_server_dynamic_registrations: Default::default(),
buffers_being_formatted: Default::default(),
buffer_snapshots: Default::default(),
prettier_store,
@@ -4317,7 +4330,7 @@ impl LspStore {
cx: &Context<Self>,
) -> bool
where
F: Fn(&lsp::ServerCapabilities) -> bool,
F: FnMut(&lsp::ServerCapabilities) -> bool,
{
let Some(language) = buffer.read(cx).language().cloned() else {
return false;
@@ -6319,12 +6332,30 @@ impl LspStore {
let buffer_id = buffer.read(cx).remote_id();
if let Some((client, upstream_project_id)) = self.upstream_client() {
let mut suitable_capabilities = None;
// Are we capable for proto request?
let any_server_has_diagnostics_provider = self.check_if_capable_for_proto_request(
&buffer,
|capabilities| {
if let Some(caps) = &capabilities.diagnostic_provider {
suitable_capabilities = Some(caps.clone());
true
} else {
false
}
},
cx,
);
// We don't really care which caps are passed into the request, as they're ignored by RPC anyways.
let Some(dynamic_caps) = suitable_capabilities else {
return Task::ready(Ok(None));
};
assert!(any_server_has_diagnostics_provider);
let request = GetDocumentDiagnostics {
previous_result_id: None,
dynamic_caps,
};
if !self.is_capable_for_proto_request(&buffer, &request, cx) {
return Task::ready(Ok(None));
}
let request_task = client.request_lsp(
upstream_project_id,
LSP_REQUEST_TIMEOUT,
@@ -6339,23 +6370,44 @@ impl LspStore {
Ok(None)
})
} else {
let server_ids = buffer.update(cx, |buffer, cx| {
let servers = buffer.update(cx, |buffer, cx| {
self.language_servers_for_local_buffer(buffer, cx)
.map(|(_, server)| server.server_id())
.map(|(_, server)| server.clone())
.collect::<Vec<_>>()
});
let pull_diagnostics = server_ids
let pull_diagnostics = servers
.into_iter()
.map(|server_id| {
let result_id = self.result_id(server_id, buffer_id, cx);
self.request_lsp(
buffer.clone(),
LanguageServerToQuery::Other(server_id),
GetDocumentDiagnostics {
previous_result_id: result_id,
},
cx,
)
.flat_map(|server| {
let result = maybe!({
let local = self.as_local()?;
let server_id = server.server_id();
let providers_with_identifiers = local
.language_server_dynamic_registrations
.get(&server_id)
.into_iter()
.flat_map(|registrations| registrations.diagnostics.values().cloned())
.collect::<Vec<_>>();
Some(
providers_with_identifiers
.into_iter()
.map(|dynamic_caps| {
let result_id = self.result_id(server_id, buffer_id, cx);
self.request_lsp(
buffer.clone(),
LanguageServerToQuery::Other(server_id),
GetDocumentDiagnostics {
previous_result_id: result_id,
dynamic_caps,
},
cx,
)
})
.collect::<Vec<_>>(),
)
});
result.unwrap_or_default()
})
.collect::<Vec<_>>();
@@ -8875,14 +8927,17 @@ impl LspStore {
);
}
lsp::ProgressParamsValue::WorkspaceDiagnostic(report) => {
let identifier = token.split_once("id:").map(|(_, id)| id.to_owned());
if let Some(LanguageServerState::Running {
workspace_refresh_task: Some(workspace_refresh_task),
workspace_diagnostics_refresh_tasks,
..
}) = self
.as_local_mut()
.and_then(|local| local.language_servers.get_mut(&language_server_id))
&& let Some(workspace_diagnostics) =
workspace_diagnostics_refresh_tasks.get_mut(&identifier)
{
workspace_refresh_task.progress_tx.try_send(()).ok();
workspace_diagnostics.progress_tx.try_send(()).ok();
self.apply_workspace_diagnostic_report(language_server_id, report, cx)
}
}
@@ -10380,13 +10435,31 @@ impl LspStore {
let workspace_folders = workspace_folders.lock().clone();
language_server.set_workspace_folders(workspace_folders);
let workspace_diagnostics_refresh_tasks = language_server
.capabilities()
.diagnostic_provider
.and_then(|provider| {
let workspace_refresher = lsp_workspace_diagnostics_refresh(
None,
provider.clone(),
language_server.clone(),
cx,
)?;
local
.language_server_dynamic_registrations
.entry(server_id)
.or_default()
.diagnostics
.entry(None)
.or_insert(provider);
Some((None, workspace_refresher))
})
.into_iter()
.collect();
local.language_servers.insert(
server_id,
LanguageServerState::Running {
workspace_refresh_task: lsp_workspace_diagnostics_refresh(
language_server.clone(),
cx,
),
workspace_diagnostics_refresh_tasks,
adapter: adapter.clone(),
server: language_server.clone(),
simulate_disk_based_diagnostics_completion: None,
@@ -11096,13 +11169,15 @@ impl LspStore {
pub fn pull_workspace_diagnostics(&mut self, server_id: LanguageServerId) {
if let Some(LanguageServerState::Running {
workspace_refresh_task: Some(workspace_refresh_task),
workspace_diagnostics_refresh_tasks,
..
}) = self
.as_local_mut()
.and_then(|local| local.language_servers.get_mut(&server_id))
{
workspace_refresh_task.refresh_tx.try_send(()).ok();
for diagnostics in workspace_diagnostics_refresh_tasks.values_mut() {
diagnostics.refresh_tx.try_send(()).ok();
}
}
}
@@ -11118,11 +11193,13 @@ impl LspStore {
local.language_server_ids_for_buffer(buffer, cx)
}) {
if let Some(LanguageServerState::Running {
workspace_refresh_task: Some(workspace_refresh_task),
workspace_diagnostics_refresh_tasks,
..
}) = local.language_servers.get_mut(&server_id)
{
workspace_refresh_task.refresh_tx.try_send(()).ok();
for diagnostics in workspace_diagnostics_refresh_tasks.values_mut() {
diagnostics.refresh_tx.try_send(()).ok();
}
}
}
}
@@ -11448,26 +11525,49 @@ impl LspStore {
"textDocument/diagnostic" => {
if let Some(caps) = reg
.register_options
.map(serde_json::from_value)
.map(serde_json::from_value::<DiagnosticServerCapabilities>)
.transpose()?
{
let state = self
let local = self
.as_local_mut()
.context("Expected LSP Store to be local")?
.context("Expected LSP Store to be local")?;
let state = local
.language_servers
.get_mut(&server_id)
.context("Could not obtain Language Servers state")?;
server.update_capabilities(|capabilities| {
capabilities.diagnostic_provider = Some(caps);
});
local
.language_server_dynamic_registrations
.get_mut(&server_id)
.and_then(|registrations| {
registrations
.diagnostics
.insert(Some(reg.id.clone()), caps.clone())
});
let mut can_now_provide_diagnostics = false;
if let LanguageServerState::Running {
workspace_refresh_task,
workspace_diagnostics_refresh_tasks,
..
} = state
&& workspace_refresh_task.is_none()
&& let Some(task) = lsp_workspace_diagnostics_refresh(
Some(reg.id.clone()),
caps.clone(),
server.clone(),
cx,
)
{
*workspace_refresh_task =
lsp_workspace_diagnostics_refresh(server.clone(), cx)
workspace_diagnostics_refresh_tasks.insert(Some(reg.id), task);
can_now_provide_diagnostics = true;
}
// We don't actually care about capabilities.diagnostic_provider, but it IS relevant for the remote peer
// to know that there's at least one provider. Otherwise, it will never ask us to issue documentdiagnostic calls on their behalf,
// as it'll think that they're not supported.
if can_now_provide_diagnostics {
server.update_capabilities(|capabilities| {
debug_assert!(capabilities.diagnostic_provider.is_none());
capabilities.diagnostic_provider = Some(caps);
});
}
notify_server_capabilities_updated(&server, cx);
@@ -11630,22 +11730,45 @@ impl LspStore {
notify_server_capabilities_updated(&server, cx);
}
"textDocument/diagnostic" => {
server.update_capabilities(|capabilities| {
capabilities.diagnostic_provider = None;
});
let state = self
let local = self
.as_local_mut()
.context("Expected LSP Store to be local")?
.context("Expected LSP Store to be local")?;
let state = local
.language_servers
.get_mut(&server_id)
.context("Could not obtain Language Servers state")?;
if let LanguageServerState::Running {
workspace_refresh_task,
..
} = state
let options = local
.language_server_dynamic_registrations
.get_mut(&server_id)
.with_context(|| {
format!("Expected dynamic registration to exist for server {server_id}")
})?.diagnostics
.remove(&Some(unreg.id.clone()))
.with_context(|| format!(
"Attempted to unregister non-existent diagnostic registration with ID {}",
unreg.id)
)?;
let mut has_any_diagnostic_providers_still = true;
if let Some(identifier) = diagnostic_identifier(&options)
&& let LanguageServerState::Running {
workspace_diagnostics_refresh_tasks,
..
} = state
{
_ = workspace_refresh_task.take();
workspace_diagnostics_refresh_tasks.remove(&identifier);
has_any_diagnostic_providers_still =
!workspace_diagnostics_refresh_tasks.is_empty();
}
if !has_any_diagnostic_providers_still {
server.update_capabilities(|capabilities| {
debug_assert!(capabilities.diagnostic_provider.is_some());
capabilities.diagnostic_provider = None;
});
}
notify_server_capabilities_updated(&server, cx);
}
"textDocument/documentColor" => {
@@ -11831,24 +11954,12 @@ fn subscribe_to_binary_statuses(
}
fn lsp_workspace_diagnostics_refresh(
registration_id: Option<String>,
options: DiagnosticServerCapabilities,
server: Arc<LanguageServer>,
cx: &mut Context<'_, LspStore>,
) -> Option<WorkspaceRefreshTask> {
let identifier = match server.capabilities().diagnostic_provider? {
lsp::DiagnosticServerCapabilities::Options(diagnostic_options) => {
if !diagnostic_options.workspace_diagnostics {
return None;
}
diagnostic_options.identifier
}
lsp::DiagnosticServerCapabilities::RegistrationOptions(registration_options) => {
let diagnostic_options = registration_options.diagnostic_options;
if !diagnostic_options.workspace_diagnostics {
return None;
}
diagnostic_options.identifier
}
};
let identifier = diagnostic_identifier(&options)?;
let (progress_tx, mut progress_rx) = mpsc::channel(1);
let (mut refresh_tx, mut refresh_rx) = mpsc::channel(1);
@@ -11894,7 +12005,14 @@ fn lsp_workspace_diagnostics_refresh(
return;
};
let token = format!("workspace/diagnostic-{}-{}", server.server_id(), requests);
let token = if let Some(identifier) = &registration_id {
format!(
"workspace/diagnostic/{}/{requests}/id:{identifier}",
server.server_id(),
)
} else {
format!("workspace/diagnostic/{}/{requests}", server.server_id())
};
progress_rx.try_recv().ok();
let timer =
@@ -11960,6 +12078,24 @@ fn lsp_workspace_diagnostics_refresh(
})
}
fn diagnostic_identifier(options: &DiagnosticServerCapabilities) -> Option<Option<String>> {
match &options {
lsp::DiagnosticServerCapabilities::Options(diagnostic_options) => {
if !diagnostic_options.workspace_diagnostics {
return None;
}
Some(diagnostic_options.identifier.clone())
}
lsp::DiagnosticServerCapabilities::RegistrationOptions(registration_options) => {
let diagnostic_options = &registration_options.diagnostic_options;
if !diagnostic_options.workspace_diagnostics {
return None;
}
Some(diagnostic_options.identifier.clone())
}
}
}
fn resolve_word_completion(snapshot: &BufferSnapshot, completion: &mut Completion) {
let CompletionSource::BufferWord {
word_range,
@@ -12364,7 +12500,7 @@ pub enum LanguageServerState {
adapter: Arc<CachedLspAdapter>,
server: Arc<LanguageServer>,
simulate_disk_based_diagnostics_completion: Option<Task<()>>,
workspace_refresh_task: Option<WorkspaceRefreshTask>,
workspace_diagnostics_refresh_tasks: HashMap<Option<String>, WorkspaceRefreshTask>,
},
}

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.4"
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)) {