mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Merge 1add778d60 into 09165c15dc
This commit is contained in:
commit
d4d841d995
14 changed files with 905 additions and 31 deletions
|
|
@ -292,6 +292,14 @@
|
|||
"ctrl-alt-shift-pagedown": "agent::ScrollOutputToNextMessage",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > MessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "agent::PromptHistoryPrevious",
|
||||
"down": "agent::PromptHistoryNext",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor",
|
||||
"use_key_equivalents": true,
|
||||
|
|
|
|||
|
|
@ -335,6 +335,14 @@
|
|||
"ctrl-alt-pagedown": "agent::ScrollOutputToNextMessage",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > MessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "agent::PromptHistoryPrevious",
|
||||
"down": "agent::PromptHistoryNext",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor",
|
||||
"use_key_equivalents": true,
|
||||
|
|
|
|||
|
|
@ -293,6 +293,14 @@
|
|||
"ctrl-alt-shift-pagedown": "agent::ScrollOutputToNextMessage",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > MessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "agent::PromptHistoryPrevious",
|
||||
"down": "agent::PromptHistoryNext",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor",
|
||||
"use_key_equivalents": true,
|
||||
|
|
|
|||
|
|
@ -159,6 +159,8 @@
|
|||
//
|
||||
// Default: true
|
||||
"restore_on_file_reopen": true,
|
||||
// Whether to restore open tabs and pane layout per active git branch.
|
||||
"restore_tabs_per_branch": true,
|
||||
// Whether to automatically close files that have been deleted on disk.
|
||||
"close_on_file_delete": false,
|
||||
// Whether toggling a panel (e.g. with its keyboard shortcut) also closes
|
||||
|
|
|
|||
|
|
@ -259,6 +259,10 @@ actions!(
|
|||
EditFirstQueuedMessage,
|
||||
/// Clears all messages from the queue.
|
||||
ClearMessageQueue,
|
||||
/// Recalls the previous submitted prompt in the focused message editor.
|
||||
PromptHistoryPrevious,
|
||||
/// Moves toward newer prompt history in the focused message editor.
|
||||
PromptHistoryNext,
|
||||
/// Opens the permission granularity dropdown for the current tool call.
|
||||
OpenPermissionDropdown,
|
||||
/// Toggles thinking mode for models that support extended thinking.
|
||||
|
|
|
|||
|
|
@ -975,7 +975,7 @@ impl ThreadView {
|
|||
|
||||
pub fn handle_message_editor_event(
|
||||
&mut self,
|
||||
_editor: &Entity<MessageEditor>,
|
||||
editor: &Entity<MessageEditor>,
|
||||
event: &MessageEditorEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
|
|
@ -1002,6 +1002,20 @@ impl ThreadView {
|
|||
MessageEditorEvent::Send => self.send(window, cx),
|
||||
MessageEditorEvent::SendImmediately => self.interrupt_and_send(window, cx),
|
||||
MessageEditorEvent::Cancel => self.cancel_generation(cx),
|
||||
MessageEditorEvent::PromptHistoryPreviousRequested => {
|
||||
let history = self.submitted_prompt_history(cx);
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_prompt_history(history);
|
||||
editor.navigate_prompt_history_previous(window, cx);
|
||||
});
|
||||
}
|
||||
MessageEditorEvent::PromptHistoryNextRequested => {
|
||||
let history = self.submitted_prompt_history(cx);
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_prompt_history(history);
|
||||
editor.navigate_prompt_history_next(window, cx);
|
||||
});
|
||||
}
|
||||
MessageEditorEvent::Focus => {
|
||||
self.cancel_editing(&Default::default(), window, cx);
|
||||
}
|
||||
|
|
@ -1083,6 +1097,18 @@ impl ThreadView {
|
|||
!self.local_queued_messages.is_empty()
|
||||
}
|
||||
|
||||
fn submitted_prompt_history(&self, cx: &App) -> Vec<Vec<acp::ContentBlock>> {
|
||||
self.thread
|
||||
.read(cx)
|
||||
.entries()
|
||||
.iter()
|
||||
.filter_map(|entry| match entry {
|
||||
AgentThreadEntry::UserMessage(user_message) => Some(user_message.chunks.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn is_imported_thread(&self, cx: &App) -> bool {
|
||||
let Some(thread) = self.as_native_thread(cx) else {
|
||||
return false;
|
||||
|
|
@ -1146,6 +1172,14 @@ impl ThreadView {
|
|||
ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Cancel) => {
|
||||
self.cancel_editing(&Default::default(), window, cx);
|
||||
}
|
||||
ViewEvent::MessageEditorEvent(
|
||||
_editor,
|
||||
MessageEditorEvent::PromptHistoryPreviousRequested,
|
||||
) => {}
|
||||
ViewEvent::MessageEditorEvent(
|
||||
_editor,
|
||||
MessageEditorEvent::PromptHistoryNextRequested,
|
||||
) => {}
|
||||
ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::SlashAutocompleteOpened) => {
|
||||
}
|
||||
ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::InputAttempted { .. }) => {}
|
||||
|
|
|
|||
|
|
@ -188,10 +188,18 @@ pub struct MessageEditor {
|
|||
session_capabilities: SharedSessionCapabilities,
|
||||
agent_id: AgentId,
|
||||
thread_store: Option<Entity<ThreadStore>>,
|
||||
prompt_history: Vec<Vec<acp::ContentBlock>>,
|
||||
prompt_history_state: PromptHistoryState,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
_parse_slash_command_task: Task<()>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct PromptHistoryState {
|
||||
saved_draft: Option<Vec<acp::ContentBlock>>,
|
||||
history_index: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum InputAttempt {
|
||||
Text(Arc<str>),
|
||||
|
|
@ -213,6 +221,8 @@ pub enum MessageEditorEvent {
|
|||
attempt: InputAttempt,
|
||||
cursor_offset: usize,
|
||||
},
|
||||
PromptHistoryPreviousRequested,
|
||||
PromptHistoryNextRequested,
|
||||
}
|
||||
|
||||
impl EventEmitter<MessageEditorEvent> for MessageEditor {}
|
||||
|
|
@ -605,6 +615,8 @@ impl MessageEditor {
|
|||
session_capabilities,
|
||||
agent_id,
|
||||
thread_store,
|
||||
prompt_history: Vec::new(),
|
||||
prompt_history_state: PromptHistoryState::default(),
|
||||
_subscriptions: subscriptions,
|
||||
_parse_slash_command_task: Task::ready(()),
|
||||
}
|
||||
|
|
@ -920,6 +932,7 @@ impl MessageEditor {
|
|||
}
|
||||
|
||||
pub fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.reset_prompt_history_navigation();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.clear(window, cx);
|
||||
editor.remove_creases(
|
||||
|
|
@ -940,6 +953,7 @@ impl MessageEditor {
|
|||
editor.clear_inlay_hints(cx);
|
||||
});
|
||||
}
|
||||
self.reset_prompt_history_navigation();
|
||||
cx.emit(MessageEditorEvent::Send)
|
||||
}
|
||||
|
||||
|
|
@ -1006,9 +1020,48 @@ impl MessageEditor {
|
|||
editor.clear_inlay_hints(cx);
|
||||
});
|
||||
|
||||
self.reset_prompt_history_navigation();
|
||||
cx.emit(MessageEditorEvent::SendImmediately)
|
||||
}
|
||||
|
||||
fn prompt_history_previous(
|
||||
&mut self,
|
||||
_: &crate::PromptHistoryPrevious,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.has_visible_completions_menu(cx) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
if !self.cursor_is_at_buffer_start(cx) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
cx.emit(MessageEditorEvent::PromptHistoryPreviousRequested);
|
||||
}
|
||||
|
||||
fn prompt_history_next(
|
||||
&mut self,
|
||||
_: &crate::PromptHistoryNext,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.has_visible_completions_menu(cx) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
if !self.is_navigating_prompt_history() {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
cx.emit(MessageEditorEvent::PromptHistoryNextRequested);
|
||||
}
|
||||
|
||||
fn chat_with_follow(
|
||||
&mut self,
|
||||
_: &ChatWithFollow,
|
||||
|
|
@ -1850,6 +1903,96 @@ impl MessageEditor {
|
|||
self.editor.read(cx).text(cx)
|
||||
}
|
||||
|
||||
fn reset_prompt_history_navigation(&mut self) {
|
||||
self.prompt_history_state = PromptHistoryState::default();
|
||||
}
|
||||
|
||||
pub fn set_prompt_history(&mut self, prompt_history: Vec<Vec<acp::ContentBlock>>) {
|
||||
self.prompt_history = prompt_history;
|
||||
}
|
||||
|
||||
fn has_visible_completions_menu(&self, cx: &App) -> bool {
|
||||
self.editor.read(cx).has_visible_completions_menu()
|
||||
}
|
||||
|
||||
fn is_navigating_prompt_history(&self) -> bool {
|
||||
self.prompt_history_state.history_index.is_some()
|
||||
}
|
||||
|
||||
fn cursor_is_at_buffer_start(&self, cx: &App) -> bool {
|
||||
let editor = self.editor.read(cx);
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let selection = editor.selections.newest_anchor();
|
||||
let start_offset = selection.start.to_offset(&snapshot).0;
|
||||
let end_offset = selection.end.to_offset(&snapshot).0;
|
||||
start_offset == 0 && end_offset == 0
|
||||
}
|
||||
|
||||
fn set_message_from_history(
|
||||
&mut self,
|
||||
message: Vec<acp::ContentBlock>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.clear(window, cx);
|
||||
});
|
||||
self.insert_message_blocks(message, false, window, cx);
|
||||
self.set_cursor_offset(0, window, cx);
|
||||
}
|
||||
|
||||
pub fn navigate_prompt_history_previous(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
if self.prompt_history.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let next_index = match self.prompt_history_state.history_index {
|
||||
Some(index) if index > 0 => index - 1,
|
||||
Some(_) => return false,
|
||||
None => {
|
||||
self.prompt_history_state.saved_draft =
|
||||
Some(self.draft_content_blocks_snapshot(cx));
|
||||
self.prompt_history.len() - 1
|
||||
}
|
||||
};
|
||||
|
||||
self.prompt_history_state.history_index = Some(next_index);
|
||||
let message = self.prompt_history[next_index].clone();
|
||||
self.set_message_from_history(message, window, cx);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn navigate_prompt_history_next(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
let Some(current_index) = self.prompt_history_state.history_index else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if current_index + 1 < self.prompt_history.len() {
|
||||
let next_index = current_index + 1;
|
||||
self.prompt_history_state.history_index = Some(next_index);
|
||||
let message = self.prompt_history[next_index].clone();
|
||||
self.set_message_from_history(message, window, cx);
|
||||
return true;
|
||||
}
|
||||
|
||||
let draft = self
|
||||
.prompt_history_state
|
||||
.saved_draft
|
||||
.take()
|
||||
.unwrap_or_default();
|
||||
self.prompt_history_state.history_index = None;
|
||||
self.set_message_from_history(draft, window, cx);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn set_cursor_offset(
|
||||
&mut self,
|
||||
offset: usize,
|
||||
|
|
@ -1893,6 +2036,16 @@ impl MessageEditor {
|
|||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn set_prompt_history_for_tests(
|
||||
&mut self,
|
||||
prompt_history: Vec<Vec<acp::ContentBlock>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
self.set_prompt_history(prompt_history);
|
||||
}
|
||||
|
||||
fn serialize_selection_with_mentions(
|
||||
&self,
|
||||
expand_empty_to_line: bool,
|
||||
|
|
@ -1992,6 +2145,8 @@ impl Render for MessageEditor {
|
|||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(Self::send_immediately))
|
||||
.on_action(cx.listener(Self::chat_with_follow))
|
||||
.on_action(cx.listener(Self::prompt_history_previous))
|
||||
.on_action(cx.listener(Self::prompt_history_next))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.capture_action(cx.listener(Self::copy))
|
||||
.capture_action(cx.listener(Self::cut))
|
||||
|
|
@ -5361,6 +5516,227 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_prompt_history_previous_and_draft_restore(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let (message_editor, cx) = setup_message_editor(cx).await;
|
||||
|
||||
message_editor.update_in(cx, |message_editor, window, cx| {
|
||||
message_editor.set_prompt_history_for_tests(
|
||||
vec![
|
||||
vec![acp::ContentBlock::Text(acp::TextContent::new(
|
||||
"first prompt",
|
||||
))],
|
||||
vec![acp::ContentBlock::Text(acp::TextContent::new(
|
||||
"second prompt",
|
||||
))],
|
||||
vec![acp::ContentBlock::Text(acp::TextContent::new(
|
||||
"third prompt",
|
||||
))],
|
||||
],
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
message_editor.set_text("draft prompt", window, cx);
|
||||
message_editor.set_cursor_offset(0, window, cx);
|
||||
});
|
||||
|
||||
message_editor.update_in(cx, |message_editor, window, cx| {
|
||||
assert!(message_editor.navigate_prompt_history_previous(window, cx));
|
||||
});
|
||||
let text = message_editor.update(cx, |message_editor, cx| message_editor.text(cx));
|
||||
assert_eq!(text, "third prompt");
|
||||
|
||||
message_editor.update_in(cx, |message_editor, window, cx| {
|
||||
assert!(message_editor.navigate_prompt_history_previous(window, cx));
|
||||
});
|
||||
let text = message_editor.update(cx, |message_editor, cx| message_editor.text(cx));
|
||||
assert_eq!(text, "second prompt");
|
||||
|
||||
message_editor.update_in(cx, |message_editor, window, cx| {
|
||||
assert!(message_editor.navigate_prompt_history_next(window, cx));
|
||||
});
|
||||
let text = message_editor.update(cx, |message_editor, cx| message_editor.text(cx));
|
||||
assert_eq!(text, "third prompt");
|
||||
|
||||
message_editor.update_in(cx, |message_editor, window, cx| {
|
||||
assert!(message_editor.navigate_prompt_history_next(window, cx));
|
||||
});
|
||||
let text = message_editor.update(cx, |message_editor, cx| message_editor.text(cx));
|
||||
assert_eq!(text, "draft prompt");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_prompt_history_previous_requires_cursor_at_start(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let (message_editor, cx) = setup_message_editor(cx).await;
|
||||
|
||||
message_editor.update_in(cx, |message_editor, window, cx| {
|
||||
message_editor.set_prompt_history_for_tests(
|
||||
vec![vec![acp::ContentBlock::Text(acp::TextContent::new(
|
||||
"previous prompt",
|
||||
))]],
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
message_editor.set_text("draft prompt", window, cx);
|
||||
message_editor.set_cursor_offset(5, window, cx);
|
||||
});
|
||||
|
||||
cx.dispatch_action(crate::PromptHistoryPrevious);
|
||||
|
||||
let text = message_editor.update(cx, |message_editor, cx| message_editor.text(cx));
|
||||
assert_eq!(text, "draft prompt");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_prompt_history_next_without_history_mode_is_noop(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let (message_editor, cx) = setup_message_editor(cx).await;
|
||||
|
||||
message_editor.update_in(cx, |message_editor, window, cx| {
|
||||
message_editor.set_prompt_history_for_tests(
|
||||
vec![vec![acp::ContentBlock::Text(acp::TextContent::new(
|
||||
"previous prompt",
|
||||
))]],
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
message_editor.set_text("draft prompt", window, cx);
|
||||
message_editor.set_cursor_offset(0, window, cx);
|
||||
});
|
||||
|
||||
message_editor.update_in(cx, |message_editor, window, cx| {
|
||||
assert!(!message_editor.navigate_prompt_history_next(window, cx));
|
||||
});
|
||||
|
||||
let text = message_editor.update(cx, |message_editor, cx| message_editor.text(cx));
|
||||
assert_eq!(text, "draft prompt");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_prompt_history_send_immediately_resets_navigation(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let (message_editor, cx) = setup_message_editor(cx).await;
|
||||
|
||||
message_editor.update_in(cx, |message_editor, window, cx| {
|
||||
message_editor.set_prompt_history_for_tests(
|
||||
vec![vec![acp::ContentBlock::Text(acp::TextContent::new(
|
||||
"previous prompt",
|
||||
))]],
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
message_editor.set_text("draft prompt", window, cx);
|
||||
message_editor.set_cursor_offset(0, window, cx);
|
||||
});
|
||||
|
||||
message_editor.update_in(cx, |message_editor, window, cx| {
|
||||
assert!(message_editor.navigate_prompt_history_previous(window, cx));
|
||||
});
|
||||
message_editor.update_in(cx, |message_editor, _window, cx| {
|
||||
message_editor.send_immediately(&crate::SendImmediately, _window, cx);
|
||||
});
|
||||
message_editor.update_in(cx, |message_editor, window, cx| {
|
||||
message_editor.set_text("new draft", window, cx);
|
||||
message_editor.set_cursor_offset(0, window, cx);
|
||||
});
|
||||
|
||||
message_editor.update_in(cx, |message_editor, window, cx| {
|
||||
assert!(!message_editor.navigate_prompt_history_next(window, cx));
|
||||
});
|
||||
|
||||
let text = message_editor.update(cx, |message_editor, cx| message_editor.text(cx));
|
||||
assert_eq!(text, "new draft");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_prompt_history_does_not_override_completions_navigation(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let app_state = cx.update(AppState::test);
|
||||
|
||||
cx.update(|cx| {
|
||||
editor::init(cx);
|
||||
workspace::init(app_state.clone(), cx);
|
||||
});
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let window =
|
||||
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
let workspace = window
|
||||
.read_with(cx, |mw, _| mw.workspace().clone())
|
||||
.unwrap();
|
||||
|
||||
let mut cx = VisualTestContext::from_window(window.into(), cx);
|
||||
|
||||
let session_capabilities = Arc::new(RwLock::new(SessionCapabilities::from_acp_commands(
|
||||
acp::PromptCapabilities::default(),
|
||||
vec![
|
||||
acp::AvailableCommand::new("quick-math", "2 + 2 = 4 - 1 = 3"),
|
||||
acp::AvailableCommand::new("say-hello", "Say hello to whoever you want"),
|
||||
],
|
||||
)));
|
||||
|
||||
let message_editor = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||
let workspace_handle = cx.weak_entity();
|
||||
let message_editor = cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
workspace_handle,
|
||||
project.downgrade(),
|
||||
None,
|
||||
None,
|
||||
session_capabilities.clone(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
EditorMode::AutoHeight {
|
||||
max_lines: None,
|
||||
min_lines: 1,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.add_item(
|
||||
Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
|
||||
true,
|
||||
true,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
message_editor.read(cx).focus_handle(cx).focus(window, cx);
|
||||
message_editor
|
||||
});
|
||||
|
||||
message_editor.update_in(&mut cx, |message_editor, window, cx| {
|
||||
message_editor.set_prompt_history_for_tests(
|
||||
vec![vec![acp::ContentBlock::Text(acp::TextContent::new(
|
||||
"previous prompt",
|
||||
))]],
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
cx.simulate_input("/");
|
||||
cx.dispatch_action(crate::PromptHistoryPrevious);
|
||||
|
||||
let (text, completions_visible) = message_editor.update(&mut cx, |message_editor, cx| {
|
||||
(
|
||||
message_editor.text(cx),
|
||||
message_editor
|
||||
.editor()
|
||||
.read(cx)
|
||||
.has_visible_completions_menu(),
|
||||
)
|
||||
});
|
||||
assert_eq!(text, "/");
|
||||
assert!(completions_visible);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_append_message_preserves_mention_offset(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
|
|
|||
|
|
@ -1033,6 +1033,7 @@ impl VsCodeSettings {
|
|||
pane_split_direction_vertical: None,
|
||||
resize_all_panels_in_dock: None,
|
||||
restore_on_file_reopen: self.read_bool("workbench.editor.restoreViewState"),
|
||||
restore_tabs_per_branch: None,
|
||||
restore_on_startup: None,
|
||||
window_decorations: None,
|
||||
show_call_status_icon: None,
|
||||
|
|
|
|||
|
|
@ -63,6 +63,10 @@ pub struct WorkspaceSettingsContent {
|
|||
///
|
||||
/// Default: true
|
||||
pub restore_on_file_reopen: Option<bool>,
|
||||
/// Whether to restore open tabs and pane layout per active git branch.
|
||||
///
|
||||
/// Default: true
|
||||
pub restore_tabs_per_branch: Option<bool>,
|
||||
/// The size of the workspace split drop targets on the outer edges.
|
||||
/// Given as a fraction that will be multiplied by the smaller dimension of the workspace.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ fn contains_wsl_path(paths: &PathList) -> bool {
|
|||
.any(|path| util::paths::WslPath::from_path(path).is_some())
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct SerializedAxis(pub(crate) gpui::Axis);
|
||||
impl sqlez::bindable::StaticColumnCount for SerializedAxis {}
|
||||
impl sqlez::bindable::Bind for SerializedAxis {
|
||||
|
|
@ -1037,6 +1037,19 @@ impl Domain for WorkspaceDb {
|
|||
ALTER TABLE workspaces ADD COLUMN identity_paths TEXT;
|
||||
ALTER TABLE workspaces ADD COLUMN identity_paths_order TEXT;
|
||||
),
|
||||
sql!(
|
||||
CREATE TABLE branch_workspace_layouts (
|
||||
workspace_id INTEGER NOT NULL,
|
||||
repository_path TEXT NOT NULL,
|
||||
branch_name TEXT NOT NULL,
|
||||
center_group TEXT NOT NULL,
|
||||
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
PRIMARY KEY (workspace_id, repository_path, branch_name),
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
) STRICT;
|
||||
),
|
||||
];
|
||||
|
||||
// Allow recovering from bad migration that was initially shipped to nightly
|
||||
|
|
@ -2396,6 +2409,59 @@ impl WorkspaceDb {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn save_branch_workspace_layout(
|
||||
&self,
|
||||
workspace_id: WorkspaceId,
|
||||
repository_path: Arc<Path>,
|
||||
branch_name: String,
|
||||
center_group: SerializedPaneGroup,
|
||||
) -> Result<()> {
|
||||
let repository_path = repository_path.to_string_lossy().into_owned();
|
||||
let center_group = serde_json::to_string(¢er_group)?;
|
||||
|
||||
self.write(move |conn| {
|
||||
conn.exec_bound(sql!(
|
||||
INSERT INTO branch_workspace_layouts(
|
||||
workspace_id,
|
||||
repository_path,
|
||||
branch_name,
|
||||
center_group
|
||||
)
|
||||
VALUES (?1, ?2, ?3, ?4)
|
||||
ON CONFLICT DO UPDATE SET
|
||||
center_group = ?4,
|
||||
timestamp = CURRENT_TIMESTAMP
|
||||
))?((workspace_id, repository_path, branch_name, center_group))?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn branch_workspace_layout(
|
||||
&self,
|
||||
workspace_id: WorkspaceId,
|
||||
repository_path: Arc<Path>,
|
||||
branch_name: String,
|
||||
) -> Result<Option<SerializedPaneGroup>> {
|
||||
let repository_path = repository_path.to_string_lossy().into_owned();
|
||||
|
||||
let mut center_groups = self.select_bound::<_, String>(sql!(
|
||||
SELECT center_group
|
||||
FROM branch_workspace_layouts
|
||||
WHERE workspace_id = ?1
|
||||
AND repository_path = ?2
|
||||
AND branch_name = ?3
|
||||
))?((workspace_id, repository_path, branch_name))
|
||||
.context("loading branch workspace layout")?;
|
||||
|
||||
center_groups
|
||||
.pop()
|
||||
.map(|center_group| serde_json::from_str(¢er_group))
|
||||
.transpose()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
query! {
|
||||
pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
|
||||
UPDATE workspaces
|
||||
|
|
@ -3277,6 +3343,90 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_branch_workspace_layouts_are_scoped_by_branch() {
|
||||
zlog::init_test();
|
||||
|
||||
let db =
|
||||
WorkspaceDb::open_test_db("test_branch_workspace_layouts_are_scoped_by_branch").await;
|
||||
let workspace = SerializedWorkspace {
|
||||
id: WorkspaceId(1),
|
||||
paths: PathList::new(&["/repo"]),
|
||||
identity_paths: None,
|
||||
location: SerializedWorkspaceLocation::Local,
|
||||
center_group: Default::default(),
|
||||
window_bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
bookmarks: Default::default(),
|
||||
breakpoints: Default::default(),
|
||||
session_id: None,
|
||||
window_id: None,
|
||||
user_toolchains: Default::default(),
|
||||
};
|
||||
db.save_workspace(workspace).await;
|
||||
|
||||
let branch_a_layout = SerializedPaneGroup::Pane(SerializedPane::new(
|
||||
vec![SerializedItem::new("Editor", 1, true, false)],
|
||||
true,
|
||||
0,
|
||||
));
|
||||
let branch_b_layout = SerializedPaneGroup::Pane(SerializedPane::new(
|
||||
vec![SerializedItem::new("Editor", 2, true, false)],
|
||||
true,
|
||||
0,
|
||||
));
|
||||
|
||||
db.save_branch_workspace_layout(
|
||||
WorkspaceId(1),
|
||||
Path::new("/repo").into(),
|
||||
"branch-a".to_owned(),
|
||||
branch_a_layout.clone(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.save_branch_workspace_layout(
|
||||
WorkspaceId(1),
|
||||
Path::new("/repo").into(),
|
||||
"branch-b".to_owned(),
|
||||
branch_b_layout.clone(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
db.branch_workspace_layout(
|
||||
WorkspaceId(1),
|
||||
Path::new("/repo").into(),
|
||||
"branch-a".to_owned()
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
Some(branch_a_layout)
|
||||
);
|
||||
assert_eq!(
|
||||
db.branch_workspace_layout(
|
||||
WorkspaceId(1),
|
||||
Path::new("/repo").into(),
|
||||
"branch-b".to_owned()
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
Some(branch_b_layout)
|
||||
);
|
||||
assert_eq!(
|
||||
db.branch_workspace_layout(
|
||||
WorkspaceId(1),
|
||||
Path::new("/repo").into(),
|
||||
"branch-c".to_owned()
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_full_workspace_serialization() {
|
||||
zlog::init_test();
|
||||
|
|
|
|||
|
|
@ -230,7 +230,7 @@ impl Bind for DockData {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub(crate) enum SerializedPaneGroup {
|
||||
Group {
|
||||
axis: SerializedAxis,
|
||||
|
|
@ -335,7 +335,7 @@ impl SerializedPaneGroup {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Default, Clone)]
|
||||
#[derive(Debug, PartialEq, Eq, Default, Clone, Serialize, Deserialize)]
|
||||
pub struct SerializedPane {
|
||||
pub(crate) active: bool,
|
||||
pub(crate) children: Vec<SerializedItem>,
|
||||
|
|
@ -420,7 +420,7 @@ pub type GroupId = i64;
|
|||
pub type PaneId = i64;
|
||||
pub type ItemId = u64;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
pub struct SerializedItem {
|
||||
pub kind: Arc<str>,
|
||||
pub item_id: ItemId,
|
||||
|
|
|
|||
|
|
@ -93,12 +93,16 @@ pub use persistence::{
|
|||
},
|
||||
read_serialized_multi_workspaces,
|
||||
};
|
||||
use persistence::{SerializedWindowBounds, model::SerializedWorkspace};
|
||||
use persistence::{
|
||||
SerializedAxis, SerializedWindowBounds,
|
||||
model::{SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace},
|
||||
};
|
||||
use postage::stream::Stream;
|
||||
use project::{
|
||||
DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
|
||||
WorktreeSettings,
|
||||
debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
|
||||
git_store::{GitStoreEvent, RepositoryEvent},
|
||||
project_settings::ProjectSettings,
|
||||
toolchain_store::ToolchainStoreEvent,
|
||||
trusted_worktrees::{RemoteHostLocation, TrustedWorktrees, TrustedWorktreesEvent},
|
||||
|
|
@ -158,14 +162,8 @@ pub use workspace_settings::{
|
|||
};
|
||||
use zed_actions::{Spawn, feedback::FileBugReport, theme::ToggleMode};
|
||||
|
||||
use crate::security_modal::SecurityModal;
|
||||
use crate::{dock::PanelSizeState, item::ItemBufferKind, notifications::NotificationId};
|
||||
use crate::{
|
||||
persistence::{
|
||||
SerializedAxis,
|
||||
model::{SerializedItem, SerializedPane, SerializedPaneGroup},
|
||||
},
|
||||
security_modal::SecurityModal,
|
||||
};
|
||||
|
||||
pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200);
|
||||
|
||||
|
|
@ -1389,6 +1387,9 @@ pub struct Workspace {
|
|||
_schedule_serialize_workspace: Option<Task<()>>,
|
||||
_serialize_workspace_task: Option<Task<()>>,
|
||||
_schedule_serialize_ssh_paths: Option<Task<()>>,
|
||||
active_branch_session: Option<BranchSessionKey>,
|
||||
_branch_session_task: Option<Task<Result<()>>>,
|
||||
restoring_branch_session: bool,
|
||||
pane_history_timestamp: Arc<AtomicUsize>,
|
||||
bounds: Bounds<Pixels>,
|
||||
pub centered_layout: bool,
|
||||
|
|
@ -1434,6 +1435,12 @@ pub enum AutoWatch {
|
|||
Paused,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct BranchSessionKey {
|
||||
repository_path: Arc<Path>,
|
||||
branch_name: String,
|
||||
}
|
||||
|
||||
impl AutoWatch {
|
||||
pub fn enabled(&self) -> bool {
|
||||
matches!(self, AutoWatch::Active { .. } | AutoWatch::Paused)
|
||||
|
|
@ -1749,7 +1756,11 @@ impl Workspace {
|
|||
Self::serialize_items(&this, serializable_items_rx, cx).await
|
||||
});
|
||||
|
||||
let git_store = project.read(cx).git_store().clone();
|
||||
let active_branch_session = Self::branch_session_key_for_project(&project, cx);
|
||||
|
||||
let subscriptions = vec![
|
||||
cx.subscribe_in(&git_store, window, Self::on_git_store_event),
|
||||
cx.observe_window_activation(window, Self::on_window_activation_changed),
|
||||
cx.observe_window_bounds(window, move |this, window, cx| {
|
||||
if this.bounds_save_task_queued.is_some() {
|
||||
|
|
@ -1831,6 +1842,9 @@ impl Workspace {
|
|||
_schedule_serialize_workspace: None,
|
||||
_serialize_workspace_task: None,
|
||||
_schedule_serialize_ssh_paths: None,
|
||||
active_branch_session,
|
||||
_branch_session_task: None,
|
||||
restoring_branch_session: false,
|
||||
leader_updates_tx,
|
||||
_subscriptions: subscriptions,
|
||||
pane_history_timestamp,
|
||||
|
|
@ -3470,6 +3484,14 @@ impl Workspace {
|
|||
self.save_all_internal(SaveIntent::Close, true, window, cx)
|
||||
}
|
||||
|
||||
fn prompt_to_save_or_discard_dirty_items_before_branch_switch(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<bool>> {
|
||||
self.save_all_internal(SaveIntent::Close, false, window, cx)
|
||||
}
|
||||
|
||||
fn save_all_internal(
|
||||
&mut self,
|
||||
mut save_intent: SaveIntent,
|
||||
|
|
@ -3967,6 +3989,7 @@ impl Workspace {
|
|||
if let Some(task) = self.close_all_internal(
|
||||
true,
|
||||
action.save_intent.unwrap_or(SaveIntent::Close),
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
|
|
@ -3983,6 +4006,7 @@ impl Workspace {
|
|||
if let Some(task) = self.close_all_internal(
|
||||
false,
|
||||
action.save_intent.unwrap_or(SaveIntent::Close),
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
|
|
@ -4049,6 +4073,7 @@ impl Workspace {
|
|||
&mut self,
|
||||
retain_active_pane: bool,
|
||||
save_intent: SaveIntent,
|
||||
close_pinned: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
|
|
@ -4061,7 +4086,7 @@ impl Workspace {
|
|||
pane.close_other_items(
|
||||
&CloseOtherItems {
|
||||
save_intent: None,
|
||||
close_pinned: false,
|
||||
close_pinned,
|
||||
},
|
||||
None,
|
||||
window,
|
||||
|
|
@ -4081,7 +4106,7 @@ impl Workspace {
|
|||
pane.close_all_items(
|
||||
&CloseAllItems {
|
||||
save_intent: Some(save_intent),
|
||||
close_pinned: false,
|
||||
close_pinned,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
|
|
@ -6769,6 +6794,244 @@ impl Workspace {
|
|||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn branch_session_key_for_project(
|
||||
project: &Entity<Project>,
|
||||
cx: &App,
|
||||
) -> Option<BranchSessionKey> {
|
||||
let repository = project.read(cx).active_repository(cx)?;
|
||||
let repository = repository.read(cx);
|
||||
Some(BranchSessionKey {
|
||||
repository_path: repository.work_directory_abs_path.clone(),
|
||||
branch_name: repository.branch.as_ref()?.name().to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
fn on_git_store_event(
|
||||
&mut self,
|
||||
_git_store: &Entity<project::git_store::GitStore>,
|
||||
event: &GitStoreEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if !matches!(
|
||||
event,
|
||||
GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::HeadChanged, true)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if self.restoring_branch_session || self._branch_session_task.is_some() {
|
||||
return;
|
||||
}
|
||||
if !self.project.read(cx).is_local()
|
||||
|| !workspace_settings::WorkspaceSettings::get_global(cx).restore_tabs_per_branch
|
||||
{
|
||||
self.active_branch_session = Self::branch_session_key_for_project(&self.project, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
let previous_key = self.active_branch_session.clone();
|
||||
let next_key = Self::branch_session_key_for_project(&self.project, cx);
|
||||
if previous_key == next_key {
|
||||
return;
|
||||
}
|
||||
|
||||
let db = WorkspaceDb::global(cx);
|
||||
self._branch_session_task = Some(cx.spawn_in(window, async move |this, cx| {
|
||||
if let Some(previous_key) = previous_key.clone() {
|
||||
let save = this.update_in(cx, |workspace, window, cx| {
|
||||
workspace.save_branch_workspace_layout(previous_key, window, cx)
|
||||
})?;
|
||||
save.await.log_err();
|
||||
}
|
||||
|
||||
let confirmed = this
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
workspace.prompt_to_save_or_discard_dirty_items_before_branch_switch(window, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
if !confirmed {
|
||||
this.update(cx, |workspace, _| {
|
||||
workspace.active_branch_session = previous_key;
|
||||
workspace._branch_session_task = None;
|
||||
})?;
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
|
||||
if let Some(next_key) = next_key.clone() {
|
||||
let layout = if let Some(workspace_id) =
|
||||
this.read_with(cx, |workspace, _| workspace.database_id)?
|
||||
{
|
||||
db.branch_workspace_layout(
|
||||
workspace_id,
|
||||
next_key.repository_path.clone(),
|
||||
next_key.branch_name.clone(),
|
||||
)
|
||||
.await
|
||||
.log_err()
|
||||
.flatten()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(layout) = layout {
|
||||
this.update_in(cx, |workspace, window, cx| {
|
||||
workspace.restore_branch_workspace_layout(layout, window, cx)
|
||||
})?
|
||||
.await
|
||||
.log_err();
|
||||
} else {
|
||||
if let Some(task) = this.update_in(cx, |workspace, window, cx| {
|
||||
workspace.close_all_internal(false, SaveIntent::Skip, true, window, cx)
|
||||
})? {
|
||||
task.await.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.update(cx, |workspace, _| {
|
||||
workspace.active_branch_session = next_key;
|
||||
workspace._branch_session_task = None;
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
fn capture_center_pane_group(
|
||||
pane_group: &Member,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> SerializedPaneGroup {
|
||||
match pane_group {
|
||||
Member::Axis(PaneAxis {
|
||||
axis,
|
||||
members,
|
||||
flexes,
|
||||
bounding_boxes: _,
|
||||
}) => SerializedPaneGroup::Group {
|
||||
axis: SerializedAxis(*axis),
|
||||
children: members
|
||||
.iter()
|
||||
.map(|member| Self::capture_center_pane_group(member, window, cx))
|
||||
.collect::<Vec<_>>(),
|
||||
flexes: Some(flexes.lock().clone()),
|
||||
},
|
||||
Member::Pane(pane_handle) => {
|
||||
let (items, active, pinned_count) = {
|
||||
let pane = pane_handle.read(cx);
|
||||
let active_item_id = pane.active_item().map(|item| item.item_id());
|
||||
(
|
||||
pane.items()
|
||||
.filter_map(|handle| {
|
||||
let handle = handle.to_serializable_item_handle(cx)?;
|
||||
|
||||
Some(SerializedItem {
|
||||
kind: Arc::from(handle.serialized_item_kind()),
|
||||
item_id: handle.item_id().as_u64(),
|
||||
active: Some(handle.item_id()) == active_item_id,
|
||||
preview: pane.is_active_preview_item(handle.item_id()),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
pane.has_focus(window, cx),
|
||||
pane.pinned_count(),
|
||||
)
|
||||
};
|
||||
|
||||
SerializedPaneGroup::Pane(SerializedPane::new(items, active, pinned_count))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn save_branch_workspace_layout(
|
||||
&mut self,
|
||||
branch_key: BranchSessionKey,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let Some(workspace_id) = self.database_id else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
|
||||
let mut serialize_item_tasks = Vec::new();
|
||||
let items = self
|
||||
.items(cx)
|
||||
.map(|item| item.boxed_clone())
|
||||
.collect::<Vec<_>>();
|
||||
for item in items {
|
||||
if let Some(task) = item
|
||||
.to_serializable_item_handle(cx)
|
||||
.and_then(|item| item.serialize(self, false, window, cx))
|
||||
{
|
||||
serialize_item_tasks.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
let center_group = Self::capture_center_pane_group(&self.center.root, window, cx);
|
||||
let db = WorkspaceDb::global(cx);
|
||||
cx.spawn(async move |_, _| {
|
||||
for task in serialize_item_tasks {
|
||||
task.await.log_err();
|
||||
}
|
||||
|
||||
db.save_branch_workspace_layout(
|
||||
workspace_id,
|
||||
branch_key.repository_path,
|
||||
branch_key.branch_name,
|
||||
center_group,
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
fn restore_branch_workspace_layout(
|
||||
&mut self,
|
||||
center_group: SerializedPaneGroup,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let Some(workspace_id) = self.database_id else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
|
||||
self.restoring_branch_session = true;
|
||||
cx.spawn_in(window, async move |workspace, cx| {
|
||||
if let Some(close_task) = workspace.update_in(cx, |workspace, window, cx| {
|
||||
workspace.close_all_internal(false, SaveIntent::Skip, true, window, cx)
|
||||
})? {
|
||||
close_task.await.log_err();
|
||||
}
|
||||
|
||||
let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
|
||||
let restored = center_group
|
||||
.deserialize(&project, workspace_id, workspace.clone(), cx)
|
||||
.await;
|
||||
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
if let Some((center_group, active_pane, _)) = restored {
|
||||
workspace.remove_panes(workspace.center.root.clone(), window, cx);
|
||||
workspace.center = PaneGroup::with_root(center_group);
|
||||
workspace.center.set_is_center(true);
|
||||
workspace.center.mark_positions(cx);
|
||||
|
||||
if let Some(active_pane) = active_pane {
|
||||
workspace.set_active_pane(&active_pane, window, cx);
|
||||
cx.focus_self(window);
|
||||
} else {
|
||||
workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
workspace.restoring_branch_session = false;
|
||||
workspace.serialize_workspace_internal(window, cx).detach();
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
|
||||
match member {
|
||||
Member::Axis(PaneAxis { members, .. }) => {
|
||||
|
|
@ -7063,6 +7326,7 @@ impl Workspace {
|
|||
pub(crate) fn load_workspace(
|
||||
serialized_workspace: SerializedWorkspace,
|
||||
paths_to_open: Vec<Option<ProjectPath>>,
|
||||
cleanup_unloaded_items: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
|
||||
|
|
@ -7170,23 +7434,25 @@ impl Workspace {
|
|||
// the database filling up, we delete items that haven't been loaded now.
|
||||
//
|
||||
// The items that have been loaded, have been saved after they've been added to the workspace.
|
||||
let clean_up_tasks = workspace.update_in(cx, |_, window, cx| {
|
||||
item_ids_by_kind
|
||||
.into_iter()
|
||||
.map(|(item_kind, loaded_items)| {
|
||||
SerializableItemRegistry::cleanup(
|
||||
item_kind,
|
||||
serialized_workspace.id,
|
||||
loaded_items,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.log_err()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})?;
|
||||
if cleanup_unloaded_items {
|
||||
let clean_up_tasks = workspace.update_in(cx, |_, window, cx| {
|
||||
item_ids_by_kind
|
||||
.into_iter()
|
||||
.map(|(item_kind, loaded_items)| {
|
||||
SerializableItemRegistry::cleanup(
|
||||
item_kind,
|
||||
serialized_workspace.id,
|
||||
loaded_items,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.log_err()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})?;
|
||||
|
||||
futures::future::join_all(clean_up_tasks).await;
|
||||
futures::future::join_all(clean_up_tasks).await;
|
||||
}
|
||||
|
||||
workspace
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
|
|
@ -8214,6 +8480,7 @@ fn open_items(
|
|||
.map(|(_, project_path)| project_path)
|
||||
.cloned()
|
||||
.collect(),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ pub struct WorkspaceSettings {
|
|||
pub restore_on_startup: settings::RestoreOnStartupBehavior,
|
||||
pub cli_default_open_behavior: settings::CliDefaultOpenBehavior,
|
||||
pub restore_on_file_reopen: bool,
|
||||
pub restore_tabs_per_branch: bool,
|
||||
pub drop_target_size: f32,
|
||||
pub use_system_path_prompts: bool,
|
||||
pub use_system_prompts: bool,
|
||||
|
|
@ -102,6 +103,7 @@ impl Settings for WorkspaceSettings {
|
|||
restore_on_startup: workspace.restore_on_startup.unwrap(),
|
||||
cli_default_open_behavior: workspace.cli_default_open_behavior.unwrap(),
|
||||
restore_on_file_reopen: workspace.restore_on_file_reopen.unwrap(),
|
||||
restore_tabs_per_branch: workspace.restore_tabs_per_branch.unwrap(),
|
||||
drop_target_size: workspace.drop_target_size.unwrap(),
|
||||
use_system_path_prompts: workspace.use_system_path_prompts.unwrap(),
|
||||
use_system_prompts: workspace.use_system_prompts.unwrap(),
|
||||
|
|
|
|||
|
|
@ -3487,6 +3487,16 @@ List of strings containing any combination of:
|
|||
|
||||
`boolean` values
|
||||
|
||||
## Restore Tabs Per Branch
|
||||
|
||||
- Description: Whether to restore open tabs and pane layout per active git branch.
|
||||
- Setting: `restore_tabs_per_branch`
|
||||
- Default: `true`
|
||||
|
||||
**Options**
|
||||
|
||||
`boolean` values
|
||||
|
||||
## Restore on Startup
|
||||
|
||||
- Description: Controls session restoration on startup.
|
||||
|
|
|
|||
Loading…
Reference in a new issue