Skip to content

Commit 34ee976

Browse files
MarcoCarnevaliLuke
authored andcommitted
feat: observe directory changes with publisher (CodeEditApp#23)
(cherry picked from commit d4d14bf)
1 parent 44c344b commit 34ee976

11 files changed

Lines changed: 224 additions & 72 deletions

File tree

CodeEdit/ContentView.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,10 @@ import WorkspaceClient
1111
struct WorkspaceView: View {
1212
@State private var directoryURL: URL?
1313
@State private var workspaceClient: WorkspaceClient?
14-
1514
// TODO: Create a ViewModel to hold selectedId, openFileItems, ... to pass it to subviews as an EnvironmentObject (less boilerplate parameters)
16-
@State var selectedId: UUID?
17-
@State var openFileItems: [WorkspaceClient.FileItem] = []
18-
@State var urlInit = false
15+
@State private var selectedId: WorkspaceClient.FileId?
16+
@State private var openFileItems: [WorkspaceClient.FileItem] = []
17+
@State private var urlInit = false
1918

2019
@State private var showingAlert = false
2120
@State private var alertTitle = ""
@@ -52,7 +51,6 @@ struct WorkspaceView: View {
5251
.help("Show/Hide Sidebar")
5352
}
5453
}
55-
5654
if openFileItems.isEmpty {
5755
Text("Open file from sidebar")
5856
} else {

CodeEdit/Documents/WorkspaceDocument.swift

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,23 @@ import Foundation
99
import AppKit
1010
import SwiftUI
1111
import WorkspaceClient
12+
import Combine
1213

1314
@objc(WorkspaceDocument)
1415
class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate {
1516

1617
@Published var workspaceClient: WorkspaceClient?
17-
@Published var selectedId: UUID?
18+
@Published var selectedId: String?
1819
@Published var openFileItems: [WorkspaceClient.FileItem] = []
1920
@Published var sortFoldersOnTop: Bool = true
20-
21+
@Published var fileItems: [WorkspaceClient.FileItem] = []
2122
var openedCodeFiles: [WorkspaceClient.FileItem : CodeFile] = [:]
23+
private var cancellable: AnyCancellable?
24+
25+
deinit {
26+
cancellable?.cancel()
27+
cancellable = nil
28+
}
2229

2330
func closeFileTab(item: WorkspaceClient.FileItem) {
2431
defer {
@@ -81,16 +88,43 @@ class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate {
8188
window.contentView = NSHostingView(rootView: contentView)
8289
self.addWindowController(windowController)
8390
}
84-
91+
8592
override func read(from url: URL, ofType typeName: String) throws {
8693
self.workspaceClient = try .default(
8794
fileManager: .default,
8895
folderURL: url,
8996
ignoredFilesAndFolders: ignoredFilesAndDirectory
9097
)
98+
cancellable = workspaceClient?
99+
.getFiles
100+
.sink { [weak self] files in
101+
guard let self = self else { return }
102+
103+
// defer {
104+
// // this sorts the array alphabetically
105+
// self.fileItems = self.fileItems.sorted()
106+
// }
107+
108+
guard !self.fileItems.isEmpty else {
109+
self.fileItems = files
110+
return
111+
}
112+
113+
// Instead of rebuilding the array we want to
114+
// calculate the difference between the last iteration
115+
// and now. If the index of the file exists in the array
116+
// it means we need to remove the element, otherwise we need to append
117+
// it.
118+
let diff = files.difference(from: self.fileItems)
119+
diff.forEach { newFile in
120+
if let index = self.fileItems.firstIndex(of: newFile) {
121+
self.fileItems.remove(at: index)
122+
} else {
123+
self.fileItems.append(newFile)
124+
}
125+
}
126+
}
91127
}
92128

93-
override func write(to url: URL, ofType typeName: String) throws {
94-
95-
}
129+
override func write(to url: URL, ofType typeName: String) throws {}
96130
}

CodeEdit/SideBar/SideBar.swift

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,25 @@ import SwiftUI
99
import WorkspaceClient
1010

1111
struct SideBar: View {
12-
1312
@ObservedObject var workspace: WorkspaceDocument
1413
var windowController: NSWindowController
15-
1614
@State private var selection: Int = 0
17-
15+
1816
var body: some View {
1917
List {
20-
switch selection {
21-
case 0:
22-
Section(header: Text(workspace.fileURL?.lastPathComponent ?? "Unknown")) {
23-
ForEach(files.sortItems(foldersOnTop: workspace.sortFoldersOnTop)) { item in // Instead of OutlineGroup
24-
SideBarItem(item: item,
25-
workspace: workspace,
26-
windowController: windowController)
27-
}
28-
}
29-
default: EmptyView()
30-
}
18+
switch selection {
19+
case 0:
20+
Section(header: Text(workspace.fileURL?.lastPathComponent ?? "Unknown")) {
21+
ForEach(workspace.fileItems.sortItems(foldersOnTop: workspace.sortFoldersOnTop)) { item in // Instead of OutlineGroup
22+
SideBarItem(
23+
item: item,
24+
workspace: workspace,
25+
windowController: windowController
26+
)
27+
}
28+
}
29+
default: EmptyView()
30+
}
3131
}
3232
.safeAreaInset(edge: .top) {
3333
SideBarToolbarTop(selection: $selection)
@@ -37,8 +37,4 @@ struct SideBar: View {
3737
SideBarToolbarBottom(workspace: workspace)
3838
}
3939
}
40-
41-
private var files: [WorkspaceClient.FileItem] {
42-
workspace.workspaceClient?.getFiles() ?? []
43-
}
4440
}

CodeEdit/SideBar/SideBarItem.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import WorkspaceClient
1111
struct SideBarItem: View {
1212

1313
var item: WorkspaceClient.FileItem
14-
1514
@ObservedObject var workspace: WorkspaceDocument
1615
var windowController: NSWindowController
1716
@State var isExpanded: Bool = false

CodeEdit/TabBar/TabBarItem.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import WorkspaceClient
1010

1111
struct TabBarItem: View {
1212
var item: WorkspaceClient.FileItem
13-
1413
var windowController: NSWindowController
1514
@ObservedObject var workspace: WorkspaceDocument
1615

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
//
2-
// File.swift
3-
//
2+
// UnitTests.swift
3+
// CodeEdit
44
//
55
// Created by Marco Carnevali on 16/03/22.
66
//
7-
@testable import WorkspaceClient
7+
import Combine
88
import Foundation
9+
@testable import WorkspaceClient
910
import XCTest
1011

1112
final class WorkspaceClientUnitTests: XCTestCase {
12-
1313
let typeOfExtensions = ["json", "txt", "swift", "js", "py", "md"]
14-
14+
1515
func testListFile() throws {
1616
let directory = try FileManager.default.url(for: .developerApplicationDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
1717
.appendingPathComponent("CodeEdit", isDirectory: true)
1818
.appendingPathComponent("WorkspaceClientTests", isDirectory: true)
1919
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
20-
21-
let randomCount = Int.random(in: 1 ... 2)
20+
21+
var cancellable: AnyCancellable?
22+
let expectation = expectation(description: "wait for files")
23+
let randomCount = Int.random(in: 1 ... 100)
2224
let files = generateRandomFiles(amount: randomCount)
2325
try files.forEach {
2426
let fakeData = "fake string".data(using: .utf8)
@@ -31,11 +33,73 @@ final class WorkspaceClientUnitTests: XCTestCase {
3133
folderURL: directory,
3234
ignoredFilesAndFolders: []
3335
)
34-
print("file: ",files.count, " 2: ", client.getFiles().count)
35-
XCTAssertEqual(files, client.getFiles().map(\.url.lastPathComponent))
36+
37+
var newFiles: [WorkspaceClient.FileItem] = []
38+
39+
cancellable = client
40+
.getFiles
41+
.sink { files in
42+
newFiles = files
43+
expectation.fulfill()
44+
}
45+
46+
waitForExpectations(timeout: 0.5)
47+
48+
XCTAssertEqual(files.count, newFiles.count)
49+
try FileManager.default.removeItem(at: directory)
50+
cancellable?.cancel()
51+
}
52+
53+
func testDirectoryChanges() throws {
54+
let directory = try FileManager.default.url(for: .developerApplicationDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
55+
.appendingPathComponent("CodeEdit", isDirectory: true)
56+
.appendingPathComponent("WorkspaceClientTests", isDirectory: true)
57+
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
58+
59+
var cancellable: AnyCancellable?
60+
let expectation = expectation(description: "wait for files")
61+
expectation.expectedFulfillmentCount = 2
62+
63+
let randomCount = Int.random(in: 1 ... 100)
64+
var files = generateRandomFiles(amount: randomCount)
65+
try files.forEach {
66+
let fakeData = "fake string".data(using: .utf8)
67+
let fileUrl = directory
68+
.appendingPathComponent($0)
69+
try fakeData!.write(to: fileUrl)
70+
}
71+
72+
let client: WorkspaceClient = try .default(
73+
fileManager: .default,
74+
folderURL: directory,
75+
ignoredFilesAndFolders: []
76+
)
77+
78+
var newFiles: [WorkspaceClient.FileItem] = []
79+
80+
cancellable = client
81+
.getFiles
82+
.sink { files in
83+
newFiles = files
84+
expectation.fulfill()
85+
}
86+
87+
let nextBatchOfFiles = generateRandomFiles(amount: 1)
88+
files.append(contentsOf: nextBatchOfFiles)
89+
try files.forEach {
90+
let fakeData = "fake string".data(using: .utf8)
91+
let fileUrl = directory
92+
.appendingPathComponent($0)
93+
try fakeData!.write(to: fileUrl)
94+
}
95+
96+
waitForExpectations(timeout: 1.5)
97+
98+
XCTAssertEqual(files.count, newFiles.count)
3699
try FileManager.default.removeItem(at: directory)
100+
cancellable?.cancel()
37101
}
38-
102+
39103
func generateRandomFiles(amount: Int) -> [String] {
40104
[String](repeating: "", count: amount)
41105
.map { _ in
@@ -44,9 +108,9 @@ final class WorkspaceClientUnitTests: XCTestCase {
44108
return "\(fileName).\(fileExtension)"
45109
}
46110
}
47-
111+
48112
func randomString(length: Int) -> String {
49113
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
50-
return String((0..<length).map{ _ in letters.randomElement()! })
114+
return String((0 ..< length).map { _ in letters.randomElement()! })
51115
}
52116
}

CodeEditModules/Modules/WorkspaceClient/src/Interface.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,28 @@
44
//
55
// Created by Marco Carnevali on 16/03/22.
66
//
7+
8+
import Combine
79
import Foundation
810

911
public struct WorkspaceClient {
1012

1113
public var folderURL: () -> URL?
1214

13-
public var getFiles: () -> [FileItem]
15+
public var getFiles: AnyPublisher<[FileItem], Never>
1416

15-
public var getFileItem: (_ id: UUID) throws -> FileItem
17+
public var getFileItem: (_ id: String) throws -> FileItem
1618

1719
public init(
1820
folderURL: @escaping () -> URL?,
19-
getFiles: @escaping () -> [FileItem],
20-
getFileItem: @escaping (_ id: UUID) throws -> FileItem
21+
getFiles: AnyPublisher<[FileItem], Never>,
22+
getFileItem: @escaping (_ id: String) throws -> FileItem
2123
) {
2224
self.folderURL = folderURL
2325
self.getFiles = getFiles
2426
self.getFileItem = getFileItem
2527
}
26-
28+
2729
enum WorkspaceClientError: Error {
2830
case fileNotExist
2931
}

0 commit comments

Comments
 (0)