Sapling/Sources/SaplingWorkspace/WorkspaceManager.swift

119 lines
3.6 KiB
Swift
Raw Normal View History

2026-05-29 15:19:33 +02:00
import Foundation
import SaplingCore
public protocol WorkspaceManaging: Sendable {
func openWorkspace(at url: URL) async throws -> Workspace
func loadItems(in directoryURL: URL) async throws -> [WorkspaceItem]
2026-05-29 15:19:33 +02:00
func sampleWorkspace() -> Workspace
}
public final class LocalWorkspaceManager: WorkspaceManaging, @unchecked Sendable {
private let fileManager: FileManager
public init(fileManager: FileManager = .default) {
2026-05-29 15:19:33 +02:00
self.fileManager = fileManager
}
public func openWorkspace(at url: URL) async throws -> Workspace {
let items = try scanItems(at: url)
2026-05-29 15:19:33 +02:00
let workspace = Workspace(name: url.lastPathComponent, rootURL: url, items: items)
try SaplingRules.validateWorkspace(workspace)
return workspace
}
public func loadItems(in directoryURL: URL) async throws -> [WorkspaceItem] {
try scanItems(at: directoryURL)
}
2026-05-29 15:19:33 +02:00
public func sampleWorkspace() -> Workspace {
SaplingSampleData.workspace
}
private func scanItems(at url: URL) throws -> [WorkspaceItem] {
2026-05-29 15:19:33 +02:00
guard let children = try? fileManager.contentsOfDirectory(
at: url,
includingPropertiesForKeys: [.isDirectoryKey],
options: directoryOptions
2026-05-29 15:19:33 +02:00
) else {
return []
}
return try children
.map { childURL in
let values = try childURL.resourceValues(forKeys: [.isDirectoryKey])
if values.isDirectory == true {
if isGitRepository(at: childURL) {
return .project(project(at: childURL))
}
2026-05-29 15:19:33 +02:00
return .folder(
WorkspaceFolder(
name: childURL.lastPathComponent,
url: childURL
2026-05-29 15:19:33 +02:00
)
)
}
return .file(
WorkspaceFile(
name: childURL.lastPathComponent,
url: childURL,
kind: fileKind(for: childURL)
)
)
}
.sorted(by: workspaceItemSort)
2026-05-29 15:19:33 +02:00
}
private var directoryOptions: FileManager.DirectoryEnumerationOptions {
[.skipsHiddenFiles, .skipsPackageDescendants]
}
2026-05-29 15:19:33 +02:00
private func isGitRepository(at url: URL) -> Bool {
fileManager.fileExists(atPath: url.appendingPathComponent(".git").path)
}
private func project(at url: URL) -> Project {
2026-05-29 15:19:33 +02:00
let repository = GitRepository(
name: url.lastPathComponent,
rootURL: url,
statusSummary: .unknown
)
return Project(
name: url.lastPathComponent,
repositoryURL: url,
gitRepository: repository
)
}
private func workspaceItemSort(_ lhs: WorkspaceItem, _ rhs: WorkspaceItem) -> Bool {
if lhs.sortGroup != rhs.sortGroup {
return lhs.sortGroup < rhs.sortGroup
}
return lhs.displayName.localizedStandardCompare(rhs.displayName) == .orderedAscending
}
2026-05-29 15:19:33 +02:00
private func fileKind(for url: URL) -> WorkspaceFileKind {
switch url.pathExtension.lowercased() {
case "md", "markdown":
return .markdown
case "png", "jpg", "jpeg", "gif", "webp", "pdf", "mp3", "mp4":
return .attachment
default:
return .other
}
}
}
private extension WorkspaceItem {
var sortGroup: Int {
switch self {
case .folder, .project, .subproject:
return 0
case .file:
return 1
}
}
}