Skip to content
Merged
63 changes: 39 additions & 24 deletions CodeEdit/Features/CEWorkspace/Models/DirectoryEventStream.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,43 +138,58 @@ class DirectoryEventStream {

for (index, dictionary) in eventDictionaries.enumerated() {
// Get get file id use dictionary[kFSEventStreamEventExtendedFileIDKey] as? UInt64
guard let path = dictionary[kFSEventStreamEventExtendedDataPathKey] as? String,
let event = getEventFromFlags(eventFlags[index])
guard let path = dictionary[kFSEventStreamEventExtendedDataPathKey] as? String
else {
continue
}

events.append(.init(path: path, eventType: event))
let fsEvents = getEventsFromFlags(eventFlags[index])

for event in fsEvents {
events.append(.init(path: path, eventType: event))
}
}

callback(events)
}

/// Parses an ``FSEvent`` from the raw flag value.
/// Parses ``FSEvent`` from the raw flag value.
///
/// There can be multiple events in the raw flag value,
/// bacause of how OS processes almost simlutaneous actions – thus this functions returns a `Set` of `FSEvent`.
///
/// Often returns ``FSEvent/changeInDirectory`` as `FSEventStream` returns
/// Often returns ``[FSEvent/changeInDirectory]`` as `FSEventStream` returns
/// `kFSEventStreamEventFlagNone (0x00000000)` frequently without more information.
/// - Parameter raw: The int value received from the FSEventStream
/// - Returns: An ``FSEvent`` if a valid one was found, or `nil` otherwise.
func getEventFromFlags(_ raw: FSEventStreamEventFlags) -> FSEvent? {
/// - Returns: A `Set` of ``FSEvent``'s if at least one valid was found, or `[]` otherwise.
private func getEventsFromFlags(_ raw: FSEventStreamEventFlags) -> Set<FSEvent> {
var events: Set<FSEvent> = []

if raw == 0 {
return .changeInDirectory
} else if raw & UInt32(kFSEventStreamEventFlagRootChanged) > 0 {
return .rootChanged
} else if raw & UInt32(kFSEventStreamEventFlagItemChangeOwner) > 0 {
return .itemChangedOwner
} else if raw & UInt32(kFSEventStreamEventFlagItemCreated) > 0 {
return .itemCreated
} else if raw & UInt32(kFSEventStreamEventFlagItemCloned) > 0 {
return .itemCloned
} else if raw & UInt32(kFSEventStreamEventFlagItemModified) > 0 {
return .itemModified
} else if raw & UInt32(kFSEventStreamEventFlagItemRemoved) > 0 {
return .itemRemoved
} else if raw & UInt32(kFSEventStreamEventFlagItemRenamed) > 0 {
return .itemRenamed
} else {
return nil
events.insert(.changeInDirectory)
}
if raw & UInt32(kFSEventStreamEventFlagRootChanged) > 0 {
events.insert(.rootChanged)
}
if raw & UInt32(kFSEventStreamEventFlagItemChangeOwner) > 0 {
events.insert(.itemChangedOwner)
}
if raw & UInt32(kFSEventStreamEventFlagItemCreated) > 0 {
events.insert(.itemCreated)
}
if raw & UInt32(kFSEventStreamEventFlagItemCloned) > 0 {
events.insert(.itemCloned)
}
if raw & UInt32(kFSEventStreamEventFlagItemModified) > 0 {
events.insert(.itemModified)
}
if raw & UInt32(kFSEventStreamEventFlagItemRemoved) > 0 {
events.insert(.itemRemoved)
}
if raw & UInt32(kFSEventStreamEventFlagItemRenamed) > 0 {
events.insert(.itemRenamed)
}

return events
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,23 @@ final class CodeFileDocument: NSDocument, ObservableObject {
NotificationCenter.default.post(name: Self.didCloseNotification, object: fileURL)
}

override func save(_ sender: Any?) {
guard let fileURL else {
super.save(sender)
return
}

do {
// Get parent directory for cases when entire folders were deleted – and recreate them as needed
let directory = fileURL.deletingLastPathComponent()
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil)

try data(ofType: fileType ?? "").write(to: fileURL, options: .atomic)
} catch {
presentError(error)
}
}

func getLanguage() -> CodeLanguage {
guard let url = fileURL else {
return .default
Expand Down
47 changes: 29 additions & 18 deletions CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ struct EditorTabView: View {
@Environment(\.isFullscreen)
private var isFullscreen

@EnvironmentObject var workspace: WorkspaceDocument
@EnvironmentObject private var editorManager: EditorManager

@StateObject private var fileObserver: EditorTabFileObserver

@AppSettings(\.general.fileIconStyle)
var fileIconStyle

Expand Down Expand Up @@ -54,25 +57,25 @@ struct EditorTabView: View {

@EnvironmentObject private var editor: Editor

/// The item associated with the current tab.
/// The file item associated with the current tab.
///
/// You can get tab-related information from here, like `label`, `icon`, etc.
private var item: CEWorkspaceFile
private let tabFile: CEWorkspaceFile

var index: Int

private var isTemporary: Bool {
editor.temporaryTab?.file == item
editor.temporaryTab?.file == tabFile
}

/// Is the current tab the active tab.
private var isActive: Bool {
item == editor.selectedTab?.file
tabFile == editor.selectedTab?.file
}

/// Is the current tab being dragged.
private var isDragging: Bool {
draggingTabId == item.id
draggingTabId == tabFile.id
}

/// Is the current tab being held (by click and hold, not drag).
Expand All @@ -86,9 +89,9 @@ struct EditorTabView: View {
private func switchAction() {
// Only set the `selectedId` when they are not equal to avoid performance issue for now.
editorManager.activeEditor = editor
if editor.selectedTab?.file != item {
let tabItem = EditorInstance(file: item)
editor.setSelectedTab(item)
if editor.selectedTab?.file != tabFile {
let tabItem = EditorInstance(file: tabFile)
editor.setSelectedTab(tabFile)
editor.clearFuture()
editor.addToHistory(tabItem)
}
Expand All @@ -97,21 +100,22 @@ struct EditorTabView: View {
/// Close the current tab.
func closeAction() {
isAppeared = false
editor.closeTab(file: item)
editor.closeTab(file: tabFile)
}

init(
item: CEWorkspaceFile,
file: CEWorkspaceFile,
index: Int,
draggingTabId: CEWorkspaceFile.ID?,
onDragTabId: CEWorkspaceFile.ID?,
closeButtonGestureActive: Binding<Bool>
) {
self.item = item
self.tabFile = file
self.index = index
self.draggingTabId = draggingTabId
self.onDragTabId = onDragTabId
self._closeButtonGestureActive = closeButtonGestureActive
self._fileObserver = StateObject(wrappedValue: EditorTabFileObserver(file: file))
}

@ViewBuilder var content: some View {
Expand All @@ -122,26 +126,27 @@ struct EditorTabView: View {
)
// Tab content (icon and text).
HStack(alignment: .center, spacing: 3) {
Image(nsImage: item.nsIcon)
Image(nsImage: tabFile.nsIcon)
.frame(width: 16, height: 16)
.foregroundColor(
fileIconStyle == .color
&& activeState != .inactive && isActiveEditor
? item.iconColor
? tabFile.iconColor
: .secondary
)
Text(item.name)
Text(tabFile.name)
.font(
isTemporary
? .system(size: 11.0).italic()
: .system(size: 11.0)
)
.lineLimit(1)
.strikethrough(fileObserver.isDeleted, color: .primary)
}
.frame(maxHeight: .infinity) // To max-out the parent (tab bar) area.
.accessibilityElement(children: .ignore)
.accessibilityAddTraits(.isStaticText)
.accessibilityLabel(item.name)
.accessibilityLabel(tabFile.name)
.padding(.horizontal, 20)
.overlay {
ZStack {
Expand All @@ -152,7 +157,7 @@ struct EditorTabView: View {
isDragging: draggingTabId != nil || onDragTabId != nil,
closeAction: closeAction,
closeButtonGestureActive: $closeButtonGestureActive,
item: item,
item: tabFile,
isHoveringClose: $isHoveringClose
)
}
Expand Down Expand Up @@ -227,8 +232,14 @@ struct EditorTabView: View {
}
)
.zIndex(isActive ? 2 : (isDragging ? 3 : (isPressing ? 1 : 0)))
.id(item.id)
.tabBarContextMenu(item: item, isTemporary: isTemporary)
.id(tabFile.id)
.tabBarContextMenu(item: tabFile, isTemporary: isTemporary)
.accessibilityElement(children: .contain)
.onAppear {
workspace.workspaceFileManager?.addObserver(fileObserver)
}
.onDisappear {
workspace.workspaceFileManager?.removeObserver(fileObserver)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// EditorTabFileObserver.swift
// CodeEdit
//
// Created by Filipp Kuznetsov on 25.02.2025.
//

import Foundation
import SwiftUI

/// Observer ViewModel for tracking file deletion
@MainActor
final class EditorTabFileObserver: ObservableObject,
CEWorkspaceFileManagerObserver {
@Published private(set) var isDeleted = false

private let tabFile: CEWorkspaceFile

init(file: CEWorkspaceFile) {
self.tabFile = file
}

nonisolated func fileManagerUpdated(updatedItems: Set<CEWorkspaceFile>) {
Task { @MainActor in
if let parent = tabFile.parent, updatedItems.contains(parent) {
isDeleted = tabFile.doesExist == false
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ struct EditorTabs: View {
ForEach(Array(openedTabs.enumerated()), id: \.element) { index, id in
if let item = editor.tabs.first(where: { $0.file.id == id }) {
EditorTabView(
item: item.file,
file: item.file,
index: index,
draggingTabId: draggingTabId,
onDragTabId: onDragTabId,
Expand Down