Skip to content

Commit fe5aa78

Browse files
committed
Merge branch 'feature/realtime-suggetion-prefetch-tweak' into develop
2 parents 87898c2 + 36fbbbc commit fe5aa78

9 files changed

Lines changed: 114 additions & 8 deletions

File tree

Copilot for Xcode.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
C8009BFF2941C551007AA7E8 /* TurnOnRealtimeSuggestionsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8009BFE2941C551007AA7E8 /* TurnOnRealtimeSuggestionsCommand.swift */; };
1111
C8009C012941C56C007AA7E8 /* TurnOffRealtimeSuggestionsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8009C002941C56C007AA7E8 /* TurnOffRealtimeSuggestionsCommand.swift */; };
1212
C8009C032941C576007AA7E8 /* RealtimeSuggestionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8009C022941C576007AA7E8 /* RealtimeSuggestionCommand.swift */; };
13+
C800DBB1294C624D00B04CAC /* PrefetchSuggestionsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C800DBB0294C624D00B04CAC /* PrefetchSuggestionsCommand.swift */; };
1314
C814588F2939EFDC00135263 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C814588E2939EFDC00135263 /* Cocoa.framework */; };
1415
C81458942939EFDC00135263 /* SourceEditorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81458932939EFDC00135263 /* SourceEditorExtension.swift */; };
1516
C81458962939EFDC00135263 /* GetSuggestionsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81458952939EFDC00135263 /* GetSuggestionsCommand.swift */; };
@@ -129,6 +130,7 @@
129130
C8009BFE2941C551007AA7E8 /* TurnOnRealtimeSuggestionsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TurnOnRealtimeSuggestionsCommand.swift; sourceTree = "<group>"; };
130131
C8009C002941C56C007AA7E8 /* TurnOffRealtimeSuggestionsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TurnOffRealtimeSuggestionsCommand.swift; sourceTree = "<group>"; };
131132
C8009C022941C576007AA7E8 /* RealtimeSuggestionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealtimeSuggestionCommand.swift; sourceTree = "<group>"; };
133+
C800DBB0294C624D00B04CAC /* PrefetchSuggestionsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefetchSuggestionsCommand.swift; sourceTree = "<group>"; };
132134
C814588C2939EFDC00135263 /* Copilot.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Copilot.appex; sourceTree = BUILT_PRODUCTS_DIR; };
133135
C814588E2939EFDC00135263 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
134136
C81458902939EFDC00135263 /* XcodeKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XcodeKit.framework; path = Library/Frameworks/XcodeKit.framework; sourceTree = DEVELOPER_DIR; };
@@ -165,6 +167,7 @@
165167
C87B03B3293B393100C77EAE /* ServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceDelegate.swift; sourceTree = "<group>"; };
166168
C87F3E5F293DC600008523E8 /* Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = "<group>"; };
167169
C87F3E61293DD004008523E8 /* Styles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Styles.swift; sourceTree = "<group>"; };
170+
C887BC832965D96000931567 /* DEVELOPMENT.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = DEVELOPMENT.md; sourceTree = "<group>"; };
168171
/* End PBXFileReference section */
169172

170173
/* Begin PBXFrameworksBuildPhase section */
@@ -219,6 +222,7 @@
219222
C8009BFE2941C551007AA7E8 /* TurnOnRealtimeSuggestionsCommand.swift */,
220223
C8009C002941C56C007AA7E8 /* TurnOffRealtimeSuggestionsCommand.swift */,
221224
C8009C022941C576007AA7E8 /* RealtimeSuggestionCommand.swift */,
225+
C800DBB0294C624D00B04CAC /* PrefetchSuggestionsCommand.swift */,
222226
C81458972939EFDC00135263 /* Info.plist */,
223227
C81458982939EFDC00135263 /* EditorExtension.entitlements */,
224228
);
@@ -241,6 +245,7 @@
241245
children = (
242246
C832A47B2940C71D000989F2 /* copilot */,
243247
C8520308293D805800460097 /* README.md */,
248+
C887BC832965D96000931567 /* DEVELOPMENT.md */,
244249
C81458AD293A009600135263 /* Config.xcconfig */,
245250
C81458AE293A009800135263 /* Config.debug.xcconfig */,
246251
C8189B282938979000C9DCDA /* Core */,
@@ -434,6 +439,7 @@
434439
C87B03AB293B262E00C77EAE /* PreviousSuggestionCommand.swift in Sources */,
435440
C87B03A7293B261900C77EAE /* RejectSuggestionCommand.swift in Sources */,
436441
C8009C032941C576007AA7E8 /* RealtimeSuggestionCommand.swift in Sources */,
442+
C800DBB1294C624D00B04CAC /* PrefetchSuggestionsCommand.swift in Sources */,
437443
C81458962939EFDC00135263 /* GetSuggestionsCommand.swift in Sources */,
438444
);
439445
runOnlyForDeploymentPostprocessing = 0;

