Skip to content

Commit bcec2a5

Browse files
Open New Files in Workspace (#2043)
### Description When a new file is opened, tries to open it in a nearby workspace. Right now new files are opened in their own window no matter what (eg using `⌘N` or a file URL). The workspace is chosen by proximity, so if a user has `/User/Desktop` and `/User/Desktop/Project` open. Opening the file `/User/Desktop/Project/MAKEFILE` will open it in the `Project` workspace. ### Related Issues * Reported by @ BSM on Discord ### Checklist - [x] I read and understood the [contributing guide](https://github.com/CodeEditApp/CodeEdit/blob/main/CONTRIBUTING.md) as well as the [code of conduct](https://github.com/CodeEditApp/CodeEdit/blob/main/CODE_OF_CONDUCT.md) - [x] The issues this PR addresses are related to each other - [x] My changes generate no new warnings - [x] My code builds and runs on my machine - [x] My changes are all related to the related issue above - [x] I documented my code ### Screenshots (please ignore the messed up gutter, it's been fixed in CESE but not cut in a release) https://github.com/user-attachments/assets/8d341dee-d043-4b0a-8fd3-f544bf952abb Files open to the nearest folder, and bring the window to front. https://github.com/user-attachments/assets/96ec98a5-20ec-4d53-a996-446d9264e7c4
1 parent f2850b0 commit bcec2a5

File tree

4 files changed

+99
-5
lines changed

4 files changed

+99
-5
lines changed

CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,12 @@ final class CEWorkspaceFileManager {
103103
return nil
104104
}
105105

106-
// Drill down towards the file, indexing any directories needed.
107-
// If file is not in the `workspaceSettingsFolderURL` or subdirectories, exit.
108-
guard url.absoluteString.starts(with: folderUrl.absoluteString),
109-
url.pathComponents.count > folderUrl.pathComponents.count else {
106+
// If file is not in the `folderUrl` or subdirectories, exit.
107+
guard folderUrl.containsSubPath(url) else {
110108
return nil
111109
}
110+
111+
// Drill down towards the file, indexing any directories needed.
112112
let pathComponents = url.pathComponents.dropFirst(folderUrl.pathComponents.count)
113113
var currentURL = folderUrl
114114

CodeEdit/Features/Documents/Controllers/CodeEditDocumentController.swift

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,11 @@ final class CodeEditDocumentController: NSDocumentController {
5959
display displayDocument: Bool,
6060
completionHandler: @escaping (NSDocument?, Bool, Error?) -> Void
6161
) {
62-
super.openDocument(withContentsOf: url, display: displayDocument) { document, documentWasAlreadyOpen, error in
62+
guard !openFileInExistingWorkspace(url: url) else {
63+
return
64+
}
6365

66+
super.openDocument(withContentsOf: url, display: displayDocument) { document, documentWasAlreadyOpen, error in
6467
if let document {
6568
self.addDocument(document)
6669
} else {
@@ -73,6 +76,28 @@ final class CodeEditDocumentController: NSDocumentController {
7376
}
7477
}
7578

79+
/// Attempt to open the file URL in an open workspace, finding the nearest workspace to open it in if possible.
80+
/// - Parameter url: The file URL to open.
81+
/// - Returns: True, if the document was opened in a workspace.
82+
private func openFileInExistingWorkspace(url: URL) -> Bool {
83+
guard !url.isFolder else { return false }
84+
let workspaces = documents.compactMap({ $0 as? WorkspaceDocument })
85+
86+
// Check open workspaces for the file being opened. Sorted by shared components with the url so we
87+
// open the nearest workspace possible.
88+
for workspace in workspaces.sorted(by: {
89+
($0.fileURL?.sharedComponents(url) ?? 0) > ($1.fileURL?.sharedComponents(url) ?? 0)
90+
}) {
91+
// createIfNotFound will still return `nil` if the files don't share a common ancestor.
92+
if let newFile = workspace.workspaceFileManager?.getFile(url.absolutePath, createIfNotFound: true) {
93+
workspace.editorManager?.openTab(item: newFile)
94+
workspace.showWindows()
95+
return true
96+
}
97+
}
98+
return false
99+
}
100+
76101
override func removeDocument(_ document: NSDocument) {
77102
super.removeDocument(document)
78103

CodeEdit/Utils/Extensions/URL/URL+componentCompare.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,39 @@ extension URL {
1515
func componentCompare(_ other: URL) -> Bool {
1616
return self.pathComponents == other.pathComponents
1717
}
18+
19+
/// Determines if another URL is lower in the file system than this URL.
20+
///
21+
/// Examples:
22+
/// ```
23+
/// URL(filePath: "/Users/Bob/Desktop").containsSubPath(URL(filePath: "/Users/Bob/Desktop/file.txt")) // true
24+
/// URL(filePath: "/Users/Bob/Desktop").containsSubPath(URL(filePath: "/Users/Bob/Desktop/")) // false
25+
/// URL(filePath: "/Users/Bob/Desktop").containsSubPath(URL(filePath: "/Users/Bob/")) // false
26+
/// URL(filePath: "/Users/Bob/Desktop").containsSubPath(URL(filePath: "/Users/Bob/Desktop/Folder")) // true
27+
/// ```
28+
///
29+
/// - Parameter other: The URL to compare.
30+
/// - Returns: True, if the other URL is lower in the file system.
31+
func containsSubPath(_ other: URL) -> Bool {
32+
other.absoluteString.starts(with: absoluteString)
33+
&& other.pathComponents.count > pathComponents.count
34+
}
35+
36+
/// Compares this url with another, counting the number of shared path components. Stops counting once a
37+
/// different component is found.
38+
///
39+
/// - Note: URL treats a leading `/` as a component, so `/Users` and `/` will return `1`.
40+
/// - Parameter other: The URL to compare against.
41+
/// - Returns: The number of shared components.
42+
func sharedComponents(_ other: URL) -> Int {
43+
var count = 0
44+
for (component, otherComponent) in zip(pathComponents, other.pathComponents) {
45+
if component == otherComponent {
46+
count += 1
47+
} else {
48+
return count
49+
}
50+
}
51+
return count
52+
}
1853
}

CodeEditTests/Utils/UnitTests_Extensions.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,4 +196,38 @@ final class CodeEditUtilsExtensionsUnitTests: XCTestCase {
196196
let path = #"/Hello World/ With Spaces/ And " Characters "#
197197
XCTAssertEqual(path.escapedDirectory(), #""/Hello World/ With Spaces/ And \" Characters ""#)
198198
}
199+
200+
// MARK: - URL + Contains
201+
202+
func testURLContainsSubPath() {
203+
XCTAssertTrue(URL(filePath: "/Users/Bob/Desktop").containsSubPath(URL(filePath: "/Users/Bob/Desktop/file.txt")))
204+
XCTAssertFalse(URL(filePath: "/Users/Bob/Desktop").containsSubPath(URL(filePath: "/Users/Bob/Desktop/")))
205+
XCTAssertFalse(URL(filePath: "/Users/Bob/Desktop").containsSubPath(URL(filePath: "/Users/Bob/")))
206+
XCTAssertTrue(URL(filePath: "/Users/Bob/Desktop").containsSubPath(URL(filePath: "/Users/Bob/Desktop/Folder")))
207+
}
208+
209+
func testURLSharedComponentsCount() {
210+
// URL Treats the leading `/` as a component, so these all appear to have + 1 but are correct.
211+
XCTAssertEqual(
212+
URL(filePath: "/Users/Bob/Desktop").sharedComponents(URL(filePath: "/Users/Bob/Desktop/file.txt")),
213+
4
214+
)
215+
XCTAssertEqual(
216+
URL(filePath: "/Users/Bob/Desktop").sharedComponents(URL(filePath: "/Users/Bob/Desktop/")),
217+
4
218+
)
219+
XCTAssertEqual(
220+
URL(filePath: "/Users/Bob/Desktop").sharedComponents(URL(filePath: "/Users/Bob/")),
221+
3
222+
)
223+
XCTAssertEqual(
224+
URL(filePath: "/Users/Bob/Desktop").sharedComponents(URL(filePath: "/Users/Bob/Desktop/Folder")),
225+
4
226+
)
227+
228+
XCTAssertEqual(
229+
URL(filePath: "/Users/Bob/Desktop").sharedComponents(URL(filePath: "/Some Other/ Path ")),
230+
1
231+
)
232+
}
199233
}

0 commit comments

Comments
 (0)