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
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ enum ProjectReplacementComposition {
},
recordHistory: { [weak model] text, url in
guard let feature = await model?.activateHistoryModule() else { return }
await feature.recordHistorySnapshot(text: text, for: url, reason: .beforeBatchReplace)
await feature.recordHistorySnapshot(text: text, for: url, event: .beforeBatchReplace)
},
saveOverride: { [weak model] url, text in
guard let model,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ enum WorkspaceProjectionComposition {
},
openFile: { [weak model] url in model?.openFile(url) },
notify: { [weak notification] message in notification?.show(message) },
recordHistory: { [weak model] url, reason in
recordHistory: { [weak model] url, event in
guard let feature = await model?.activateHistoryModule() else { return }
await feature.recordHistory(containedIn: url, reason: reason)
await feature.recordHistory(containedIn: url, event: event)
},
relocateHistory: { [weak model] source, destination in
guard let feature = await model?.activateHistoryModule() else { return }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ extension LitheWorkspaceModule.WorkspaceFeatureModel {
restoreSession: @escaping @MainActor @Sendable (WorkspaceSession, [URL]) async -> Void,
openFile: @escaping @MainActor @Sendable (URL) -> Void,
notify: @escaping @MainActor @Sendable (String) -> Void,
recordHistory: @escaping @MainActor @Sendable (URL, LocalHistoryReason) async -> Void,
recordHistory: @escaping @MainActor @Sendable (URL, LocalHistoryEvent) async -> Void,
relocateHistory: @escaping @MainActor @Sendable (URL, URL) async -> Void,
moveTrackedItem: @escaping @MainActor @Sendable (URL, URL) async -> Bool = { _, _ in false },
relocateOpenDocuments: @escaping @MainActor @Sendable (URL, URL) -> Void,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Foundation
import LitheCoreContracts
import LitheModuleAPI

extension RustCoreBridge {
/// Snapshots Core records for an editing event; nil means Core rejected it, which was
/// logged here.
func historyPlanEvent(_ event: LocalHistoryEvent) -> [LocalHistorySnapshotPlan]? {
struct Request: Encodable { let event: String }
struct Snapshot: Decodable { let source: String; let reason: String }
struct Response: Decodable { let snapshots: [Snapshot] }
let result: Result<Response, CoreCallError> = executeResult(
command: "history.planEvent", payload: Request(event: event.rawValue)
)
switch result {
case .success(let response):
let plans = response.snapshots.compactMap { snapshot -> LocalHistorySnapshotPlan? in
guard let source = LocalHistorySnapshotPlan.Source(rawValue: snapshot.source),
let reason = LocalHistoryReason(rawValue: snapshot.reason) else { return nil }
return LocalHistorySnapshotPlan(source: source, reason: reason)
}
return plans.count == response.snapshots.count ? plans : nil
case .failure(let error):
coreLogSink?.emit(AppCoreFallbackSelected(
error: LitheModuleAPI.LogFailure(domain: "RustCore", code: error.code),
stage: "history.planEvent"
))
return nil
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import Foundation
import LitheCoreContracts
import LitheModuleAPI

extension RustCoreBridge {
/// Name for a copy of `name` that avoids the sibling names in `existing`. Core owns the
/// rule; nil means Core rejected the request, which was logged here.
func copyName(for name: String, isDirectory: Bool, existing: [String], duplicate: Bool) -> String? {
struct Item: Encodable { let name: String; let isDirectory: Bool }
struct DuplicateRequest: Encodable {
let name: String; let isDirectory: Bool; let existing: [String]
}
struct PasteRequest: Encodable { let items: [Item]; let existing: [String] }
struct DuplicateResponse: Decodable { let name: String }
struct PasteResponse: Decodable { let names: [String] }
if duplicate {
let result: Result<DuplicateResponse, CoreCallError> = executeResult(
command: "workspace.planDuplicate",
payload: DuplicateRequest(name: name, isDirectory: isDirectory, existing: existing)
)
return logged(result, stage: "workspace.planDuplicate")?.name
}
let result: Result<PasteResponse, CoreCallError> = executeResult(
command: "workspace.planPaste",
payload: PasteRequest(items: [Item(name: name, isDirectory: isDirectory)], existing: existing)
)
return logged(result, stage: "workspace.planPaste")?.names.first
}

/// Applies a directory action to the persisted rules in Core.
func reduceDirectoryAction(
_ action: WorkspaceDirectoryAction, at path: String, rules: WorkspaceDirectoryRules
) -> WorkspaceDirectoryReduction? {
struct Action: Encodable { let kind: String; let role: String? }
struct Request: Encodable { let path: String; let action: Action; let rules: WorkspaceDirectoryRules }
struct Response: Decodable { let changed: Bool; let rules: WorkspaceDirectoryRules }
let wire: Action
switch action {
case .exclude: wire = Action(kind: "exclude", role: nil)
case .cancelExclusion: wire = Action(kind: "cancelExclusion", role: nil)
case .setRole(let role): wire = Action(kind: "setRole", role: role.rawValue)
}
let result: Result<Response, CoreCallError> = executeResult(
command: "workspace.reduceDirectoryAction",
payload: Request(path: path, action: wire, rules: rules)
)
return logged(result, stage: "workspace.reduceDirectoryAction")
.map { WorkspaceDirectoryReduction(changed: $0.changed, rules: $0.rules) }
}

/// Migrates the persisted rules after a move (or a delete, without a destination).
func relocateDirectoryRules(
_ rules: WorkspaceDirectoryRules, moving source: String, to destination: String?
) -> WorkspaceDirectoryRules? {
struct Request: Encodable { let source: String; let destination: String?; let rules: WorkspaceDirectoryRules }
struct Response: Decodable { let rules: WorkspaceDirectoryRules }
let result: Result<Response, CoreCallError> = executeResult(
command: "workspace.relocateDirectoryRules",
payload: Request(source: source, destination: destination, rules: rules)
)
return logged(result, stage: "workspace.relocateDirectoryRules")?.rules
}

private func logged<Value>(_ result: Result<Value, CoreCallError>, stage: String) -> Value? {
switch result {
case .success(let value): return value
case .failure(let error):
coreLogSink?.emit(AppCoreFallbackSelected(
error: LitheModuleAPI.LogFailure(domain: "RustCore", code: error.code), stage: stage
))
return nil
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ struct RustLocalHistoryOperations: LocalHistoryOperations, Sendable {
func content(at storageURL: URL, contentPath: String) -> String? { core.historyContent(storageURL: storageURL, contentPath: contentPath)?.text }
func relocate(at storageURL: URL, sourcePath: String, destinationPath: String) -> Bool { core.historyRelocate(storageURL: storageURL, sourcePath: sourcePath, destinationPath: destinationPath) }

func planEvent(_ event: LocalHistoryEvent) -> [LocalHistorySnapshotPlan]? { core.historyPlanEvent(event) }

func usage(at storageURL: URL, limitBytes: Int64?) -> LocalHistoryUsage? {
core.historyUsage(storageURL: storageURL, limitBytes: limitBytes).map { LocalHistoryUsage(usedBytes: $0.usedBytes, limitBytes: $0.limitBytes) }
}
Expand Down
16 changes: 16 additions & 0 deletions macos/Sources/Lithe/Core/Rust/RustWorkspaceOperations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,22 @@ struct RustWorkspaceOperations: WorkspaceOperations, Sendable {
})?.map { rows[$0] }
}

func copyName(for name: String, isDirectory: Bool, existing: [String], duplicate: Bool) -> String? {
core.copyName(for: name, isDirectory: isDirectory, existing: existing, duplicate: duplicate)
}

func reduceDirectoryAction(
_ action: WorkspaceDirectoryAction, at path: String, rules: WorkspaceDirectoryRules
) -> WorkspaceDirectoryReduction? {
core.reduceDirectoryAction(action, at: path, rules: rules)
}

func relocateDirectoryRules(
_ rules: WorkspaceDirectoryRules, moving source: String, to destination: String?
) -> WorkspaceDirectoryRules? {
core.relocateDirectoryRules(rules, moving: source, to: destination)
}

func rootRows(at rootURL: URL, visibilityRules: FileVisibilityRules) -> [FileNode]? {
guard let entries = listDirectories([rootURL], at: rootURL, visibilityRules: visibilityRules)?[rootURL.standardizedFileURL.path] else {
return nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,22 @@ struct RemoteRoutingWorkspaceOperations: WorkspaceOperations {
local.orderedTreeSiblings(rows)
}

func copyName(for name: String, isDirectory: Bool, existing: [String], duplicate: Bool) -> String? {
local.copyName(for: name, isDirectory: isDirectory, existing: existing, duplicate: duplicate)
}

func reduceDirectoryAction(
_ action: WorkspaceDirectoryAction, at path: String, rules: WorkspaceDirectoryRules
) -> WorkspaceDirectoryReduction? {
local.reduceDirectoryAction(action, at: path, rules: rules)
}

func relocateDirectoryRules(
_ rules: WorkspaceDirectoryRules, moving source: String, to destination: String?
) -> WorkspaceDirectoryRules? {
local.relocateDirectoryRules(rules, moving: source, to: destination)
}

func warmSearchIndex(at rootURL: URL, visibilityRules: FileVisibilityRules) {
local.warmSearchIndex(at: rootURL, visibilityRules: visibilityRules)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,20 @@ package protocol WorkspaceOperations: Sendable {
/// first, then `workspace.orderEntries` name order); nil when Core cannot
/// order them. Swift never compares file names itself.
func orderedTreeSiblings(_ rows: [FileNode]) -> [FileNode]?
/// Core's name for a copy (`workspace.planPaste`, or `workspace.planDuplicate` when
/// `duplicate`) given the sibling names already known to exist; nil when Core cannot
/// plan. Swift never builds copy names itself.
func copyName(for name: String, isDirectory: Bool, existing: [String], duplicate: Bool) -> String?
/// Core's result of applying `action` to `path` (workspace-relative) in `rules`
/// (`workspace.reduceDirectoryAction`); nil when Core rejects the request.
func reduceDirectoryAction(
_ action: WorkspaceDirectoryAction, at path: String, rules: WorkspaceDirectoryRules
) -> WorkspaceDirectoryReduction?
/// Core's migration of `rules` after `source` moved to `destination`, or was deleted
/// when `destination` is nil (`workspace.relocateDirectoryRules`); nil when rejected.
func relocateDirectoryRules(
_ rules: WorkspaceDirectoryRules, moving source: String, to destination: String?
) -> WorkspaceDirectoryRules?
}

extension WorkspaceOperations {
Expand Down Expand Up @@ -168,6 +182,14 @@ package extension WorkspaceOperations {
/// Operations without Core (test doubles) cannot order rows, so an inserted
/// file reloads its directory listing instead.
func orderedTreeSiblings(_ rows: [FileNode]) -> [FileNode]? { nil }
/// Operations without Core (test doubles) cannot plan, so the file operation is refused.
func copyName(for name: String, isDirectory: Bool, existing: [String], duplicate: Bool) -> String? { nil }
func reduceDirectoryAction(
_ action: WorkspaceDirectoryAction, at path: String, rules: WorkspaceDirectoryRules
) -> WorkspaceDirectoryReduction? { nil }
func relocateDirectoryRules(
_ rules: WorkspaceDirectoryRules, moving source: String, to destination: String?
) -> WorkspaceDirectoryRules? { nil }
}

package protocol DirectoryWatcherFactory {
Expand Down Expand Up @@ -249,6 +271,33 @@ public enum LocalHistoryReason: String, Codable, Sendable {
}
}

/// An editing event that may record local-history snapshots. Rust Core decides which
/// snapshots each event records (`history.planEvent`), so every platform records at the
/// same moments.
public enum LocalHistoryEvent: String, Sendable {
case save, autoSave, restore, manual, unsavedDiscard, externalChange
case beforeRename, beforeBatchReplace
}

/// One snapshot Core plans for an event.
public struct LocalHistorySnapshotPlan: Equatable, Sendable {
/// Whose text the snapshot holds.
public enum Source: String, Sendable {
/// The file's text before the event changed it.
case beforeChange
/// The file's text after the event completed.
case afterChange
/// Unsaved editor text the event discards.
case editorBuffer
}
public let source: Source
public let reason: LocalHistoryReason
public init(source: Source, reason: LocalHistoryReason) {
self.source = source
self.reason = reason
}
}

package extension WorkspaceDirectoryMarkStoring {
func loadIndexingSettings(for workspaceURL: URL) throws -> WorkspaceIndexingSettings { WorkspaceIndexingSettings() }
func saveIndexingSettings(_ settings: WorkspaceIndexingSettings, for workspaceURL: URL) throws {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,33 @@ package enum WorkspaceDirectoryAction: Equatable, Sendable {
case cancelExclusion
}

/// The persisted directory rules of one workspace root, exactly as Rust Core's
/// directory rules model (`workspace.reduceDirectoryAction`) reads and returns them.
package struct WorkspaceDirectoryRules: Codable, Equatable, Sendable {
package var marks: [String: WorkspaceDirectoryMark]
package var includedDirectories: [String]
package var previousRoles: [String: WorkspaceDirectoryMark]
package init(
marks: [String: WorkspaceDirectoryMark],
includedDirectories: [String],
previousRoles: [String: WorkspaceDirectoryMark]
) {
self.marks = marks
self.includedDirectories = includedDirectories
self.previousRoles = previousRoles
}
}

/// Core's answer to a directory action: the full state to persist, and whether it differs.
package struct WorkspaceDirectoryReduction: Equatable, Sendable {
package let changed: Bool
package let rules: WorkspaceDirectoryRules
package init(changed: Bool, rules: WorkspaceDirectoryRules) {
self.changed = changed
self.rules = rules
}
}

/// Stable application failures. Presentation owns translation; adapters retain diagnostics.
package enum WorkspaceIndexingFailure: Error, Equatable, Sendable {
case invalidSettings, readFailed, writeFailed, configureFailed, workspaceUnavailable, permissionDenied
Expand Down
Loading
Loading