Sources/Orgstar/SidebarFiles.swift
121 lines · 5186 bytes
8 symbols in this file
1import AppKit
2import OrgApp
3import SwiftUI
4
5/// Creating, renaming and trashing files from the sidebar.
6enum FileOperation: Equatable {
7 case create(folder: String)
8 case rename(path: String)
9 case trash(path: String)
10}
11
12extension Sidebar {
13 @ViewBuilder
14 func fileMenu(_ node: FileTreeNode) -> some View {
15 let folder = node.file == nil && !node.isDownloading ? node.path : (node.path as NSString).deletingLastPathComponent
16 Button("New File…") { ask(.create(folder: folder)) }
17 Divider()
18 Button("Rename…") { ask(.rename(path: node.path)) }
19 Button("Show in Finder") { NSWorkspace.shared.activateFileViewerSelecting([URL(fileURLWithPath: node.path)]) }
20 Divider()
21 Button("Move to Trash…") { ask(.trash(path: node.path)) }
22 }
23
24 func ask(_ operation: FileOperation) {
25 switch operation {
26 case .create: name = ""
27 case .rename(let path): name = (path as NSString).lastPathComponent
28 case .trash: break
29 }
30 pending = operation
31 }
32
33 /// The open buffers of `path`, a file or a folder holding them.
34 func openFiles(under path: String) -> [String] {
35 session.openPaths.filter { $0 == path || $0.hasPrefix(path + "/") }
36 }
37
38 /// Whether `path` is an open file with unsaved edits or a folder holding one.
39 func holdsUnsavedFile(_ path: String) -> Bool { openFiles(under: path).contains(where: session.dirtyPaths.contains) }
40
41 func finish() {
42 guard let operation = pending else { return }
43 pending = nil
44 Task {
45 do {
46 switch operation {
47 case .create(let folder):
48 selection = try await workspace.createFile(named: name, in: folder)
49 case .rename(let path):
50 let affected = openFiles(under: path)
51 // Open files are saved first, so the rename carries their edits, and open
52 // again under their new paths.
53 if !UnsavedChanges.resolve(session, paths: affected) { return }
54 let shown = session.path
55 let renamed = try await workspace.rename(path, to: name)
56 for open in affected {
57 session.kill(open)
58 session.open(renamed + open.dropFirst(path.count))
59 }
60 if let shown, affected.contains(shown) {
61 let moved = renamed + shown.dropFirst(path.count)
62 session.open(moved)
63 selection = moved
64 } else if let shown {
65 session.open(shown)
66 }
67 case .trash(let path):
68 for open in openFiles(under: path) { session.kill(open) }
69 if session.path == nil { selection = nil }
70 try await workspace.trash(path)
71 }
72 } catch {
73 failure = error.localizedDescription
74 }
75 }
76 }
77}
78
79extension View {
80 func fileOperations(_ sidebar: Sidebar) -> some View {
81 let names: Binding<String> = sidebar.$name
82 return self
83 .alert(title(sidebar.pending), isPresented: Binding { sidebar.pending?.needsName == true } set: { if !$0 { sidebar.pending = nil } }) {
84 TextField("Name", text: names)
85 Button(sidebar.pending.map { if case .create = $0 { "Create" } else { "Rename" } } ?? "OK") { sidebar.finish() }
86 Button("Cancel", role: .cancel) { sidebar.pending = nil }
87 } message: {
88 if case .create = sidebar.pending { Text("A name without an extension gets .org. Use folder/name to make folders.") }
89 }
90 .confirmationDialog(title(sidebar.pending), isPresented: Binding { sidebar.pending?.needsName == false } set: { if !$0 { sidebar.pending = nil } }) {
91 Button("Move to Trash", role: .destructive) { sidebar.finish() }
92 } message: {
93 if case .trash(let path) = sidebar.pending, sidebar.holdsUnsavedFile(path) {
94 Text("Unsaved changes to the open file will be lost.")
95 } else {
96 Text("You can put it back from the Trash in Finder.")
97 }
98 }
99 .alert("Couldn't change the file", isPresented: Binding { sidebar.failure != nil } set: { if !$0 { sidebar.failure = nil } }) {
100 Button("OK") { sidebar.failure = nil }
101 } message: {
102 Text(sidebar.failure ?? "")
103 }
104 }
105
106 private func title(_ operation: FileOperation?) -> String {
107 switch operation {
108 case .create(let folder): "New file in “\((folder as NSString).lastPathComponent)”"
109 case .rename(let path): "Rename “\((path as NSString).lastPathComponent)”"
110 case .trash(let path): "Move “\((path as NSString).lastPathComponent)” to the Trash?"
111 case nil: ""
112 }
113 }
114}
115
116extension FileOperation {
117 var needsName: Bool {
118 if case .trash = self { return false }
119 return true
120 }
121}