Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
4daab87
Merge tag '0.29.0' into develop
intitni Jan 9, 2024
2f97bd4
Update SwiftSyntax to 509.0.2
intitni Jan 9, 2024
66c5151
Merge branch 'feature/update-swift-syntax-version' into develop
intitni Jan 9, 2024
c23d06f
Update
intitni Jan 9, 2024
0540c2b
Update
intitni Jan 9, 2024
3bbaf83
Update to only use closure and case as focused range when it's more t…
intitni Jan 9, 2024
1a4d5b1
Move command trigger to XcodeInspector
intitni Jan 9, 2024
5d6bd73
Remove unused content from Environment.swift
intitni Jan 9, 2024
a377916
Fix unit tests
intitni Jan 9, 2024
59418d2
Remove Environment.swift and it's package
intitni Jan 9, 2024
d0e905d
Remove useless imports of Environment
intitni Jan 9, 2024
d8065c3
Move NoAccessToAccessibilityAPIError to XPCService.swift
intitni Jan 9, 2024
ea98260
Move now to Workspace
intitni Jan 9, 2024
d36a861
Make the extensions public
intitni Jan 9, 2024
7393c89
Fix incorrect variable call
intitni Jan 9, 2024
fbf5644
Use XcodeInspector to get urls instead of Environment
intitni Jan 9, 2024
837c2da
Change currentDirectoryPath to currentDirectoryURL
intitni Jan 9, 2024
dd95c9a
Merge branch 'feature/remove-environment-dot-swift' into develop
intitni Jan 9, 2024
012b4f9
Make activateXcode optional
intitni Jan 11, 2024
29737d4
Merge branch 'feature/copilot-for-xcode-kit-update-240109' into develop
intitni Jan 11, 2024
815ac67
Merge tag '0.29.1' into develop
intitni Jan 15, 2024
e73363f
Update TextSplitter to generate TextChunk
intitni Jan 14, 2024
bbf2bd2
Support creating CodeLanguage with file path
intitni Jan 14, 2024
31a12e9
Update get code function
intitni Jan 15, 2024
7985d0a
Update
intitni Jan 15, 2024
ee31bb1
Update
intitni Jan 16, 2024
dd81d17
Merge branch 'feature/integrating-keyword-base-retrieval' into develop
intitni Jan 16, 2024
c2d0fa2
Update
intitni Jan 16, 2024
7eebdb8
Supports esc to dismiss suggestion
intitni Jan 16, 2024
27c576e
Change the close button in compact suggestion panel to dismiss instea…
intitni Jan 16, 2024
b47de8f
Add a toggle for dismiss suggestion with ESC
intitni Jan 16, 2024
57f2577
Update
intitni Jan 16, 2024
ffd258c
Merge branch 'feature/esc-to-dismiss-suggestion' into develop
intitni Jan 16, 2024
2541553
Add normal title bar to chat window
intitni Jan 16, 2024
1c81ab1
Add attach button to title bar
intitni Jan 16, 2024
a875b92
Merge branch 'feature/chat-panel-with-normal-title-bar' into develop
intitni Jan 16, 2024
56151df
Fix mouse events in hidden chat window
intitni Jan 16, 2024
7cab434
Adjust style of chat window
intitni Jan 17, 2024
c364703
Move detach/attach button to the right side
intitni Jan 17, 2024
0920414
Fix the conflicts between fullscreen and attach
intitni Jan 17, 2024
1100db9
Fix reattaching animation
intitni Jan 17, 2024
c85654a
Support exporting custom commands
intitni Jan 17, 2024
974bba1
Merge branch 'feature/export-import-custom-command' into develop
intitni Jan 17, 2024
5d6b7e3
Update
intitni Jan 17, 2024
1a26846
Add accessibility API status to menu item
intitni Jan 17, 2024
4ad3387
Update
intitni Jan 17, 2024
8433e9b
Update
intitni Jan 20, 2024
d189798
Add merged() to TextChunk
intitni Jan 21, 2024
1f1fac1
Update tests
intitni Jan 21, 2024
854b611
Adjust prompt for retrieved content
intitni Jan 21, 2024
861e462
Update
intitni Jan 21, 2024
69442b5
Move prompt reformat to ChatGPTService
intitni Jan 21, 2024
20f5de9
Update tests
intitni Jan 21, 2024
70ceefc
Add tests for prompt reformat for Google Gemini
intitni Jan 21, 2024
a3aff20
Merge branch 'feature/fix-gemini-as-utility-model' into develop
intitni Jan 21, 2024
cf7535c
Split XcodeInspector.swift into multiple files
intitni Jan 21, 2024
c909e08
Update
intitni Jan 21, 2024
4d6e690
Add restart method to XcodeInspector
intitni Jan 21, 2024
7b18d0f
Make appElement a computed property
intitni Jan 21, 2024
0c56c85
Restart observations when XcodeInspector restarts
intitni Jan 21, 2024
28ead25
Update
intitni Jan 21, 2024
1aa0803
Add Active Source Editor to menu bar app
intitni Jan 21, 2024
0a2d371
Bump version 0.30.0
intitni Jan 21, 2024
910a84b
Fix retrieved content number in rag when context window is not big en…
intitni Jan 21, 2024
6706199
Add tests
intitni Jan 21, 2024
d339f97
Add more information to Xcode Inspector debug menu
intitni Jan 22, 2024
1ad6a58
Update appcast.xml
intitni Jan 22, 2024
381005f
Merge branch 'release/0.30.0'
intitni Jan 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Split XcodeInspector.swift into multiple files
  • Loading branch information
