Skip to content

Commit

Permalink
Merge branch 'feature/1417_uniqueDocumentName'
Browse files Browse the repository at this point in the history
# Conflicts:
#	CHANGELOG.md
  • Loading branch information
1024jp committed Jan 19, 2025
2 parents 4e212ee + 9485e47 commit 2742c2f
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 3 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
5.1.0 (unreleased)
--------------------------

### New Features

- Append the nearest unique ancestor folder name to the document title if multiple documents with the same name are opened.



5.0.8 (692)
Expand Down
42 changes: 41 additions & 1 deletion CotEditor/Sources/Document Window/DocumentWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
// ---------------------------------------------------------------------------
//
// © 2004-2007 nakamuxu
// © 2013-2024 1024jp
// © 2013-2025 1024jp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -60,6 +60,7 @@ final class DocumentWindowController: NSWindowController, NSWindowDelegate {

private var windowAutosaveName: NSWindow.FrameAutosaveName
private var needsManualOnAppear = false
private var uniqueDirectory: String?

private lazy var editedIndicator: NSView = NSHostingView(rootView: Circle()
.fill(.tertiary)
Expand All @@ -73,6 +74,7 @@ final class DocumentWindowController: NSWindowController, NSWindowDelegate {
private var opacityObserver: AnyCancellable?
private var appearanceModeObserver: AnyCancellable?
private var fileDocumentNameObserver: AnyCancellable?
private var documentsObserver: AnyCancellable?

private var documentSyntaxObserver: AnyCancellable?
private var syntaxListObserver: AnyCancellable?
Expand Down Expand Up @@ -156,6 +158,14 @@ final class DocumentWindowController: NSWindowController, NSWindowDelegate {
UserDefaults.standard.publisher(for: .recentSyntaxNames))
.receive(on: RunLoop.main)
.sink { [weak self] _ in self?.buildSyntaxPopUpButton() }

// observe documents to update window title
self.documentsObserver = Publishers.Merge(
NotificationCenter.default.publisher(for: NSDocument.didChangeFileURLNotification, object: nil),
NotificationCenter.default.publisher(for: NSDocument.didMakeWindowNotification, object: nil)
)
.debounce(for: .seconds(0.1), scheduler: RunLoop.main)
.sink { [weak self] _ in self?.invalidateUniqueDirectory() }
}


Expand Down Expand Up @@ -187,6 +197,16 @@ final class DocumentWindowController: NSWindowController, NSWindowDelegate {
}


override func windowTitle(forDocumentDisplayName displayName: String) -> String {

if let uniqueDirectory {
displayName + " \u{2014} " + uniqueDirectory // EM DASH
} else {
displayName
}
}


override func synchronizeWindowTitleWithDocumentName() {

super.synchronizeWindowTitleWithDocumentName()
Expand Down Expand Up @@ -306,6 +326,26 @@ final class DocumentWindowController: NSWindowController, NSWindowDelegate {
}


/// Updates `uniqueDirectory`,
private func invalidateUniqueDirectory() {

// apply unique folder name to the window title
let fileURLs = NSApp.windows
.compactMap(\.windowController)
.compactMap { $0.document as? NSDocument }
.compactMap(\.fileURL)

let uniqueDirectory = (self.document as? NSDocument)?.fileURL?
.firstUniqueDirectoryURL(in: fileURLs)
.map { FileManager.default.displayName(atPath: $0.path(percentEncoded: false)) }

guard uniqueDirectory != self.uniqueDirectory else { return }

self.uniqueDirectory = uniqueDirectory
self.synchronizeWindowTitleWithDocumentName()
}


/// Builds syntax popup menu in toolbar.
private func buildSyntaxPopUpButton() {

Expand Down
10 changes: 10 additions & 0 deletions CotEditor/Sources/Document/DirectoryDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ final class DirectoryDocument: NSDocument {
}


override nonisolated var fileURL: URL? {

didSet {
NotificationCenter.default.post(name: NSDocument.didChangeFileURLNotification, object: self)
}
}


override func encodeRestorableState(with coder: NSCoder, backgroundQueue queue: OperationQueue) {

super.encodeRestorableState(with: coder, backgroundQueue: queue)
Expand Down Expand Up @@ -102,6 +110,8 @@ final class DirectoryDocument: NSDocument {

self.addWindowController(DocumentWindowController(directoryDocument: self))

NotificationCenter.default.post(name: NSDocument.didMakeWindowNotification, object: self)

// observe document updates for the edited marker in the close button
if self.documentObserver == nil {
self.documentObserver = NotificationCenter.default.addObserver(forName: Document.didUpdateChange, object: nil, queue: .main) { [unowned self] _ in
Expand Down
10 changes: 10 additions & 0 deletions CotEditor/Sources/Document/Document.swift
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,14 @@ extension Document: EditorSource {
}


@ObservationIgnored override nonisolated var fileURL: URL? {

didSet {
NotificationCenter.default.post(name: NSDocument.didChangeFileURLNotification, object: self)
}
}


override func makeWindowControllers() {

// -> The window controller already exists either when:
Expand All @@ -243,6 +251,8 @@ extension Document: EditorSource {
.receive(on: RunLoop.main)
.assign(to: \.isWhitePaper, on: windowController)
}

NotificationCenter.default.post(name: NSDocument.didMakeWindowNotification, object: self)
}

self.applyContentToWindow()
Expand Down
9 changes: 8 additions & 1 deletion CotEditor/Sources/Utilities/AppKit/NSDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//
// ---------------------------------------------------------------------------
//
// © 2016-2024 1024jp
// © 2016-2025 1024jp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -25,6 +25,13 @@

import AppKit.NSDocument

extension NSDocument {

nonisolated static let didChangeFileURLNotification = Notification.Name("DocumentDidChangeFileURLNotification")
nonisolated static let didMakeWindowNotification = Notification.Name("DocumentDidMakeWindowNotification")
}


extension NSDocument.SaveOperationType {

/// The save operation is a kind of an autosave.
Expand Down
33 changes: 33 additions & 0 deletions Packages/EditorCore/Sources/URLUtils/URL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,39 @@ public extension URL {

return zip(ancestorComponents, childComponents).allSatisfy(==)
}


/// Returns the URL of the first unique directory among the given URLs.
///
/// - Parameter urls: The file URLs to find.
/// - Returns: A directory URL.
func firstUniqueDirectoryURL(in urls: [URL]) -> URL? {

let duplicatedURLs = urls
.filter { $0 != self }
.filter { $0.lastPathComponent == self.lastPathComponent }

guard !duplicatedURLs.isEmpty else { return nil }

let components = duplicatedURLs
.map { Array($0.pathComponents.reversed()) }

let offset = self.pathComponents
.reversed()
.enumerated()
.dropFirst() // last path component is already checked
.first { (index, component) in
!components
.filter { $0.indices.contains(index) }
.compactMap { $0[index] }
.contains(component)
}?
.offset

guard let offset else { return nil }

return (0..<offset).reduce(into: self) { (url, _) in url.deleteLastPathComponent() }
}
}


Expand Down
16 changes: 15 additions & 1 deletion Packages/EditorCore/Tests/URLUtilsTests/URLExtensionsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
//
// ---------------------------------------------------------------------------
//
// © 2016-2024 1024jp
// © 2016-2025 1024jp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -82,6 +82,20 @@ struct URLExtensionsTests {
}


@Test func firstUniqueDirectoryURL() {

let urls: [URL] = [
URL(string: "Dog/Cow/file.txt")!,
URL(string: "Dog/Sheep/file.txt")!,

]

#expect(URL(string: "Dog/Cow/file copy.txt")!.firstUniqueDirectoryURL(in: urls) == nil)
#expect(URL(string: "Cat/Cow/file.txt")!.firstUniqueDirectoryURL(in: urls) == URL(string: "Cat/"))
#expect(URL(string: "Dog/Pig/file.txt")!.firstUniqueDirectoryURL(in: urls) == URL(string: "Dog/Pig/"))
}


@Test func createItemReplacementDirectory() throws {

#expect(throws: Never.self) { try URL.itemReplacementDirectory }
Expand Down

0 comments on commit 2742c2f

Please sign in to comment.