Core/Sources/Client/AsyncXPCService.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,15 @@ public struct AsyncXPCService {
140140
}
141141
}
142142
}
143+
144+
public func prefetchRealtimeSuggestions(editorContent: EditorContent) async {
145+
guard let data = try? JSONEncoder().encode(editorContent) else { return }
146+
try? await withXPCServiceConnected(connection: connection) { service, continuation in
147+
service.prefetchRealtimeSuggestions(editorContent: data) {
148+
continuation.resume(())
149+
}
150+
}
151+
}
143152
}
144153

145154
struct AutoFinishContinuation<T> {

Core/Sources/Service/AutoTrigger.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ actor AutoTrigger {
2525

2626
func start(by listener: ObjectIdentifier) {
2727
listeners.insert(listener)
28+
2829
if task == nil {
2930
task = Task { [stream = eventObserver.stream] in
3031
var triggerTask: Task<Void, Error>?
31-
try? await Environment.triggerAction("Real-time Suggestions")
32+
try? await Environment.triggerAction("Prefetch Suggestions")
3233
for await _ in stream {
3334
triggerTask?.cancel()
3435
if Task.isCancelled { break }
@@ -43,15 +44,15 @@ actor AutoTrigger {
4344
}
4445

4546
triggerTask = Task { @ServiceActor in
46-
try? await Task.sleep(nanoseconds: 3_000_000_000)
47+
try? await Task.sleep(nanoseconds: 2_000_000_000)
4748
if Task.isCancelled { return }
4849
let fileURL = try? await Environment.fetchCurrentFileURL()
4950
guard let folderURL = try? await Environment.fetchCurrentProjectRootURL(fileURL),
5051
let workspace = workspaces[folderURL],
5152
workspace.isRealtimeSuggestionEnabled
5253
else { return }
5354
if Task.isCancelled { return }
54-
try? await Environment.triggerAction("Real-time Suggestions")
55+
try? await Environment.triggerAction("Prefetch Suggestions")
5556
}
5657
}
5758
}

Core/Sources/Service/XPCService.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,4 +258,30 @@ public class XPCService: NSObject, XPCServiceProtocol {
258258
reply(nil)
259259
}
260260
}
261+
262+
public func prefetchRealtimeSuggestions(
263+
editorContent: Data,
264+
withReply reply: @escaping () -> Void
265+
) {
266+
Task { @ServiceActor in
267+
do {
268+
let editor = try JSONDecoder().decode(EditorContent.self, from: editorContent)
269+
let fileURL = try await Environment.fetchCurrentFileURL()
270+
let workspace = try await fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
271+
_ = workspace.getRealtimeSuggestedCode(
272+
forFileAt: fileURL,
273+
content: editor.content,
274+
lines: editor.lines,
275+
cursorPosition: editor.cursorPosition,
276+
tabSize: editor.tabSize,
277+
indentSize: editor.indentSize,
278+
usesTabsForIndentation: editor.usesTabsForIndentation
279+
)
280+
reply()
281+
} catch {
282+
print(error)
283+
reply()
284+
}
285+
}
286+
}
261287
}

Core/Sources/XPCShared/XPCServiceProtocol.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,9 @@ public protocol XPCServiceProtocol {
3535
)
3636

3737
func setAutoSuggestion(enabled: Bool, withReply reply: @escaping (Error?) -> Void)
38+
39+
func prefetchRealtimeSuggestions(
40+
editorContent: Data,
41+
withReply reply: @escaping () -> Void
42+
)
3843
}

DEVELOPMENT.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Development
2+
3+
## Targets
4+
5+
### Copilot for Xcode
6+
7+
Copilot for Xcode is the app containing both the XPCService and the editor extension.
8+
9+
### EditorExtension
10+
11+
As its name suggests, the editor extension. Since an editor extension must be sandboxed, it will need to talk to a trusted non-sandboxed XPCService to break out the limitations. The identifier of the XPCService must be listed under `com.apple.security.temporary-exception.mach-lookup.global-name` in entitlements.
12+
13+
### XPCService
14+
15+
The XPCService is a program that runs in the background and does basically everything. It redirects the requests from EditorExtension to `CopilotService` and returns the updated code back to the extension.
16+
17+
Since the Xcode source editor extension only allows its commands to be triggered manually, the XPCService has to use Apple Scripts to trigger the menu items to generate real-time suggestions.
18+
19+
The XPCService is also using a lot of Apple Script tricks to get the file paths and project/workspace paths of the active Xcode window because Xcode is not providing this information.
20+
21+
## Building and Archiving the App
22+
23+
This project contains a Git submodule `copilot.vim`, so you will have to initialize the submodule or download it from [copilot.vim](https://github.com/github/copilot.vim).
24+
25+
Then archive the target Copilot for Xcode.
26+
27+
## Testing Extension
28+
29+
### Testing Real-time Suggestions Commands
30+
31+
Testing Real-time Suggestions is a little bit different because the Apple Script can't find the commands when debugging the extension in Xcode. Instead, you will have to archive the debug version of the app, run the XPCService target simultaneously and use them against each other.
32+
33+
### Testing Other Commands
34+
35+
Just run both the XPCService and the EditorExtension Target.
36+
37+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import Client
2+
import CopilotModel
3+
import Foundation
4+
import XcodeKit
5+
6+
class PrefetchSuggestionsCommand: NSObject, XCSourceEditorCommand, CommandType {
7+
var name: String { "Prefetch Suggestions" }
8+
9+
func perform(
10+
with invocation: XCSourceEditorCommandInvocation,
11+
completionHandler: @escaping (Error?) -> Void
12+
) {
13+
completionHandler(nil)
14+
15+
Task {
16+
let service = try getService()
17+
await service.prefetchRealtimeSuggestions(editorContent: .init(invocation))
18+
}
19+
}
20+
}

EditorExtension/SourceEditorExtension.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class SourceEditorExtension: NSObject, XCSourceEditorExtension {
1212
TurnOnRealtimeSuggestionsCommand(),
1313
TurnOffRealtimeSuggestionsCommand(),
1414
RealtimeSuggestionsCommand(),
15+
PrefetchSuggestionsCommand(),
1516
].map(makeCommandDefinition)
1617
}
1718
}

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ Thanks to [LSP-copilot](https://github.com/TerminalFi/LSP-copilot) for showing t
2727
5. After signing in, go back to the app and click "Confirm Sign-in" to finish.
2828
5. Enable the extension in the Settings.app, then maybe restart Xcode.
2929

30-
The first time the actions run, the extension will ask for 2 types of permissions:
30+
The first time the commands run, the extension will ask for 2 types of permissions:
3131
1. Accessibility API: which the extension uses to get the editing file path.
3232
2. Folder Access: the extension needs, to run some Apple Scripts to get the project/workspace path.
3333

34-
## Actions
34+
## Commands
3535

3636
- Get Suggestions: Get suggestions for the editing file at the current cursor position.
3737
- Next Suggestion: If there is more than 1 suggestion, switch to the next one.
@@ -40,19 +40,20 @@ The first time the actions run, the extension will ask for 2 types of permission
4040
- Reject Suggestion: Remove the suggestion comments.
4141
- Turn On Real-time Suggestions: When turn on, Copilot will auto-insert suggestion comments to your code while editing. You have to manually turn it on for every open window of Xcode.
4242
- Turn Off Real-time Suggestions: Turns the real-time suggestions off.
43-
- Real-time Suggestions: It is an entry point only for Copilot for Xcode. In the background, Copilot for Xcode will occasionally run this action to bring you real-time suggestions.
43+
- Real-time Suggestions: It is an entry point only for Copilot for Xcode. When suggestions are successfully fetched, Copilot for Xcode will run this command to present the suggestions.
44+
- Prefetch Suggestions: It is an entry point only for Copilot for Xcode. In the background, Copilot for Xcode will occasionally run this command to prefetch real-time suggestions.
4445

4546
**About real-time suggestions**
4647

4748
The implementation won't feel as smooth as that of VSCode.
4849

49-
The magic behind it is that it will keep calling the action from the menu when you are not typing, or clicking mouse. So it will have to listen to those events, I am not sure if people like it.
50+
The magic behind it is that it will keep calling the command from the menu when you are not typing, or clicking mouse. So it will have to listen to those events, I am not sure if people like it.
5051

5152
Hope that next year, Apple can spend some time on Xcode Extensions.
5253

5354
## Prevent Suggestions Being Committed
5455

55-
Since the suggestions are presented as comments, they are in your code. If you are not careful enough, they can be committed to your git repo. To avoid that, I would recommend adding a pre-commit git hook to prevent this from happening. Maybe later I will add an action for that.
56+
Since the suggestions are presented as comments, they are in your code. If you are not careful enough, they can be committed to your git repo. To avoid that, I would recommend adding a pre-commit git hook to prevent this from happening. Maybe later I will add an command for that.
5657

5758
```sh
5859
#!/bin/sh

0 commit comments

Comments
 (0)