Skip to content

Commit f60b595

Browse files
committed
Tweak workspace cleanup
1 parent cd70355 commit f60b595

File tree

4 files changed

+88
-56
lines changed

4 files changed

+88
-56
lines changed

Core/Sources/Service/ScheduledCleaner.swift

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import AppKit
33
import AXExtension
44
import Foundation
55
import Logger
6+
import XcodeInspector
67

78
public final class ScheduledCleaner {
89
public init() {
@@ -19,59 +20,56 @@ public final class ScheduledCleaner {
1920
for await app in ActiveApplicationMonitor.createStream() {
2021
try Task.checkCancellation()
2122
if let app, !app.isXcode {
22-
cleanUp()
23+
cleanUp()
2324
}
2425
}
2526
}
2627
}
2728

2829
@ServiceActor
2930
func cleanUp() {
30-
let availableTabs = findAvailableOpenedTabs()
31+
let workspaceInfos = XcodeInspector.shared.xcodes.reduce(
32+
into: [
33+
XcodeAppInstanceInspector.WorkspaceIdentifier:
34+
XcodeAppInstanceInspector.WorkspaceInfo
35+
]()
36+
) { result, xcode in
37+
let infos = xcode.workspaces
38+
for (id, info) in infos {
39+
if let existed = result[id] {
40+
result[id] = existed.combined(with: info)
41+
} else {
42+
result[id] = info
43+
}
44+
}
45+
}
46+
dump(workspaceInfos)
3147
for (url, workspace) in workspaces {
32-
if workspace.isExpired {
48+
if workspace.isExpired, workspaceInfos[.url(url)] == nil {
3349
Logger.service.info("Remove idle workspace")
3450
for url in workspace.filespaces.keys {
3551
WidgetDataSource.shared.cleanup(for: url)
3652
}
37-
workspace.cleanUp(availableTabs: availableTabs)
53+
workspace.cleanUp(availableTabs: [])
3854
workspaces[url] = nil
3955
} else {
56+
let tabs = (workspaceInfos[.url(url)]?.tabs ?? [])
57+
.union(workspaceInfos[.unknown]?.tabs ?? [])
4058
// cleanup chats for unused files
4159
let filespaces = workspace.filespaces
4260
for (url, _) in filespaces {
4361
if workspace.isFilespaceExpired(
4462
fileURL: url,
45-
availableTabs: availableTabs
63+
availableTabs: tabs
4664
) {
4765
Logger.service.info("Remove idle filespace")
4866
WidgetDataSource.shared.cleanup(for: url)
4967
}
5068
}
5169
// cleanup workspace
52-
workspace.cleanUp(availableTabs: availableTabs)
53-
}
54-
}
55-
}
56-
57-
func findAvailableOpenedTabs() -> Set<String> {
58-
guard let xcode = ActiveApplicationMonitor.latestXcode else { return [] }
59-
let app = AXUIElementCreateApplication(xcode.processIdentifier)
60-
let windows = app.windows.filter { $0.identifier == "Xcode.WorkspaceWindow" }
61-
guard !windows.isEmpty else { return [] }
62-
var allTabs = Set<String>()
63-
for window in windows {
64-
guard let editArea = window.firstChild(where: { $0.description == "editor area" })
65-
else { continue }
66-
let tabBars = editArea.children { $0.description == "tab bar" }
67-
for tabBar in tabBars {
68-
let tabs = tabBar.children { $0.roleDescription == "tab" }
69-
for tab in tabs {
70-
allTabs.insert(tab.title)
71-
}
70+
workspace.cleanUp(availableTabs: tabs)
7271
}
7372
}
74-
return allTabs
7573
}
7674
}
7775

Core/Sources/Service/Workspace.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ final class Workspace {
104104
let openedFileRecoverableStorage: OpenedFileRecoverableStorage
105105
var lastSuggestionUpdateTime = Environment.now()
106106
var isExpired: Bool {
107-
Environment.now().timeIntervalSince(lastSuggestionUpdateTime) > 60 * 60 * 8
107+
Environment.now().timeIntervalSince(lastSuggestionUpdateTime) > 60 * 60 * 4
108108
}
109109

110110
private(set) var filespaces = [URL: Filespace]()

Core/Sources/XcodeInspector/Helpers.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import AppKit
22
import Foundation
33

4-
public extension NSRunningApplication {
4+
extension NSRunningApplication {
55
var isXcode: Bool { bundleIdentifier == "com.apple.dt.Xcode" }
66
var isCopilotForXcodeExtensionService: Bool {
77
bundleIdentifier == Bundle.main.bundleIdentifier

Core/Sources/XcodeInspector/XcodeInspector.swift

Lines changed: 62 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ public final class XcodeInspector: ObservableObject {
108108
latestActiveXcode = xcode
109109
activeDocumentURL = xcode.documentURL
110110
focusedWindow = xcode.focusedWindow
111-
111+
112112
let setFocusedElement = { [weak self] in
113113
guard let self else { return }
114114
focusedElement = xcode.appElement.focusedElement
@@ -150,10 +150,23 @@ public class AppInstanceInspector: ObservableObject {
150150
}
151151

152152
public final class XcodeAppInstanceInspector: AppInstanceInspector {
153-
@Published var focusedWindow: XcodeWindowInspector?
154-
@Published var documentURL: URL = .init(fileURLWithPath: "/")
155-
@Published var projectURL: URL = .init(fileURLWithPath: "/")
156-
@Published var tabs: Set<String> = []
153+
public struct WorkspaceInfo {
154+
public let tabs: Set<String>
155+
156+
public func combined(with info: WorkspaceInfo) -> WorkspaceInfo {
157+
return .init(tabs: info.tabs.union(tabs))
158+
}
159+
}
160+
161+
public enum WorkspaceIdentifier: Hashable {
162+
case url(URL)
163+
case unknown
164+
}
165+
166+
@Published public var focusedWindow: XcodeWindowInspector?
167+
@Published public var documentURL: URL = .init(fileURLWithPath: "/")
168+
@Published public var projectURL: URL = .init(fileURLWithPath: "/")
169+
@Published public var workspaces = [WorkspaceIdentifier: WorkspaceInfo]()
157170
private var longRunningTasks = Set<Task<Void, Error>>()
158171
private var focusedWindowObservations = Set<AnyCancellable>()
159172

@@ -178,27 +191,22 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector {
178191

179192
longRunningTasks.insert(focusedWindowChanged)
180193

181-
if let updatedTabs = Self.findAvailableOpenedTabs(runningApplication) {
182-
tabs = updatedTabs
183-
}
194+
workspaces = Self.fetchWorkspaceInfo(runningApplication)
184195
let updateTabsTask = Task { @MainActor in
185196
let notification = AXNotificationStream(
186197
app: runningApplication,
187-
notificationNames: kAXFocusedUIElementChangedNotification
198+
notificationNames: kAXFocusedUIElementChangedNotification,
199+
kAXApplicationDeactivatedNotification
188200
)
189201
if #available(macOS 13.0, *) {
190202
for await _ in notification.debounce(for: .seconds(5)) {
191203
try Task.checkCancellation()
192-
if let updatedTabs = Self.findAvailableOpenedTabs(runningApplication) {
193-
tabs = updatedTabs
194-
}
204+
workspaces = Self.fetchWorkspaceInfo(runningApplication)
195205
}
196206
} else {
197207
for await _ in notification {
198208
try Task.checkCancellation()
199-
if let updatedTabs = Self.findAvailableOpenedTabs(runningApplication) {
200-
tabs = updatedTabs
201-
}
209+
workspaces = Self.fetchWorkspaceInfo(runningApplication)
202210
}
203211
}
204212
}
@@ -239,24 +247,50 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector {
239247
}
240248
}
241249

242-
static func findAvailableOpenedTabs(_ app: NSRunningApplication) -> Set<String>? {
250+
static func fetchWorkspaceInfo(
251+
_ app: NSRunningApplication
252+
) -> [WorkspaceIdentifier: WorkspaceInfo] {
243253
let app = AXUIElementCreateApplication(app.processIdentifier)
244-
guard app.isFocused else { return nil }
245254
let windows = app.windows.filter { $0.identifier == "Xcode.WorkspaceWindow" }
246-
guard !windows.isEmpty else { return [] }
247-
var allTabs = Set<String>()
255+
256+
var dict = [WorkspaceIdentifier: WorkspaceInfo]()
257+
248258
for window in windows {
249-
guard let editArea = window.firstChild(where: { $0.description == "editor area" })
250-
else { continue }
251-
let tabBars = editArea.children { $0.description == "tab bar" }
252-
for tabBar in tabBars {
253-
let tabs = tabBar.children { $0.roleDescription == "tab" }
254-
for tab in tabs {
255-
allTabs.insert(tab.title)
259+
let workspaceIdentifier = {
260+
for child in window.children {
261+
if child.description.starts(with: "/"), child.description.count > 1 {
262+
let path = child.description
263+
let trimmedNewLine = path.trimmingCharacters(in: .newlines)
264+
var url = URL(fileURLWithPath: trimmedNewLine)
265+
while !FileManager.default.fileIsDirectory(atPath: url.path) ||
266+
!url.pathExtension.isEmpty
267+
{
268+
url = url.deletingLastPathComponent()
269+
}
270+
return WorkspaceIdentifier.url(url)
271+
}
256272
}
257-
}
273+
return WorkspaceIdentifier.unknown
274+
}()
275+
276+
let tabs = {
277+
guard let editArea = window.firstChild(where: { $0.description == "editor area" })
278+
else { return Set<String>() }
279+
var allTabs = Set<String>()
280+
let tabBars = editArea.children { $0.description == "tab bar" }
281+
for tabBar in tabBars {
282+
let tabs = tabBar.children { $0.roleDescription == "tab" }
283+
for tab in tabs {
284+
allTabs.insert(tab.title)
285+
}
286+
}
287+
return allTabs
288+
}()
289+
290+
dict[workspaceIdentifier] = .init(tabs: tabs)
258291
}
259-
return allTabs
292+
293+
return dict
260294
}
261295
}
262296

0 commit comments

Comments
 (0)