Skip to content

Commit a17bb0d

Browse files
committed
WIP
1 parent bd805e4 commit a17bb0d

File tree

8 files changed

+539
-9
lines changed

8 files changed

+539
-9
lines changed

Core/Package.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ let package = Package(
1717
"UpdateChecker",
1818
"Logger",
1919
"UserDefaultsObserver",
20+
"XcodeInspector",
2021
]
2122
),
2223
.library(
@@ -175,7 +176,7 @@ let package = Package(
175176

176177
.target(
177178
name: "ChatService",
178-
dependencies: ["OpenAIService", "ChatPlugins", "Environment"]
179+
dependencies: ["OpenAIService", "ChatPlugins", "Environment", "XcodeInspector"]
179180
),
180181
.target(
181182
name: "ChatPlugins",
@@ -225,6 +226,15 @@ let package = Package(
225226
dependencies: ["Preferences", "GitHubCopilotService"]
226227
),
227228
.target(name: "UserDefaultsObserver"),
229+
.target(
230+
name: "XcodeInspector",
231+
dependencies: [
232+
"AXExtension",
233+
"Environment",
234+
"Logger",
235+
"AXNotificationStream"
236+
]
237+
),
228238

229239
// MARK: - GitHub Copilot
230240

Core/Sources/AXNotificationStream/AXNotificationStream.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,19 @@ public final class AXNotificationStream: AsyncSequence {
1818
deinit {
1919
continuation.finish()
2020
}
21+
22+
public convenience init(
23+
app: NSRunningApplication,
24+
element: AXUIElement? = nil,
25+
notificationNames: String...
26+
) {
27+
self.init(app: app, element: element, notificationNames: notificationNames)
28+
}
2129

2230
public init(
2331
app: NSRunningApplication,
2432
element: AXUIElement? = nil,
25-
notificationNames: String...
33+
notificationNames: [String]
2634
) {
2735
var cont: Continuation!
2836
stream = Stream { continuation in

Core/Sources/ChatService/ChatPluginController.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ final class ChatPluginController {
1717
self.plugins = all
1818
}
1919

20+
/// Handle the message in a plugin if required. Return false if no plugin handles the message.
2021
func handleContent(_ content: String) async throws -> Bool {
2122
// look for the prefix of content, see if there is something like /command.
2223
// If there is, then we need to find the plugin that can handle this command.
@@ -88,8 +89,18 @@ final class ChatPluginController {
8889
return false
8990
}
9091
}
92+
93+
func stopResponding() async {
94+
await runningPlugin?.stopResponding()
95+
}
96+
97+
func cancel() async {
98+
await runningPlugin?.cancel()
99+
}
91100
}
92101

102+
// MARK: - ChatPluginDelegate
103+
93104
extension ChatPluginController: ChatPluginDelegate {
94105
public func pluginDidStartResponding(_: ChatPlugins.ChatPlugin) {
95106
Task {

Core/Sources/ChatService/ChatService.swift

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import OpenAIService
66
public final class ChatService: ObservableObject {
77
public let chatGPTService: any ChatGPTServiceType
88
let pluginController: ChatPluginController
9-
var runningPlugin: ChatPlugin?
109
var cancellable = Set<AnyCancellable>()
1110

1211
public init<T: ChatGPTServiceType>(chatGPTService: T) {
@@ -31,16 +30,12 @@ public final class ChatService: ObservableObject {
3130
}
3231

3332
public func stopReceivingMessage() async {
34-
if let runningPlugin {
35-
await runningPlugin.stopResponding()
36-
}
33+
await pluginController.stopResponding()
3734
await chatGPTService.stopReceivingMessage()
3835
}
3936

4037
public func clearHistory() async {
41-
if let runningPlugin {
42-
await runningPlugin.cancel()
43-
}
38+
await pluginController.cancel()
4439
await chatGPTService.clearHistory()
4540
}
4641

@@ -59,4 +54,8 @@ public final class ChatService: ObservableObject {
5954
public func mutateSystemPrompt(_ newPrompt: String) async {
6055
await chatGPTService.mutateSystemPrompt(newPrompt)
6156
}
57+
58+
public func mutateHistory(_ mutator: @escaping (inout [ChatMessage]) -> Void) async {
59+
await chatGPTService.mutateHistory(mutator)
60+
}
6261
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Foundation
2+
import OpenAIService
3+
import XcodeInspector
4+
5+
final class DynamicContextController {
6+
let chatGPTService: any ChatGPTServiceType
7+
8+
init(chatGPTService: any ChatGPTServiceType) {
9+
self.chatGPTService = chatGPTService
10+
}
11+
12+
func updatePromptToMatchContent(systemPrompt: String) async throws {
13+
14+
}
15+
}
16+
17+
extension DynamicContextController {
18+
func getEditorInformation() -> Any? {
19+
guard let editor = XcodeInspector.shared.focusedEditor else { return nil }
20+
let content = editor.content
21+
22+
return nil
23+
}
24+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import AppKit
2+
import AXNotificationStream
3+
import Foundation
4+
import SuggestionModel
5+
6+
/// Representing a source editor inside Xcode.
7+
public class SourceEditor {
8+
public struct Content {
9+
/// The content of the source editor.
10+
var content: String
11+
/// The content of the source editor in lines.
12+
var lines: [String]
13+
/// The selection ranges of the source editor.
14+
var selections: [CursorRange]
15+
/// The cursor position of the source editor.
16+
var cursorPosition: CursorPosition
17+
/// Line annotations of the source editor.
18+
var lineAnnotations: [String]
19+
}
20+
21+
let runningApplication: NSRunningApplication
22+
let element: AXUIElement
23+
24+
/// The content of the source editor.
25+
public var content: Content {
26+
let content = element.value
27+
let split = Self.breakLines(content)
28+
let lineAnnotationElements = element.children { $0.identifier == "Line Annotation" }
29+
let lineAnnotations = lineAnnotationElements.map(\.label)
30+
31+
if let selectionRange = element.selectedTextRange {
32+
let range = Self.convertRangeToCursorRange(selectionRange, in: content)
33+
return .init(
34+
content: content,
35+
lines: split,
36+
selections: [range],
37+
cursorPosition: range.end,
38+
lineAnnotations: lineAnnotations
39+
)
40+
}
41+
return .init(
42+
content: content,
43+
lines: split,
44+
selections: [],
45+
cursorPosition: .outOfScope,
46+
lineAnnotations: lineAnnotations
47+
)
48+
}
49+
50+
public init(runningApplication: NSRunningApplication, element: AXUIElement) {
51+
self.runningApplication = runningApplication
52+
self.element = element
53+
}
54+
55+
/// Observe to changes in the source editor.
56+
public func observe(notificationNames: String...) -> AXNotificationStream {
57+
return AXNotificationStream(
58+
app: runningApplication,
59+
element: element,
60+
notificationNames: notificationNames
61+
)
62+
}
63+
64+
/// Observe to changes in the source editor scroll view.
65+
public func observeScrollView(notificationNames: String...) -> AXNotificationStream? {
66+
guard let scrollView = element.parent else { return nil }
67+
return AXNotificationStream(
68+
app: runningApplication,
69+
element: scrollView,
70+
notificationNames: notificationNames
71+
)
72+
}
73+
}
74+
75+
// MARK: - Helpers
76+
77+
public extension SourceEditor {
78+
static func convertCursorRangeToRange(
79+
_ cursorRange: CursorRange,
80+
in lines: [String]
81+
) -> CFRange {
82+
var countS = 0
83+
var countE = 0
84+
var range = CFRange(location: 0, length: 0)
85+
for (i, line) in lines.enumerated() {
86+
if i == cursorRange.start.line {
87+
countS = countS + cursorRange.start.character
88+
range.location = countS
89+
}
90+
if i == cursorRange.end.line {
91+
countE = countE + cursorRange.end.character
92+
range.length = max(countE - range.location, 0)
93+
break
94+
}
95+
countS += line.count
96+
countE += line.count
97+
}
98+
return range
99+
}
100+
101+
static func convertCursorRangeToRange(
102+
_ cursorRange: CursorRange,
103+
in content: String
104+
) -> CFRange {
105+
let lines = breakLines(content)
106+
return convertCursorRangeToRange(cursorRange, in: lines)
107+
}
108+
109+
static func convertRangeToCursorRange(
110+
_ range: ClosedRange<Int>,
111+
in lines: [String]
112+
) -> CursorRange {
113+
guard !lines.isEmpty else { return CursorRange(start: .zero, end: .zero) }
114+
var countS = 0
115+
var countE = 0
116+
var cursorRange = CursorRange(start: .zero, end: .outOfScope)
117+
for (i, line) in lines.enumerated() {
118+
if countS <= range.lowerBound, range.lowerBound < countS + line.count {
119+
cursorRange.start = .init(line: i, character: range.lowerBound - countS)
120+
}
121+
if countE <= range.upperBound, range.upperBound < countE + line.count {
122+
cursorRange.end = .init(line: i, character: range.upperBound - countE)
123+
break
124+
}
125+
countS += line.count
126+
countE += line.count
127+
}
128+
if cursorRange.end == .outOfScope {
129+
cursorRange.end = .init(line: lines.endIndex - 1, character: lines.last?.count ?? 0)
130+
}
131+
return cursorRange
132+
}
133+
134+
static func convertRangeToCursorRange(
135+
_ range: ClosedRange<Int>,
136+
in content: String
137+
) -> CursorRange {
138+
let lines = breakLines(content)
139+
return convertRangeToCursorRange(range, in: lines)
140+
}
141+
142+
static func breakLines(_ string: String) -> [String] {
143+
let lines = string.split(separator: "\n", omittingEmptySubsequences: false)
144+
var all = [String]()
145+
for (index, line) in lines.enumerated() {
146+
if index == lines.endIndex - 1 {
147+
all.append(String(line))
148+
} else {
149+
all.append(String(line) + "\n")
150+
}
151+
}
152+
return all
153+
}
154+
}
155+

0 commit comments

Comments
 (0)