intitni committed Jan 21, 2024
commit cf7535cb7cbba136368c4801e7e764e0ee83e2e9
16 changes: 16 additions & 0 deletions Tool/Sources/XcodeInspector/AppInstanceInspector.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import AppKit
import Foundation

public class AppInstanceInspector: ObservableObject {
public let appElement: AXUIElement
public let runningApplication: NSRunningApplication
public var isActive: Bool { runningApplication.isActive }
public var isXcode: Bool { runningApplication.isXcode }
public var isExtensionService: Bool { runningApplication.isCopilotForXcodeExtensionService }

init(runningApplication: NSRunningApplication) {
self.runningApplication = runningApplication
appElement = AXUIElementCreateApplication(runningApplication.processIdentifier)
}
}

305 changes: 305 additions & 0 deletions Tool/Sources/XcodeInspector/Apps/XcodeAppInstanceInspector.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
import AppKit
import AXExtension
import AXNotificationStream
import Combine
import Foundation

public final class XcodeAppInstanceInspector: AppInstanceInspector {
@Published public var focusedWindow: XcodeWindowInspector?
@Published public var documentURL: URL? = nil
@Published public var workspaceURL: URL? = nil
@Published public var projectRootURL: URL? = nil
@Published public var workspaces = [WorkspaceIdentifier: Workspace]()
public var realtimeWorkspaces: [WorkspaceIdentifier: WorkspaceInfo] {
updateWorkspaceInfo()
return workspaces.mapValues(\.info)
}

@Published public private(set) var completionPanel: AXUIElement?

public var realtimeDocumentURL: URL? {
guard let window = appElement.focusedWindow,
window.identifier == "Xcode.WorkspaceWindow"
else { return nil }

return WorkspaceXcodeWindowInspector.extractDocumentURL(windowElement: window)
}

public var realtimeWorkspaceURL: URL? {
guard let window = appElement.focusedWindow,
window.identifier == "Xcode.WorkspaceWindow"
else { return nil }

return WorkspaceXcodeWindowInspector.extractWorkspaceURL(windowElement: window)
}

public var realtimeProjectURL: URL? {
let workspaceURL = realtimeWorkspaceURL
let documentURL = realtimeDocumentURL
return WorkspaceXcodeWindowInspector.extractProjectURL(
workspaceURL: workspaceURL,
documentURL: documentURL
)
}

var _version: String?
public var version: String? {
if let _version { return _version }
guard let plistPath = runningApplication.bundleURL?
.appendingPathComponent("Contents")
.appendingPathComponent("version.plist")
.path
else { return nil }
guard let plistData = FileManager.default.contents(atPath: plistPath) else { return nil }
var format = PropertyListSerialization.PropertyListFormat.xml
guard let plistDict = try? PropertyListSerialization.propertyList(
from: plistData,
options: .mutableContainersAndLeaves,
format: &format
) as? [String: AnyObject] else { return nil }
let result = plistDict["CFBundleShortVersionString"] as? String
_version = result
return result
}

private var longRunningTasks = Set<Task<Void, Error>>()
private var focusedWindowObservations = Set<AnyCancellable>()

deinit {
for task in longRunningTasks { task.cancel() }
}

override init(runningApplication: NSRunningApplication) {
super.init(runningApplication: runningApplication)

observeFocusedWindow()
observeAXNotifications()

Task {
try await Task.sleep(nanoseconds: 3_000_000_000)
// Sometimes the focused window may not be ready on app launch.
if !(focusedWindow is WorkspaceXcodeWindowInspector) {
observeFocusedWindow()
}
}
}

func observeFocusedWindow() {
if let window = appElement.focusedWindow {
if window.identifier == "Xcode.WorkspaceWindow" {
let window = WorkspaceXcodeWindowInspector(
app: runningApplication,
uiElement: window
)
focusedWindow = window

// should find a better solution to do this thread safe
Task { @MainActor in
focusedWindowObservations.forEach { $0.cancel() }
focusedWindowObservations.removeAll()

documentURL = window.documentURL
workspaceURL = window.workspaceURL
projectRootURL = window.projectRootURL

window.$documentURL
.filter { $0 != .init(fileURLWithPath: "/") }
.sink { [weak self] url in
self?.documentURL = url
}.store(in: &focusedWindowObservations)
window.$workspaceURL
.filter { $0 != .init(fileURLWithPath: "/") }
.sink { [weak self] url in
self?.workspaceURL = url
}.store(in: &focusedWindowObservations)
window.$projectRootURL
.filter { $0 != .init(fileURLWithPath: "/") }
.sink { [weak self] url in
self?.projectRootURL = url
}.store(in: &focusedWindowObservations)
}
} else {
let window = XcodeWindowInspector(uiElement: window)
focusedWindow = window
}
} else {
focusedWindow = nil
}
}

func refresh() {
if let focusedWindow = focusedWindow as? WorkspaceXcodeWindowInspector {
focusedWindow.refresh()
} else {
observeFocusedWindow()
}
}

func observeAXNotifications() {
longRunningTasks.forEach { $0.cancel() }
longRunningTasks = []

let focusedWindowChanged = Task {
let notification = AXNotificationStream(
app: runningApplication,
notificationNames: kAXFocusedWindowChangedNotification
)
for await _ in notification {
try Task.checkCancellation()
observeFocusedWindow()
}
}

longRunningTasks.insert(focusedWindowChanged)

updateWorkspaceInfo()
let updateTabsTask = Task { @MainActor in
let notification = AXNotificationStream(
app: runningApplication,
notificationNames: kAXFocusedUIElementChangedNotification,
kAXApplicationDeactivatedNotification
)
if #available(macOS 13.0, *) {
for await _ in notification.debounce(for: .seconds(2)) {
try Task.checkCancellation()
updateWorkspaceInfo()
}
} else {
for await _ in notification {
try Task.checkCancellation()
updateWorkspaceInfo()
}
}
}

longRunningTasks.insert(updateTabsTask)

let completionPanelTask = Task {
let stream = AXNotificationStream(
app: runningApplication,
notificationNames: kAXCreatedNotification, kAXUIElementDestroyedNotification
)

for await event in stream {
// We can only observe the creation and closing of the parent
// of the completion panel.
let isCompletionPanel = {
event.element.firstChild { element in
element.identifier == "_XC_COMPLETION_TABLE_"
} != nil
}
switch event.name {
case kAXCreatedNotification:
if isCompletionPanel() {
completionPanel = event.element
}
case kAXUIElementDestroyedNotification:
if isCompletionPanel() {
completionPanel = nil
}
default: break
}

try Task.checkCancellation()
}
}

longRunningTasks.insert(completionPanelTask)
}
}

