Make remote sidebar directory canonicalization preserve live paths

This commit is contained in:
Lawrence Chen 2026-03-19 01:23:53 -07:00
parent 55ec827d48
commit 183c5601be
No known key found for this signature in database
2 changed files with 216 additions and 27 deletions

View file

@ -4555,6 +4555,154 @@ enum SidebarBranchOrdering {
let directory: String?
}
fileprivate static func normalizedDirectory(_ text: String?) -> String? {
guard let text else { return nil }
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
private static func relativePathFromTilde(_ directory: String) -> String? {
let normalized = normalizedDirectory(directory)
switch normalized {
case "~":
return ""
case let path? where path.hasPrefix("~/"):
return String(path.dropFirst(2))
default:
return nil
}
}
private static func commonHomeDirectoryPrefix(from absoluteDirectory: String) -> String? {
guard let normalized = normalizedDirectory(absoluteDirectory) else { return nil }
let standardized = NSString(string: normalized).standardizingPath
if standardized == "/root" || standardized.hasPrefix("/root/") {
return "/root"
}
let components = NSString(string: standardized).pathComponents
if components.count >= 3, components[0] == "/", components[1] == "Users" {
return NSString.path(withComponents: Array(components.prefix(3)))
}
if components.count >= 3, components[0] == "/", components[1] == "home" {
return NSString.path(withComponents: Array(components.prefix(3)))
}
if components.count >= 4, components[0] == "/", components[1] == "var", components[2] == "home" {
return NSString.path(withComponents: Array(components.prefix(4)))
}
return nil
}
private static func inferredHomeDirectory(
matchingTildeDirectory tildeDirectory: String,
absoluteDirectory: String
) -> String? {
guard let relativePath = relativePathFromTilde(tildeDirectory),
let normalizedAbsolute = normalizedDirectory(absoluteDirectory) else { return nil }
let standardizedAbsolute = NSString(string: normalizedAbsolute).standardizingPath
let homeDirectory: String
if relativePath.isEmpty {
homeDirectory = standardizedAbsolute
} else {
let suffix = "/" + relativePath
guard standardizedAbsolute.hasSuffix(suffix) else { return nil }
homeDirectory = String(standardizedAbsolute.dropLast(suffix.count))
}
guard commonHomeDirectoryPrefix(from: homeDirectory) == homeDirectory else { return nil }
return homeDirectory
}
fileprivate static func inferredRemoteHomeDirectory(
from directories: [String],
fallbackDirectory: String?
) -> String? {
let candidates = directories + [fallbackDirectory].compactMap { $0 }
let tildeDirectories = candidates.compactMap { directory -> String? in
guard let normalized = normalizedDirectory(directory),
relativePathFromTilde(normalized) != nil else { return nil }
return normalized
}
let absoluteDirectories = candidates.compactMap { directory -> String? in
guard let normalized = normalizedDirectory(directory), normalized.hasPrefix("/") else { return nil }
return NSString(string: normalized).standardizingPath
}
let inferredHomes = Set(
tildeDirectories.flatMap { tildeDirectory in
absoluteDirectories.compactMap { absoluteDirectory in
inferredHomeDirectory(
matchingTildeDirectory: tildeDirectory,
absoluteDirectory: absoluteDirectory
)
}
}
)
if inferredHomes.count == 1 {
return inferredHomes.first
}
if !inferredHomes.isEmpty {
return nil
}
return absoluteDirectories.lazy.compactMap(commonHomeDirectoryPrefix(from:)).first
}
private static func expandedTildePath(
_ directory: String,
homeDirectoryForTildeExpansion: String?
) -> String {
guard let relativePath = relativePathFromTilde(directory),
let homeDirectory = normalizedDirectory(homeDirectoryForTildeExpansion) else {
return directory
}
if relativePath.isEmpty {
return homeDirectory
}
return NSString(string: homeDirectory).appendingPathComponent(relativePath)
}
fileprivate static func canonicalDirectoryKey(
_ directory: String?,
homeDirectoryForTildeExpansion: String?
) -> String? {
guard let directory = normalizedDirectory(directory) else { return nil }
let expanded = expandedTildePath(
directory,
homeDirectoryForTildeExpansion: homeDirectoryForTildeExpansion
)
let standardized = NSString(string: expanded).standardizingPath
let cleaned = standardized.trimmingCharacters(in: .whitespacesAndNewlines)
return cleaned.isEmpty ? nil : cleaned
}
private static func preferredDisplayedDirectory(
existing: String?,
replacement: String?,
homeDirectoryForTildeExpansion: String?
) -> String? {
guard let replacement = normalizedDirectory(replacement) else { return existing }
guard let existing = normalizedDirectory(existing) else { return replacement }
let existingUsesTilde = relativePathFromTilde(existing) != nil
let replacementUsesTilde = relativePathFromTilde(replacement) != nil
if existingUsesTilde != replacementUsesTilde {
return replacementUsesTilde ? existing : replacement
}
if canonicalDirectoryKey(existing, homeDirectoryForTildeExpansion: homeDirectoryForTildeExpansion)
== canonicalDirectoryKey(
replacement,
homeDirectoryForTildeExpansion: homeDirectoryForTildeExpansion
) {
return existing
}
return replacement
}
static func orderedPaneIds(tree: ExternalTreeNode) -> [String] {
switch tree {
case .pane(let pane):
@ -4699,6 +4847,7 @@ enum SidebarBranchOrdering {
panelBranches: [UUID: SidebarGitBranchState],
panelDirectories: [UUID: String],
defaultDirectory: String?,
homeDirectoryForTildeExpansion: String?,
fallbackBranch: SidebarGitBranchState?
) -> [BranchDirectoryEntry] {
struct EntryKey: Hashable {
@ -4712,20 +4861,7 @@ enum SidebarBranchOrdering {
var directory: String?
}
func normalized(_ text: String?) -> String? {
guard let text else { return nil }
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
func canonicalDirectoryKey(_ directory: String?) -> String? {
guard let directory = normalized(directory) else { return nil }
let expanded = NSString(string: directory).expandingTildeInPath
let standardized = NSString(string: expanded).standardizingPath
let cleaned = standardized.trimmingCharacters(in: .whitespacesAndNewlines)
return cleaned.isEmpty ? nil : cleaned
}
let normalized = normalizedDirectory
let normalizedFallbackBranch = normalized(fallbackBranch?.branch)
let shouldUseFallbackBranchPerPanel = !orderedPanelIds.contains {
normalized(panelBranches[$0]?.branch) != nil
@ -4747,7 +4883,10 @@ enum SidebarBranchOrdering {
: defaultBranchDirty
let key: EntryKey
if let directoryKey = canonicalDirectoryKey(directory) {
if let directoryKey = canonicalDirectoryKey(
directory,
homeDirectoryForTildeExpansion: homeDirectoryForTildeExpansion
) {
// Keep one line per directory and allow the latest branch state to overwrite.
key = EntryKey(directory: directoryKey, branch: nil)
} else {
@ -4764,9 +4903,11 @@ enum SidebarBranchOrdering {
} else if existing.branch == nil {
existing.isDirty = panelDirty
}
if let directory {
existing.directory = directory
}
existing.directory = preferredDisplayedDirectory(
existing: existing.directory,
replacement: directory,
homeDirectoryForTildeExpansion: homeDirectoryForTildeExpansion
)
entries[key] = existing
} else if panelDirty {
existing.isDirty = true
@ -5939,12 +6080,16 @@ final class Workspace: Identifiable, ObservableObject {
return trimmed.isEmpty ? nil : trimmed
}
private func canonicalSidebarDirectoryKey(_ directory: String?) -> String? {
guard let directory = normalizedSidebarDirectory(directory) else { return nil }
let expanded = NSString(string: directory).expandingTildeInPath
let standardized = NSString(string: expanded).standardizingPath
let cleaned = standardized.trimmingCharacters(in: .whitespacesAndNewlines)
return cleaned.isEmpty ? nil : cleaned
private func sidebarHomeDirectoryForCanonicalization(
resolvedPanelDirectories: [UUID: String]
) -> String? {
if isRemoteWorkspace {
return SidebarBranchOrdering.inferredRemoteHomeDirectory(
from: Array(resolvedPanelDirectories.values),
fallbackDirectory: normalizedSidebarDirectory(currentDirectory)
)
}
return FileManager.default.homeDirectoryForCurrentUser.path
}
private func sidebarResolvedDirectory(for panelId: UUID) -> String? {
@ -5972,12 +6117,18 @@ final class Workspace: Identifiable, ObservableObject {
func sidebarDirectoriesInDisplayOrder(orderedPanelIds: [UUID]) -> [String] {
let resolvedDirectories = sidebarResolvedPanelDirectories(orderedPanelIds: orderedPanelIds)
let homeDirectoryForCanonicalization = sidebarHomeDirectoryForCanonicalization(
resolvedPanelDirectories: resolvedDirectories
)
var ordered: [String] = []
var seen: Set<String> = []
for panelId in orderedPanelIds {
guard let directory = resolvedDirectories[panelId],
let key = canonicalSidebarDirectoryKey(directory) else { continue }
let key = SidebarBranchOrdering.canonicalDirectoryKey(
directory,
homeDirectoryForTildeExpansion: homeDirectoryForCanonicalization
) else { continue }
if seen.insert(key).inserted {
ordered.append(directory)
}
@ -6011,11 +6162,15 @@ final class Workspace: Identifiable, ObservableObject {
func sidebarBranchDirectoryEntriesInDisplayOrder(
orderedPanelIds: [UUID]
) -> [SidebarBranchOrdering.BranchDirectoryEntry] {
SidebarBranchOrdering.orderedUniqueBranchDirectoryEntries(
let resolvedDirectories = sidebarResolvedPanelDirectories(orderedPanelIds: orderedPanelIds)
return SidebarBranchOrdering.orderedUniqueBranchDirectoryEntries(
orderedPanelIds: orderedPanelIds,
panelBranches: panelGitBranches,
panelDirectories: sidebarResolvedPanelDirectories(orderedPanelIds: orderedPanelIds),
panelDirectories: resolvedDirectories,
defaultDirectory: normalizedSidebarDirectory(currentDirectory),
homeDirectoryForTildeExpansion: sidebarHomeDirectoryForCanonicalization(
resolvedPanelDirectories: resolvedDirectories
),
fallbackBranch: gitBranch
)
}

View file

@ -238,6 +238,7 @@ final class SidebarBranchOrderingTests: XCTestCase {
fifth: "/repo/e"
],
defaultDirectory: "/repo/default",
homeDirectoryForTildeExpansion: nil,
fallbackBranch: SidebarGitBranchState(branch: "fallback", isDirty: false)
)
@ -264,6 +265,7 @@ final class SidebarBranchOrderingTests: XCTestCase {
second: "/repo/two"
],
defaultDirectory: "/repo/default",
homeDirectoryForTildeExpansion: nil,
fallbackBranch: SidebarGitBranchState(branch: "main", isDirty: true)
)
@ -282,6 +284,7 @@ final class SidebarBranchOrderingTests: XCTestCase {
panelBranches: [:],
panelDirectories: [:],
defaultDirectory: "/repo/default",
homeDirectoryForTildeExpansion: nil,
fallbackBranch: SidebarGitBranchState(branch: "main", isDirty: false)
)
@ -291,6 +294,37 @@ final class SidebarBranchOrderingTests: XCTestCase {
)
}
func testOrderedUniqueBranchDirectoryEntriesKeepsAbsoluteDirectoryWhenLaterEntryUsesTildeAlias() {
let first = UUID()
let second = UUID()
let rows = SidebarBranchOrdering.orderedUniqueBranchDirectoryEntries(
orderedPanelIds: [first, second],
panelBranches: [
first: SidebarGitBranchState(branch: "main", isDirty: false),
second: SidebarGitBranchState(branch: "feature", isDirty: true)
],
panelDirectories: [
first: "/home/remoteuser/project",
second: "~/project"
],
defaultDirectory: nil,
homeDirectoryForTildeExpansion: "/home/remoteuser",
fallbackBranch: nil
)
XCTAssertEqual(
rows,
[
SidebarBranchOrdering.BranchDirectoryEntry(
branch: "feature",
isDirty: true,
directory: "/home/remoteuser/project"
)
]
)
}
func testOrderedUniquePullRequestsFollowsPanelOrderAcrossSplitsAndTabs() {
let first = UUID()
let second = UUID()