118 lines
3.6 KiB
Swift
118 lines
3.6 KiB
Swift
import Foundation
|
|
import SaplingCore
|
|
|
|
public protocol WorkspaceManaging: Sendable {
|
|
func openWorkspace(at url: URL) async throws -> Workspace
|
|
func loadItems(in directoryURL: URL) async throws -> [WorkspaceItem]
|
|
func sampleWorkspace() -> Workspace
|
|
}
|
|
|
|
public final class LocalWorkspaceManager: WorkspaceManaging, @unchecked Sendable {
|
|
private let fileManager: FileManager
|
|
|
|
public init(fileManager: FileManager = .default) {
|
|
self.fileManager = fileManager
|
|
}
|
|
|
|
public func openWorkspace(at url: URL) async throws -> Workspace {
|
|
let items = try scanItems(at: url)
|
|
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)
|
|
}
|
|
|
|
public func sampleWorkspace() -> Workspace {
|
|
SaplingSampleData.workspace
|
|
}
|
|
|
|
private func scanItems(at url: URL) throws -> [WorkspaceItem] {
|
|
guard let children = try? fileManager.contentsOfDirectory(
|
|
at: url,
|
|
includingPropertiesForKeys: [.isDirectoryKey],
|
|
options: directoryOptions
|
|
) 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))
|
|
}
|
|
|
|
return .folder(
|
|
WorkspaceFolder(
|
|
name: childURL.lastPathComponent,
|
|
url: childURL
|
|
)
|
|
)
|
|
}
|
|
|
|
return .file(
|
|
WorkspaceFile(
|
|
name: childURL.lastPathComponent,
|
|
url: childURL,
|
|
kind: fileKind(for: childURL)
|
|
)
|
|
)
|
|
}
|
|
.sorted(by: workspaceItemSort)
|
|
}
|
|
|
|
private var directoryOptions: FileManager.DirectoryEnumerationOptions {
|
|
[.skipsHiddenFiles, .skipsPackageDescendants]
|
|
}
|
|
|
|
private func isGitRepository(at url: URL) -> Bool {
|
|
fileManager.fileExists(atPath: url.appendingPathComponent(".git").path)
|
|
}
|
|
|
|
private func project(at url: URL) -> Project {
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|