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
61 changes: 42 additions & 19 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1878,6 +1878,40 @@ private func installFileDropOverlayWhenReady(
}
}

@MainActor
private final class SelectedWorkspaceDirectoryObserver: ObservableObject {
@Published private(set) var directoryChangeGeneration: UInt64 = 0
private weak var tabManager: TabManager?
private var cancellable: AnyCancellable?

func wire(tabManager: TabManager) {
guard self.tabManager !== tabManager || cancellable == nil else { return }
self.tabManager = tabManager
cancellable = tabManager.$selectedTabId
.map { [weak tabManager] tabId -> Workspace? in
guard let tabId, let tabManager else { return nil }
return tabManager.tabs.first(where: { $0.id == tabId })
}
.removeDuplicates(by: { $0?.id == $1?.id })
.map { workspace -> AnyPublisher<(UUID?, String?), Never> in
guard let workspace else {
return Just<(UUID?, String?)>((nil, nil)).eraseToAnyPublisher()
}
return workspace.$currentDirectory
.map { (Optional(workspace.id), Optional($0)) }
.eraseToAnyPublisher()
}
.switchToLatest()
.removeDuplicates { previous, next in
previous.0 == next.0 && previous.1 == next.1
}
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.directoryChangeGeneration &+= 1
}
Comment thread
lawrencecchen marked this conversation as resolved.
}
Comment thread
lawrencecchen marked this conversation as resolved.
}

struct ContentView: View {
@ObservedObject var updateViewModel: UpdateViewModel
let windowId: UUID
Expand All @@ -1900,6 +1934,7 @@ struct ContentView: View {
@StateObject private var fullscreenControlsViewModel = TitlebarControlsViewModel()
@StateObject private var fileExplorerStore = FileExplorerStore()
@StateObject private var sessionIndexStore = SessionIndexStore()
@StateObject private var selectedWorkspaceDirectoryObserver = SelectedWorkspaceDirectoryObserver()
@State private var fileExplorerWidth: CGFloat = 220
@State private var fileExplorerDragStartWidth: CGFloat?
@State private var previousSelectedWorkspaceId: UUID?
Expand Down Expand Up @@ -3074,21 +3109,21 @@ struct ContentView: View {
let tab = tabManager.tabs.first(where: { $0.id == selectedId }) else {
// No selection means we have no local cwd to scope by; clear so the
// sessions panel doesn't keep filtering by a stale previous tab.
sessionIndexStore.currentDirectory = nil
sessionIndexStore.setCurrentDirectoryIfChanged(nil)
return
}

let dir = tab.currentDirectory.trimmingCharacters(in: .whitespacesAndNewlines)
guard !dir.isEmpty else {
sessionIndexStore.currentDirectory = nil
sessionIndexStore.setCurrentDirectoryIfChanged(nil)
return
}

fileExplorerStore.showHiddenFiles = true
if !tab.isRemoteWorkspace {
sessionIndexStore.currentDirectory = dir
sessionIndexStore.setCurrentDirectoryIfChanged(dir)
} else {
sessionIndexStore.currentDirectory = nil
sessionIndexStore.setCurrentDirectoryIfChanged(nil)
}

if tab.isRemoteWorkspace {
Expand Down Expand Up @@ -3215,6 +3250,7 @@ struct ContentView: View {
)

view = AnyView(view.onAppear {
selectedWorkspaceDirectoryObserver.wire(tabManager: tabManager)
tabManager.applyWindowBackgroundForSelectedTab()
reconcileMountedWorkspaceIds()
previousSelectedWorkspaceId = tabManager.selectedTabId
Expand Down Expand Up @@ -3309,21 +3345,8 @@ struct ContentView: View {
syncSidebarSelectedWorkspaceIds()
})

// File explorer: reactively sync CWD when selected workspace or its directory changes.
// Uses switchToLatest to automatically unsubscribe from the old workspace's publisher.
view = AnyView(view.onReceive(
tabManager.$selectedTabId
.compactMap { [weak tabManager] tabId -> Workspace? in
guard let tabId, let tabManager else { return nil }
return tabManager.tabs.first(where: { $0.id == tabId })
}
.map { workspace -> AnyPublisher<String, Never> in
workspace.$currentDirectory.eraseToAnyPublisher()
}
.switchToLatest()
.removeDuplicates()
.receive(on: DispatchQueue.main)
) { _ in
// File explorer: keep the Combine subscription stable across body re-evaluations.
view = AnyView(view.onChange(of: selectedWorkspaceDirectoryObserver.directoryChangeGeneration) { _ in
syncFileExplorerDirectory()
})

Expand Down
12 changes: 6 additions & 6 deletions Sources/RightSidebarPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@ struct RightSidebarPanelView: View {
if fileExplorerState.mode != mode {
fileExplorerState.mode = mode
if mode == .sessions {
sessionIndexStore.currentDirectory = fileExplorerStore.rootPath.isEmpty
? nil
: fileExplorerStore.rootPath
sessionIndexStore.setCurrentDirectoryIfChanged(sessionIndexDirectory)
if sessionIndexStore.entries.isEmpty {
sessionIndexStore.reload()
}
Expand All @@ -73,12 +71,14 @@ struct RightSidebarPanelView: View {
case .sessions:
SessionIndexView(store: sessionIndexStore, onResume: onResumeSession)
.onAppear {
sessionIndexStore.currentDirectory = fileExplorerStore.rootPath.isEmpty
? nil
: fileExplorerStore.rootPath
sessionIndexStore.setCurrentDirectoryIfChanged(sessionIndexDirectory)
}
}
}

private var sessionIndexDirectory: String? {
fileExplorerStore.rootPath.isEmpty ? nil : fileExplorerStore.rootPath
}
}

private struct ModeBarButton: View {
Expand Down
5 changes: 5 additions & 0 deletions Sources/SessionIndexStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,11 @@ final class SessionIndexStore: ObservableObject {
}
}

func setCurrentDirectoryIfChanged(_ next: String?) {
guard currentDirectory != next else { return }
currentDirectory = next
}

@Published var grouping: SessionGrouping {
didSet {
guard grouping != oldValue else { return }
Expand Down
18 changes: 17 additions & 1 deletion cmuxTests/SessionIndexViewTests.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AppKit
import Combine
import SwiftUI
import XCTest

Expand All @@ -10,6 +11,20 @@ import XCTest

@MainActor
final class SessionIndexViewTests: XCTestCase {
func testCurrentDirectorySetterDoesNotPublishEqualValue() {
let store = SessionIndexStore()
var emittedValues: [String?] = []
let cancellable = store.$currentDirectory
.dropFirst()
.sink { emittedValues.append($0) }
defer { cancellable.cancel() }

store.setCurrentDirectoryIfChanged("/foo")
store.setCurrentDirectoryIfChanged("/foo")

XCTAssertEqual(emittedValues, ["/foo"])
}

func testSectionPopoverHostCoordinatorSkipsHiddenRefreshes() {
let harness = makeHarness()
let coordinator = harness.host.makeCoordinator()
Expand Down Expand Up @@ -87,7 +102,8 @@ final class SessionIndexViewTests: XCTestCase {
let section = IndexSection(
key: .directory("/tmp"),
title: "tmp",
icon: .folder
icon: .folder,
entries: []
)
let search: SessionSearchFn = { _, _, _, _ in
SessionIndexStore.SearchOutcome(entries: [], errors: [])
Expand Down
Loading