Skip to content

Commit 54ffb03

Browse files
committed
Add PseudoCommandHandler
1 parent 874ae39 commit 54ffb03

File tree

5 files changed

+221
-76
lines changed

5 files changed

+221
-76
lines changed

Core/Sources/Service/RealtimeSuggestionController.swift

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -188,12 +188,9 @@ public class RealtimeSuggestionController {
188188
os_log(.info, "Completion panel is open, blocked.")
189189
return
190190
}
191-
192-
do {
193-
try await Environment.triggerAction("Prefetch Suggestions")
194-
} catch {
195-
os_log(.info, "%@", error.localizedDescription)
196-
}
191+
192+
// So the editor won't be blocked (after information are cached)!
193+
await PseudoCommandHandler().generateRealtimeSuggestions()
197194
}
198195
}
199196

@@ -236,13 +233,27 @@ extension AXUIElement {
236233
var identifier: String {
237234
(try? copyValue(key: kAXIdentifierAttribute)) ?? ""
238235
}
236+
237+
var value: String {
238+
(try? copyValue(key: kAXValueAttribute)) ?? ""
239+
}
240+
241+
var focusedElement: AXUIElement? {
242+
try? copyValue(key: kAXFocusedUIElementAttribute)
243+
}
239244

240245
var description: String {
241246
(try? copyValue(key: kAXDescriptionAttribute)) ?? ""
242247
}
243248

244-
var focusedElement: AXUIElement? {
245-
try? copyValue(key: kAXFocusedUIElementAttribute)
249+
var selectedTextRange: Range<Int>? {
250+
guard let value: AXValue = try? copyValue(key: kAXSelectedTextRangeAttribute)
251+
else { return nil }
252+
var range: CFRange = .init(location: 0, length: 0)
253+
if AXValueGetValue(value, .cfRange, &range) {
254+
return Range(.init(location: range.location, length: range.length))
255+
}
256+
return nil
246257
}
247258

248259
var sharedFocusElements: [AXUIElement] {

Core/Sources/Service/SuggestionCommandHandler/CommentBaseCommandHandler.swift

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,7 @@ struct CommentBaseCommandHandler: SuggestionCommandHandler {
1414
.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
1515
try await workspace.generateSuggestions(
1616
forFileAt: fileURL,
17-
content: editor.content,
18-
lines: editor.lines,
19-
cursorPosition: editor.cursorPosition,
20-
tabSize: editor.tabSize,
21-
indentSize: editor.indentSize,
22-
usesTabsForIndentation: editor.usesTabsForIndentation
17+
editor: editor
2318
)
2419

2520
let presenter = PresentInCommentSuggestionPresenter()
@@ -36,11 +31,7 @@ struct CommentBaseCommandHandler: SuggestionCommandHandler {
3631
let fileURL = try await Environment.fetchCurrentFileURL()
3732
let (workspace, filespace) = try await Workspace
3833
.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
39-
workspace.selectNextSuggestion(
40-
forFileAt: fileURL,
41-
content: editor.content,
42-
lines: editor.lines
43-
)
34+
workspace.selectNextSuggestion(forFileAt: fileURL)
4435

4536
let presenter = PresentInCommentSuggestionPresenter()
4637
return try await presenter.presentSuggestion(
@@ -56,11 +47,7 @@ struct CommentBaseCommandHandler: SuggestionCommandHandler {
5647
let fileURL = try await Environment.fetchCurrentFileURL()
5748
let (workspace, filespace) = try await Workspace
5849
.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
59-
workspace.selectPreviousSuggestion(
60-
forFileAt: fileURL,
61-
content: editor.content,
62-
lines: editor.lines
63-
)
50+
workspace.selectPreviousSuggestion(forFileAt: fileURL)
6451

6552
let presenter = PresentInCommentSuggestionPresenter()
6653
return try await presenter.presentSuggestion(
@@ -76,7 +63,7 @@ struct CommentBaseCommandHandler: SuggestionCommandHandler {
7663
let fileURL = try await Environment.fetchCurrentFileURL()
7764
let (workspace, filespace) = try await Workspace
7865
.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
79-
workspace.rejectSuggestion(forFileAt: fileURL)
66+
workspace.rejectSuggestion(forFileAt: fileURL, editor: editor)
8067

8168
let presenter = PresentInCommentSuggestionPresenter()
8269
return try await presenter.discardSuggestion(
@@ -92,7 +79,10 @@ struct CommentBaseCommandHandler: SuggestionCommandHandler {
9279
let fileURL = try await Environment.fetchCurrentFileURL()
9380
let (workspace, _) = try await Workspace.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
9481

95-
guard let acceptedSuggestion = workspace.acceptSuggestion(forFileAt: fileURL)
82+
guard let acceptedSuggestion = workspace.acceptSuggestion(
83+
forFileAt: fileURL,
84+
editor: editor
85+
)
9686
else { return nil }
9787

9888
let injector = SuggestionInjector()
@@ -168,12 +158,7 @@ struct CommentBaseCommandHandler: SuggestionCommandHandler {
168158

169159
let suggestions = try await workspace.generateSuggestions(
170160
forFileAt: fileURL,
171-
content: editor.content,
172-
lines: editor.lines,
173-
cursorPosition: editor.cursorPosition,
174-
tabSize: editor.tabSize,
175-
indentSize: editor.indentSize,
176-
usesTabsForIndentation: editor.usesTabsForIndentation
161+
editor: editor
177162
)
178163

179164
try Task.checkCancellation()
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import ActiveApplicationMonitor
2+
import AppKit
3+
import CopilotModel
4+
import Environment
5+
import SuggestionInjector
6+
import XPCShared
7+
8+
/// It's used to run some commands without really triggering the menu bar item.
9+
///
10+
/// For example, we can use it to generate real-time suggestions without Apple Scripts.
11+
struct PseudoCommandHandler {
12+
func presentPreviousSuggestion() async {
13+
let handler = WindowBaseCommandHandler()
14+
_ = try? await handler.presentPreviousSuggestion(editor: .init(
15+
content: "",
16+
lines: [],
17+
uti: "",
18+
cursorPosition: .outOfScope,
19+
tabSize: 0,
20+
indentSize: 0,
21+
usesTabsForIndentation: false
22+
))
23+
}
24+
25+
func presentNextSuggestion() async {
26+
let handler = WindowBaseCommandHandler()
27+
_ = try? await handler.presentNextSuggestion(editor: .init(
28+
content: "",
29+
lines: [],
30+
uti: "",
31+
cursorPosition: .outOfScope,
32+
tabSize: 0,
33+
indentSize: 0,
34+
usesTabsForIndentation: false
35+
))
36+
}
37+
38+
func generateRealtimeSuggestions() async {
39+
guard let editor = await getEditorContent() else {
40+
try? await Environment.triggerAction("Prefetch Suggestions")
41+
return
42+
}
43+
let mode = PresentationMode(
44+
rawValue: UserDefaults.shared
45+
.integer(forKey: SettingsKey.suggestionPresentationMode)
46+
) ?? .comment
47+
let handler: SuggestionCommandHandler = {
48+
switch mode {
49+
case .comment:
50+
return CommentBaseCommandHandler()
51+
case .floatingWidget:
52+
return WindowBaseCommandHandler()
53+
}
54+
}()
55+
_ = try? await handler.generateRealtimeSuggestions(editor: editor)
56+
}
57+
58+
func rejectSuggestions() async {
59+
let handler = WindowBaseCommandHandler()
60+
_ = try? await handler.rejectSuggestion(editor: .init(
61+
content: "",
62+
lines: [],
63+
uti: "",
64+
cursorPosition: .outOfScope,
65+
tabSize: 0,
66+
indentSize: 0,
67+
usesTabsForIndentation: false
68+
))
69+
}
70+
}
71+
72+
private extension PseudoCommandHandler {
73+
func getFileContent() async -> (String, [String], CursorPosition)? {
74+
guard let xcode = ActiveApplicationMonitor.activeXcode else { return nil }
75+
let application = AXUIElementCreateApplication(xcode.processIdentifier)
76+
guard let focusElement = application.focusedElement,
77+
focusElement.description == "Source Editor"
78+
else { return nil }
79+
guard let selectionRange = focusElement.selectedTextRange else { return nil }
80+
let content = focusElement.value
81+
let split = content.breakLines()
82+
let selectedPosition = selectionRange.upperBound
83+
// find row and col from content at selected position
84+
var rowIndex = 0
85+
var count = 0
86+
var colIndex = 0
87+
for (i, row) in split.enumerated() {
88+
if count + row.count > selectedPosition {
89+
rowIndex = i
90+
colIndex = selectedPosition - count
91+
break
92+
}
93+
count += row.count
94+
}
95+
return (content, split, CursorPosition(line: rowIndex, character: colIndex))
96+
}
97+
98+
func getFileURL() async -> URL? {
99+
try? await Environment.fetchCurrentFileURL()
100+
}
101+
102+
@ServiceActor
103+
func getFilespace() async -> Filespace? {
104+
guard let fileURL = await getFileURL() else { return nil }
105+
for (_, workspace) in workspaces {
106+
if let space = workspace.filespaces[fileURL] { return space }
107+
}
108+
return nil
109+
}
110+
111+
@ServiceActor
112+
func getEditorContent() async -> EditorContent? {
113+
guard
114+
let filespace = await getFilespace(),
115+
let uti = filespace.uti,
116+
let tabSize = filespace.tabSize,
117+
let indentSize = filespace.indentSize,
118+
let usesTabsForIndentation = filespace.usesTabsForIndentation,
119+
let content = await getFileContent()
120+
else { return nil }
121+
return .init(
122+
content: content.0,
123+
lines: content.1,
124+
uti: uti,
125+
cursorPosition: content.2,
126+
tabSize: tabSize,
127+
indentSize: indentSize,
128+
usesTabsForIndentation: usesTabsForIndentation
129+
)
130+
}
131+
}
132+
133+
public extension String {
134+
/// Break a string into lines.
135+
func breakLines() -> [String] {
136+
let lines = split(separator: "\n", omittingEmptySubsequences: false)
137+
var all = [String]()
138+
for (index, line) in lines.enumerated() {
139+
if index == lines.endIndex - 1 {
140+
all.append(String(line))
141+
} else {
142+
all.append(String(line) + "\n")
143+
}
144+
}
145+
return all
146+
}
147+
}

Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,7 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler {
4848

4949
try await workspace.generateSuggestions(
5050
forFileAt: fileURL,
51-
content: editor.content,
52-
lines: editor.lines,
53-
cursorPosition: editor.cursorPosition,
54-
tabSize: editor.tabSize,
55-
indentSize: editor.indentSize,
56-
usesTabsForIndentation: editor.usesTabsForIndentation
51+
editor: editor
5752
)
5853

5954
if let suggestion = filespace.presentingSuggestion {
@@ -82,11 +77,7 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler {
8277
let fileURL = try await Environment.fetchCurrentFileURL()
8378
let (workspace, filespace) = try await Workspace
8479
.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
85-
workspace.selectNextSuggestion(
86-
forFileAt: fileURL,
87-
content: editor.content,
88-
lines: editor.lines
89-
)
80+
workspace.selectNextSuggestion(forFileAt: fileURL)
9081

9182
if let suggestion = filespace.presentingSuggestion {
9283
presenter.presentSuggestion(
@@ -114,11 +105,7 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler {
114105
let fileURL = try await Environment.fetchCurrentFileURL()
115106
let (workspace, filespace) = try await Workspace
116107
.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
117-
workspace.selectPreviousSuggestion(
118-
forFileAt: fileURL,
119-
content: editor.content,
120-
lines: editor.lines
121-
)
108+
workspace.selectPreviousSuggestion(forFileAt: fileURL)
122109

123110
if let suggestion = filespace.presentingSuggestion {
124111
presenter.presentSuggestion(
@@ -145,7 +132,7 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler {
145132
defer { presenter.markAsProcessing(false) }
146133
let fileURL = try await Environment.fetchCurrentFileURL()
147134
let (workspace, _) = try await Workspace.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
148-
workspace.rejectSuggestion(forFileAt: fileURL)
135+
workspace.rejectSuggestion(forFileAt: fileURL, editor: editor)
149136
presenter.discardSuggestion(fileURL: fileURL)
150137
}
151138

0 commit comments

Comments
 (0)