// MARK: - Workspace Info

extension XcodeAppInstanceInspector {
public enum WorkspaceIdentifier: Hashable {
case url(URL)
case unknown
}

public class Workspace {
public let element: AXUIElement
public var info: WorkspaceInfo

/// When a window is closed, all it's properties will be set to nil.
/// Since we can't get notification for window closing,
/// we will use it to check if the window is closed.
var isValid: Bool {
element.parent != nil
}

init(element: AXUIElement) {
self.element = element
info = .init(tabs: [])
}
}

public struct WorkspaceInfo {
public let tabs: Set<String>

public func combined(with info: WorkspaceInfo) -> WorkspaceInfo {
return .init(tabs: tabs.union(info.tabs))
}
}

func updateWorkspaceInfo() {
let workspaceInfoInVisibleSpace = Self.fetchVisibleWorkspaces(runningApplication)
workspaces = Self.updateWorkspace(workspaces, with: workspaceInfoInVisibleSpace)
}

/// Use the project path as the workspace identifier.
static func workspaceIdentifier(_ window: AXUIElement) -> WorkspaceIdentifier {
if let url = WorkspaceXcodeWindowInspector.extractWorkspaceURL(windowElement: window) {
return WorkspaceIdentifier.url(url)
}
return WorkspaceIdentifier.unknown
}

/// With Accessibility API, we can ONLY get the information of visible windows.
static func fetchVisibleWorkspaces(
_ app: NSRunningApplication
) -> [WorkspaceIdentifier: Workspace] {
let app = AXUIElementCreateApplication(app.processIdentifier)
let windows = app.windows.filter { $0.identifier == "Xcode.WorkspaceWindow" }

var dict = [WorkspaceIdentifier: Workspace]()

for window in windows {
let workspaceIdentifier = workspaceIdentifier(window)

let tabs = {
guard let editArea = window.firstChild(where: { $0.description == "editor area" })
else { return Set<String>() }
var allTabs = Set<String>()
let tabBars = editArea.children { $0.description == "tab bar" }
for tabBar in tabBars {
let tabs = tabBar.children { $0.roleDescription == "tab" }
for tab in tabs {
allTabs.insert(tab.title)
}
}
return allTabs
}()

let workspace = Workspace(element: window)
workspace.info = .init(tabs: tabs)
dict[workspaceIdentifier] = workspace
}
return dict
}

static func updateWorkspace(
_ old: [WorkspaceIdentifier: Workspace],
with new: [WorkspaceIdentifier: Workspace]
) -> [WorkspaceIdentifier: Workspace] {
var updated = old.filter { $0.value.isValid } // remove closed windows.
for (identifier, workspace) in new {
if let existed = updated[identifier] {
existed.info = workspace.info
} else {
updated[identifier] = workspace
}
}
return updated
}
}

Loading