mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 00:07:50 +00:00
fix(windows): reopen every project window on startup (#1574)
"Reopen projects on startup" only saved window groups, so windows opened with New Window were lost. The saved session now lists every open project window (groups with their tabs and selection, single windows on their own), and the previous run's session is read before this run first overwrites it, so the startup project can't replace it before the other windows are restored.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
//! Window grouping is independent of a window's project or workspace.
|
||||
use ide_core::{IdeError, IdeResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Platform adapters supply the system's tab preference and active project state.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -117,6 +118,43 @@ impl WindowGroups {
|
||||
}
|
||||
}
|
||||
|
||||
/// One entry of the window session reopened at the next start (`window-groups.json`): a
|
||||
/// window group, or a single project window (one project).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SavedWindows {
|
||||
pub projects: Vec<PathBuf>,
|
||||
/// Index into `projects` of the group's selected tab.
|
||||
pub selected: usize,
|
||||
}
|
||||
|
||||
/// The project windows open now, to reopen at the next start: every group with its selected
|
||||
/// tab, then each window outside a group, in the order of `windows`. `project_of` gives a
|
||||
/// window's project (`None` for the welcome screen or LightEdit). `None` while a grouped
|
||||
/// window has no project yet (a restored member still opening): keep the previous session.
|
||||
pub fn session(
|
||||
groups: &[WindowGroup],
|
||||
windows: &[String],
|
||||
project_of: impl Fn(&str) -> Option<PathBuf>,
|
||||
) -> Option<Vec<SavedWindows>> {
|
||||
let mut saved = Vec::new();
|
||||
for group in groups {
|
||||
let projects: Option<Vec<PathBuf>> = group.members.iter().map(|label| project_of(label)).collect();
|
||||
let selected = group.members.iter().position(|l| l == &group.selected).unwrap_or(0);
|
||||
saved.push(SavedWindows { projects: projects?, selected });
|
||||
}
|
||||
for label in windows {
|
||||
if groups.iter().any(|g| g.members.contains(label)) {
|
||||
continue;
|
||||
}
|
||||
if let Some(project) = project_of(label)
|
||||
&& !saved.iter().any(|s| s.projects.contains(&project))
|
||||
{
|
||||
saved.push(SavedWindows { projects: vec![project], selected: 0 });
|
||||
}
|
||||
}
|
||||
Some(saved)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -186,4 +224,28 @@ mod tests {
|
||||
groups.reorder("a", members(&["d", "c", "b", "a"])).unwrap();
|
||||
assert_eq!(groups.group("a").unwrap().members[0], "d");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_session_keeps_groups_and_every_ungrouped_project_window() {
|
||||
let groups = vec![WindowGroup { members: members(&["project-1", "project-2"]), selected: "project-2".into() }];
|
||||
let windows = members(&["main", "project-1", "project-2", "project-3", "project-4"]);
|
||||
let project_of = |label: &str| match label {
|
||||
"main" => Some(PathBuf::from("/a")),
|
||||
"project-1" => Some(PathBuf::from("/b")),
|
||||
"project-2" => Some(PathBuf::from("/c")),
|
||||
"project-3" => Some(PathBuf::from("/d")),
|
||||
// The welcome screen or LightEdit.
|
||||
_ => None,
|
||||
};
|
||||
assert_eq!(
|
||||
session(&groups, &windows, project_of).unwrap(),
|
||||
[
|
||||
SavedWindows { projects: vec!["/b".into(), "/c".into()], selected: 1 },
|
||||
SavedWindows { projects: vec!["/a".into()], selected: 0 },
|
||||
SavedWindows { projects: vec!["/d".into()], selected: 0 },
|
||||
]
|
||||
);
|
||||
// A grouped window still opening its project: keep the previous session.
|
||||
assert_eq!(session(&groups, &windows, |l| (l != "project-2").then(|| PathBuf::from(l))), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,10 +85,8 @@ pub async fn open_project(window: Window, path: PathBuf) -> IdeResult<ProjectInf
|
||||
let info = project_info(&project);
|
||||
let _ = window.set_title(&info.name);
|
||||
window.state::<AppState>().sink.emit(ide_core::IdeEvent::WindowGroupsChanged);
|
||||
let grouped = window.state::<crate::windows::WindowsState>().groups.lock().group(window.label()).is_some();
|
||||
if grouped {
|
||||
crate::window_groups::persist(window.app_handle()).await?;
|
||||
}
|
||||
// Every project window is part of the session reopened at the next start.
|
||||
crate::window_groups::persist(window.app_handle()).await?;
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
|
||||
@@ -433,44 +433,36 @@ fn close_next(app: &AppHandle) {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedGroup {
|
||||
projects: Vec<PathBuf>,
|
||||
selected: usize,
|
||||
/// Window labels in creation order: `main`, then `project-1`, `project-2`, ….
|
||||
fn labels_in_order(app: &AppHandle) -> Vec<String> {
|
||||
let mut labels: Vec<String> = app.webview_windows().into_keys().collect();
|
||||
labels.sort_by_key(|l| (l != "main", l.rsplit('-').next().and_then(|n| n.parse::<u32>().ok()), l.clone()));
|
||||
labels
|
||||
}
|
||||
|
||||
/// Saves the open project windows (groups and single windows) for "Reopen projects on startup".
|
||||
/// Nothing is written while quitting, so the windows open at quit are the ones reopened.
|
||||
pub async fn persist(app: &AppHandle) -> IdeResult<()> {
|
||||
let state = app.state::<WindowsState>();
|
||||
let _write = state.groups_persist.lock().await;
|
||||
let mut previous = state.groups_persist.lock().await;
|
||||
if state.quitting() {
|
||||
return Ok(());
|
||||
}
|
||||
let groups = state.groups.lock().groups().to_vec();
|
||||
let host = app.state::<AppState>();
|
||||
let saved: Vec<_> = groups
|
||||
.iter()
|
||||
.filter_map(|group| {
|
||||
let projects: Vec<_> = group
|
||||
.members
|
||||
.iter()
|
||||
.filter_map(|label| {
|
||||
host.project(label)
|
||||
.ok()
|
||||
.map(|p| p.root.clone())
|
||||
.or_else(|| state.pending(label).and_then(|l| l.project).map(Into::into))
|
||||
})
|
||||
.collect();
|
||||
(projects.len() == group.members.len()).then(|| SavedGroup {
|
||||
projects,
|
||||
selected: group.members.iter().position(|l| l == &group.selected).unwrap_or(0),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
// A restored member may have taken its launch but not finished opening yet.
|
||||
if saved.len() != groups.len() {
|
||||
return Ok(());
|
||||
}
|
||||
let path = host.config_dir.join("window-groups.json");
|
||||
if previous.is_none() {
|
||||
*previous = Some(read_session(path.clone()).await);
|
||||
}
|
||||
let groups = state.groups.lock().groups().to_vec();
|
||||
let windows = labels_in_order(app);
|
||||
let project_of = |label: &str| match host.project(label) {
|
||||
Ok(p) => (!p.light).then(|| p.root.clone()),
|
||||
// A restored window may have taken its launch but not finished opening yet.
|
||||
Err(_) => state.pending(label).and_then(|l| l.project).map(Into::into),
|
||||
};
|
||||
let Some(saved) = ide_workspace::window_groups::session(&groups, &windows, project_of) else {
|
||||
return Ok(());
|
||||
};
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let bytes = serde_json::to_vec(&saved).map_err(|e| IdeError::internal(e.to_string()))?;
|
||||
ide_vfs::write_atomic(&path, &bytes).map(|_| ())
|
||||
@@ -479,6 +471,22 @@ pub async fn persist(app: &AppHandle) -> IdeResult<()> {
|
||||
.map_err(|e| IdeError::internal(e.to_string()))?
|
||||
}
|
||||
|
||||
/// The saved session; empty when there is none or it can't be read.
|
||||
async fn read_session(path: PathBuf) -> Vec<ide_workspace::window_groups::SavedWindows> {
|
||||
let read = tauri::async_runtime::spawn_blocking(move || match std::fs::read(&path) {
|
||||
Ok(bytes) => serde_json::from_slice(&bytes).map_err(|e| format!("{}: {e}", path.display())),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
|
||||
Err(e) => Err(format!("{}: {e}", path.display())),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
.and_then(|r| r);
|
||||
read.unwrap_or_else(|e| {
|
||||
tracing::warn!("cannot read the saved windows: {e}");
|
||||
Vec::new()
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn restore(app: &AppHandle, caller: &str) -> IdeResult<()> {
|
||||
if caller != "main" {
|
||||
return Ok(());
|
||||
@@ -489,41 +497,51 @@ pub async fn restore(app: &AppHandle, caller: &str) -> IdeResult<()> {
|
||||
if !project.settings.effective().general.reopen_last_project || state.groups_restored.swap(true, Ordering::SeqCst) {
|
||||
return Ok(());
|
||||
}
|
||||
let path = host.config_dir.join("window-groups.json");
|
||||
let saved: Vec<SavedGroup> = tauri::async_runtime::spawn_blocking(move || -> IdeResult<_> {
|
||||
match std::fs::read(path) {
|
||||
Ok(bytes) => serde_json::from_slice(&bytes).map_err(|e| IdeError::invalid(e.to_string())),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
|
||||
Err(e) => Err(e.into()),
|
||||
let saved = {
|
||||
let mut previous = state.groups_persist.lock().await;
|
||||
if previous.is_none() {
|
||||
*previous = Some(read_session(host.config_dir.join("window-groups.json")).await);
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|e| IdeError::internal(e.to_string()))??;
|
||||
if let Some(group) = saved.iter().find(|g| g.projects.contains(&project.root)) {
|
||||
previous.clone().unwrap_or_default()
|
||||
};
|
||||
// The caller's own group first (it already shows one of its projects), then every other
|
||||
// group and single window that was open at quit. Missing folders are skipped.
|
||||
let order = saved
|
||||
.iter()
|
||||
.filter(|g| g.projects.contains(&project.root))
|
||||
.chain(saved.iter().filter(|g| !g.projects.contains(&project.root)));
|
||||
for group in order {
|
||||
let mut labels = Vec::new();
|
||||
let mut selected = caller.to_owned();
|
||||
let mut selected = None;
|
||||
for path in &group.projects {
|
||||
let label = if path == &project.root {
|
||||
Some(caller.to_owned())
|
||||
} else if path.is_dir() {
|
||||
let window = match windows::find_project_window(app, path) {
|
||||
Some(w) => w,
|
||||
None => windows::create_restored_window(app, path.clone())?,
|
||||
Some(w) => Ok(w),
|
||||
None => windows::create_restored_window(app, path.clone()),
|
||||
};
|
||||
Some(window.label().to_owned())
|
||||
match window {
|
||||
Ok(w) => Some(w.label().to_owned()),
|
||||
Err(e) => {
|
||||
tracing::warn!("cannot reopen {}: {e}", path.display());
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(label) = label {
|
||||
if group.projects.get(group.selected) == Some(path) {
|
||||
selected = label.clone();
|
||||
selected = Some(label.clone());
|
||||
}
|
||||
labels.push(label);
|
||||
}
|
||||
}
|
||||
if labels.len() > 1 {
|
||||
state.groups.lock().merge(labels, caller)?;
|
||||
select(app, caller, &selected)?;
|
||||
let selected = selected.unwrap_or_else(|| labels[0].clone());
|
||||
state.groups.lock().merge(labels, &selected)?;
|
||||
select(app, &selected, &selected)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -120,7 +120,9 @@ pub fn wsl_launch(path: &Path) -> Option<RemoteLaunch> {
|
||||
pub struct WindowsState {
|
||||
pub groups: Mutex<ide_workspace::window_groups::WindowGroups>,
|
||||
pub groups_restored: AtomicBool,
|
||||
pub groups_persist: tokio::sync::Mutex<()>,
|
||||
/// Serialises writes of the saved session. Holds the previous run's session once read: it is
|
||||
/// read before this run first overwrites the file, so restoring still sees it.
|
||||
pub groups_persist: tokio::sync::Mutex<Option<Vec<ide_workspace::window_groups::SavedWindows>>>,
|
||||
launches: Mutex<HashMap<String, Launch>>,
|
||||
/// Caret positions asked for on the command line, by canonical file path, until a window takes the file.
|
||||
positions: Mutex<HashMap<PathBuf, (u32, u32)>>,
|
||||
|
||||
Reference in New Issue
Block a user