Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion rust/crates/labolabo-app/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ settings:
footer: "Automatically resumes the previous session when you reopen a restored task."
git_pane_default_visible:
label: "Show Git pane by default"
footer: "Default visibility of the Git pane for newly opened tasks (doesn't affect tasks already open)."
footer: "Default visibility for tasks whose Git pane has never been toggled (tasks you've already shown or hidden it for remember that choice, unaffected by this setting)."
scrollback:
label: "Scrollback lines"
footer: "Changes apply to tabs opened after this point (existing tabs are unaffected)."
Expand Down
2 changes: 1 addition & 1 deletion rust/crates/labolabo-app/locales/ja.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ settings:
footer: "復元したタスクを開いたとき、前回のセッションを自動的に再開します。"
git_pane_default_visible:
label: "Git ペインを既定で表示"
footer: "新しく開くタスクの Git ペイン表示/非表示の既定値です(開いているタスクには影響しません)。"
footer: "Git ペインの表示/非表示を切り替えたことがないタスクに適用される既定値です(切り替え済みのタスクはその表示状態を記憶しており、この設定を変えても影響しません)。"
scrollback:
label: "スクロールバック行数"
footer: "変更は次に開くタブから反映されます(既存のタブには影響しません)。"
Expand Down
51 changes: 45 additions & 6 deletions rust/crates/labolabo-app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1789,9 +1789,13 @@ impl LaboLaboApp {
.map(|p| p.id)
.collect();

let git_pane_visible = resolve_git_pane_visible(
self.db.task_git_pane_visible(task_id).ok().flatten(),
self.settings.git_pane_default_visible,
);
self.workspaces.insert(
task_id.to_string(),
TaskWorkspace::new(model, self.settings.git_pane_default_visible),
TaskWorkspace::new(model, git_pane_visible),
);

for pane_id in pane_ids {
Expand Down Expand Up @@ -3844,6 +3848,12 @@ impl LaboLaboApp {
/// watch outright anymore (`plans` W6d) -- a Files/Diff/Commits tile
/// pane elsewhere in the same Task's tree can still need it; that
/// decision is [`Self::git_pane_state_needed`]'s job.
///
/// Persists the new value as `task_id`'s own memory (`plans` wave 19,
/// `TaskDatabase::set_task_git_pane_visible`), so it's restored as-is
/// the next time this Task's workspace is loaded --
/// [`Self::ensure_workspace_loaded`] prefers this memory over
/// `settings.git_pane_default_visible`.
pub(crate) fn set_git_pane_visible(
&mut self,
task_id: &str,
Expand All @@ -3857,6 +3867,9 @@ impl LaboLaboApp {
return;
}
workspace.git.visible = visible;
if let Err(err) = self.db.set_task_git_pane_visible(task_id, visible) {
eprintln!("labolabo-app: failed to persist task_git_pane_visible: {err}");
}
cx.notify();
self.sync_git_pane_activation(task_id, cx);
}
Expand Down Expand Up @@ -4266,11 +4279,12 @@ impl LaboLaboApp {
}

/// Toggles "Git ペインを既定で表示" and persists it -- seeds `GitPaneState::
/// visible` for every Task workspace loaded *after* this call
/// (`ensure_workspace_loaded` reads `self.settings.git_pane_default_visible`
/// fresh); already-loaded workspaces are unaffected (use `Cmd+Shift+G`
/// or the pane's own close button for those, same as before this
/// setting existed).
/// visible` for every Task workspace loaded *after* this call that has
/// no per-task memory of its own yet (`ensure_workspace_loaded` prefers
/// `TaskDatabase::task_git_pane_visible` over this default whenever
/// it's `Some`, `plans` wave 19); already-loaded workspaces are
/// unaffected (use `Cmd+Shift+G` or the pane's own close button for
/// those, same as before this setting existed).
pub(crate) fn set_git_pane_default_visible(&mut self, visible: bool, cx: &mut Context<Self>) {
if self.settings.git_pane_default_visible == visible {
return;
Expand Down Expand Up @@ -4438,6 +4452,17 @@ impl LaboLaboApp {
}
}

/// Pure decision behind [`LaboLaboApp::ensure_workspace_loaded`]'s Git pane
/// seeding (`plans` wave 19): a Task's own remembered visibility
/// (`TaskDatabase::task_git_pane_visible`) wins whenever it exists,
/// otherwise fall back to the app-wide `settings.git_pane_default_visible`.
/// Split out purely so this priority rule is unit-testable without a real
/// `TaskDatabase`/`LaboLaboApp`, the same way [`git_pane_needed`] and
/// [`compute_task_conflicts`] are.
fn resolve_git_pane_visible(memory: Option<bool>, default_visible: bool) -> bool {
memory.unwrap_or(default_visible)
}

/// Pure decision behind [`LaboLaboApp::git_pane_state_needed`]: is `task_id`'s
/// Git state needed by *something* currently on screen -- the fixed pane's
/// own visibility flag, or any front-facing (`PaneTilingModel::
Expand Down Expand Up @@ -5126,6 +5151,20 @@ mod tests {
assert!(compute_task_conflicts(&[], &HashMap::new(), "a").is_empty());
}

// MARK: - resolve_git_pane_visible (`plans` wave 19 per-task memory)

#[test]
fn remembered_value_wins_over_the_default_either_way() {
assert!(!resolve_git_pane_visible(Some(false), true));
assert!(resolve_git_pane_visible(Some(true), false));
}

#[test]
fn falls_back_to_the_default_when_never_remembered() {
assert!(resolve_git_pane_visible(None, true));
assert!(!resolve_git_pane_visible(None, false));
}

// MARK: - git_pane_needed (`plans` W6d watch-visibility generalization)

#[test]
Expand Down
111 changes: 111 additions & 0 deletions rust/crates/labolabo-core/src/store/task_database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,15 @@ impl TaskDatabase {
pub fn delete_task(&self, id: &str) -> StoreResult<()> {
self.conn
.execute("DELETE FROM task WHERE id = ?1", params![id])?;
// Also drop this task's per-task `appState` memory (today: only
// `gitPaneVisible:<id>`, `plans` wave 19) -- otherwise the row would
// just accumulate forever, and a future task created with the same
// id (unlikely but not impossible for externally-supplied ids)
// would inherit a stale value.
self.conn.execute(
"DELETE FROM appState WHERE key = ?1",
params![Self::git_pane_visible_key(id)],
)?;
Ok(())
}

Expand Down Expand Up @@ -394,6 +403,37 @@ impl TaskDatabase {
self.set_app_state(Some(bool_flag(visible)), Self::KEY_GIT_PANE_DEFAULT_VISIBLE)
}

// MARK: - App state (per-task Git pane visibility, `plans` wave 19)
//
// Unlike `KEY_GIT_PANE_DEFAULT_VISIBLE` above (one app-wide default),
// this remembers each Task's *own* last Git-pane visibility, keyed by
// task id -- the first per-task-keyed `appState` entry in this store
// (Swift precedent: `SessionStore.paneLayout(for:)`'s `"paneLayout:" +
// id.uuidString` key shape). The caller (`ensure_workspace_loaded`)
// prefers this over `git_pane_default_visible` whenever it's `Some`,
// falling back to the app-wide default only for a Task that has never
// had its Git pane toggled.

fn git_pane_visible_key(task_id: &str) -> String {
format!("gitPaneVisible:{task_id}")
}

/// `None` if this Task's Git pane has never been toggled (fresh Task,
/// or one from before this wave) -- caller should apply
/// `git_pane_default_visible` instead.
pub fn task_git_pane_visible(&self, task_id: &str) -> StoreResult<Option<bool>> {
Ok(self
.app_state(&Self::git_pane_visible_key(task_id))?
.map(|v| v != "0"))
}

pub fn set_task_git_pane_visible(&self, task_id: &str, visible: bool) -> StoreResult<()> {
self.set_app_state(
Some(bool_flag(visible)),
&Self::git_pane_visible_key(task_id),
)
}

/// `None` if never set, or if the stored text somehow isn't a valid
/// `usize` (treated the same as "never set" -- this crate's usual
/// "unknown/invalid persisted data degrades gracefully" posture, see
Expand Down Expand Up @@ -850,6 +890,22 @@ mod tests {
assert_eq!(all[0].id, b.id);
}

#[test]
fn delete_task_clears_its_git_pane_visible_memory_but_not_another_tasks() {
let db = TaskDatabase::open_in_memory().unwrap();
let a = sample_task(0);
let b = Task::new_attached("k", "r", "n", "/tmp/b", TileLayout::default(), 1);
db.upsert_task(&a).unwrap();
db.upsert_task(&b).unwrap();
db.set_task_git_pane_visible(&a.id, false).unwrap();
db.set_task_git_pane_visible(&b.id, false).unwrap();

db.delete_task(&a.id).unwrap();

assert_eq!(db.task_git_pane_visible(&a.id).unwrap(), None);
assert_eq!(db.task_git_pane_visible(&b.id).unwrap(), Some(false));
}

#[test]
fn next_sort_order_is_max_plus_one() {
let db = TaskDatabase::open_in_memory().unwrap();
Expand Down Expand Up @@ -890,6 +946,27 @@ mod tests {
assert_eq!(db.git_pane_default_visible().unwrap(), Some(true));
}

// MARK: - App state (per-task Git pane visibility, `plans` wave 19)

#[test]
fn task_git_pane_visible_defaults_to_none_until_set() {
let db = TaskDatabase::open_in_memory().unwrap();
assert_eq!(db.task_git_pane_visible("t1").unwrap(), None);
db.set_task_git_pane_visible("t1", false).unwrap();
assert_eq!(db.task_git_pane_visible("t1").unwrap(), Some(false));
db.set_task_git_pane_visible("t1", true).unwrap();
assert_eq!(db.task_git_pane_visible("t1").unwrap(), Some(true));
}

#[test]
fn task_git_pane_visible_is_scoped_per_task() {
let db = TaskDatabase::open_in_memory().unwrap();
db.set_task_git_pane_visible("t1", false).unwrap();
assert_eq!(db.task_git_pane_visible("t1").unwrap(), Some(false));
// A different task id has its own, independent memory.
assert_eq!(db.task_git_pane_visible("t2").unwrap(), None);
}

#[test]
fn scrollback_lines_round_trips_and_defaults_to_none() {
let db = TaskDatabase::open_in_memory().unwrap();
Expand Down Expand Up @@ -1043,6 +1120,40 @@ mod tests {
std::fs::remove_dir_all(&dir).unwrap();
}

/// Same "real file, reopened" shape as
/// `open_creates_parent_directory_and_persists_across_reopen`, but for
/// `task_git_pane_visible` specifically -- the in-memory-only tests
/// above (`task_git_pane_visible_defaults_to_none_until_set` etc.)
/// prove the SQL is right, but not that it survives an actual
/// close-and-reopen of the same on-disk file, i.e. an app restart.
#[test]
fn task_git_pane_visible_persists_across_a_real_file_reopen() {
let dir = std::env::temp_dir().join(format!(
"labolabo-task-store-test-{}-{:x}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos() as u64
));
let db_path = dir.join("tasks.db");
let _ = std::fs::remove_dir_all(&dir);

{
let db = TaskDatabase::open(&db_path).unwrap();
db.set_task_git_pane_visible("t1", false).unwrap();
}
{
// A fresh `TaskDatabase::open` of the same file -- exactly
// what `LaboLaboApp::new`'s restart-resume path does -- must
// still see the memory written by the previous "process".
let db = TaskDatabase::open(&db_path).unwrap();
assert_eq!(db.task_git_pane_visible("t1").unwrap(), Some(false));
}

std::fs::remove_dir_all(&dir).unwrap();
}

#[test]
fn malformed_kind_surfaces_as_invalid_task_enum_error() {
let db = TaskDatabase::open_in_memory().unwrap();
Expand Down
Loading