Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions Copilot for Xcode.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
C8009BFF2941C551007AA7E8 /* TurnOnRealtimeSuggestionsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8009BFE2941C551007AA7E8 /* TurnOnRealtimeSuggestionsCommand.swift */; };
C8009C012941C56C007AA7E8 /* TurnOffRealtimeSuggestionsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8009C002941C56C007AA7E8 /* TurnOffRealtimeSuggestionsCommand.swift */; };
C8009C032941C576007AA7E8 /* RealtimeSuggestionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8009C022941C576007AA7E8 /* RealtimeSuggestionCommand.swift */; };
C800DBB1294C624D00B04CAC /* PrefetchSuggestionsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C800DBB0294C624D00B04CAC /* PrefetchSuggestionsCommand.swift */; };
C814588F2939EFDC00135263 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C814588E2939EFDC00135263 /* Cocoa.framework */; };
C81458942939EFDC00135263 /* SourceEditorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81458932939EFDC00135263 /* SourceEditorExtension.swift */; };
C81458962939EFDC00135263 /* GetSuggestionsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81458952939EFDC00135263 /* GetSuggestionsCommand.swift */; };
Expand Down Expand Up @@ -129,6 +130,7 @@
C8009BFE2941C551007AA7E8 /* TurnOnRealtimeSuggestionsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TurnOnRealtimeSuggestionsCommand.swift; sourceTree = "<group>"; };
C8009C002941C56C007AA7E8 /* TurnOffRealtimeSuggestionsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TurnOffRealtimeSuggestionsCommand.swift; sourceTree = "<group>"; };
C8009C022941C576007AA7E8 /* RealtimeSuggestionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealtimeSuggestionCommand.swift; sourceTree = "<group>"; };
C800DBB0294C624D00B04CAC /* PrefetchSuggestionsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefetchSuggestionsCommand.swift; sourceTree = "<group>"; };
C814588C2939EFDC00135263 /* Copilot.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Copilot.appex; sourceTree = BUILT_PRODUCTS_DIR; };
C814588E2939EFDC00135263 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
C81458902939EFDC00135263 /* XcodeKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XcodeKit.framework; path = Library/Frameworks/XcodeKit.framework; sourceTree = DEVELOPER_DIR; };
Expand Down Expand Up @@ -165,6 +167,7 @@
C87B03B3293B393100C77EAE /* ServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceDelegate.swift; sourceTree = "<group>"; };
C87F3E5F293DC600008523E8 /* Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = "<group>"; };
C87F3E61293DD004008523E8 /* Styles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Styles.swift; sourceTree = "<group>"; };
C887BC832965D96000931567 /* DEVELOPMENT.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = DEVELOPMENT.md; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -219,6 +222,7 @@
C8009BFE2941C551007AA7E8 /* TurnOnRealtimeSuggestionsCommand.swift */,
C8009C002941C56C007AA7E8 /* TurnOffRealtimeSuggestionsCommand.swift */,
C8009C022941C576007AA7E8 /* RealtimeSuggestionCommand.swift */,
C800DBB0294C624D00B04CAC /* PrefetchSuggestionsCommand.swift */,
C81458972939EFDC00135263 /* Info.plist */,
C81458982939EFDC00135263 /* EditorExtension.entitlements */,
);
Expand All @@ -241,6 +245,7 @@
children = (
C832A47B2940C71D000989F2 /* copilot */,
C8520308293D805800460097 /* README.md */,
C887BC832965D96000931567 /* DEVELOPMENT.md */,
C81458AD293A009600135263 /* Config.xcconfig */,
C81458AE293A009800135263 /* Config.debug.xcconfig */,
C8189B282938979000C9DCDA /* Core */,
Expand Down Expand Up @@ -434,6 +439,7 @@
C87B03AB293B262E00C77EAE /* PreviousSuggestionCommand.swift in Sources */,
C87B03A7293B261900C77EAE /* RejectSuggestionCommand.swift in Sources */,
C8009C032941C576007AA7E8 /* RealtimeSuggestionCommand.swift in Sources */,
C800DBB1294C624D00B04CAC /* PrefetchSuggestionsCommand.swift in Sources */,
C81458962939EFDC00135263 /* GetSuggestionsCommand.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
9 changes: 9 additions & 0 deletions Core/Sources/Client/AsyncXPCService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,15 @@ public struct AsyncXPCService {
}
}
}

public func prefetchRealtimeSuggestions(editorContent: EditorContent) async {
guard let data = try? JSONEncoder().encode(editorContent) else { return }
try? await withXPCServiceConnected(connection: connection) { service, continuation in
service.prefetchRealtimeSuggestions(editorContent: data) {
continuation.resume(())
}
}
}
}

struct AutoFinishContinuation<T> {
Expand Down
7 changes: 4 additions & 3 deletions Core/Sources/Service/AutoTrigger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ actor AutoTrigger {

func start(by listener: ObjectIdentifier) {
listeners.insert(listener)

if task == nil {
task = Task { [stream = eventObserver.stream] in
var triggerTask: Task<Void, Error>?
try? await Environment.triggerAction("Real-time Suggestions")
try? await Environment.triggerAction("Prefetch Suggestions")
for await _ in stream {
triggerTask?.cancel()
if Task.isCancelled { break }
Expand All @@ -43,15 +44,15 @@ actor AutoTrigger {
}

triggerTask = Task { @ServiceActor in
try? await Task.sleep(nanoseconds: 3_000_000_000)
try? await Task.sleep(nanoseconds: 2_000_000_000)
if Task.isCancelled { return }
let fileURL = try? await Environment.fetchCurrentFileURL()
guard let folderURL = try? await Environment.fetchCurrentProjectRootURL(fileURL),
let workspace = workspaces[folderURL],
workspace.isRealtimeSuggestionEnabled
else { return }
if Task.isCancelled { return }
try? await Environment.triggerAction("Real-time Suggestions")
try? await Environment.triggerAction("Prefetch Suggestions")
}
}
}
Expand Down
26 changes: 26 additions & 0 deletions Core/Sources/Service/XPCService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -258,4 +258,30 @@ public class XPCService: NSObject, XPCServiceProtocol {
reply(nil)
}
}

public func prefetchRealtimeSuggestions(
editorContent: Data,
withReply reply: @escaping () -> Void
) {
Task { @ServiceActor in
do {
let editor = try JSONDecoder().decode(EditorContent.self, from: editorContent)
let fileURL = try await Environment.fetchCurrentFileURL()
let workspace = try await fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
_ = workspace.getRealtimeSuggestedCode(
forFileAt: fileURL,
content: editor.content,
lines: editor.lines,
cursorPosition: editor.cursorPosition,
tabSize: editor.tabSize,
indentSize: editor.indentSize,
usesTabsForIndentation: editor.usesTabsForIndentation
)
reply()
} catch {
print(error)
reply()
}
}
}
}
5 changes: 5 additions & 0 deletions Core/Sources/XPCShared/XPCServiceProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,9 @@ public protocol XPCServiceProtocol {
)

func setAutoSuggestion(enabled: Bool, withReply reply: @escaping (Error?) -> Void)

func prefetchRealtimeSuggestions(
editorContent: Data,
withReply reply: @escaping () -> Void
)
}
37 changes: 37 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Development

## Targets

### Copilot for Xcode

Copilot for Xcode is the app containing both the XPCService and the editor extension.

### EditorExtension

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.

### XPCService

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.

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.

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.

## Building and Archiving the App

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).

Then archive the target Copilot for Xcode.

## Testing Extension

### Testing Real-time Suggestions Commands

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.

### Testing Other Commands

Just run both the XPCService and the EditorExtension Target.


20 changes: 20 additions & 0 deletions EditorExtension/PrefetchSuggestionsCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Client
import CopilotModel
import Foundation
import XcodeKit

class PrefetchSuggestionsCommand: NSObject, XCSourceEditorCommand, CommandType {
var name: String { "Prefetch Suggestions" }

func perform(
with invocation: XCSourceEditorCommandInvocation,
completionHandler: @escaping (Error?) -> Void
) {
completionHandler(nil)

Task {
let service = try getService()
await service.prefetchRealtimeSuggestions(editorContent: .init(invocation))
}
}
}
1 change: 1 addition & 0 deletions EditorExtension/SourceEditorExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class SourceEditorExtension: NSObject, XCSourceEditorExtension {
TurnOnRealtimeSuggestionsCommand(),
TurnOffRealtimeSuggestionsCommand(),
RealtimeSuggestionsCommand(),
PrefetchSuggestionsCommand(),
].map(makeCommandDefinition)
}
}
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ Thanks to [LSP-copilot](https://github.com/TerminalFi/LSP-copilot) for showing t
5. After signing in, go back to the app and click "Confirm Sign-in" to finish.
5. Enable the extension in the Settings.app, then maybe restart Xcode.

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

## Actions
## Commands

- Get Suggestions: Get suggestions for the editing file at the current cursor position.
- Next Suggestion: If there is more than 1 suggestion, switch to the next one.
Expand All @@ -40,19 +40,20 @@ The first time the actions run, the extension will ask for 2 types of permission
- Reject Suggestion: Remove the suggestion comments.
- 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.
- Turn Off Real-time Suggestions: Turns the real-time suggestions off.
- 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.
- 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.
- 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.

**About real-time suggestions**

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

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.
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.

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

## Prevent Suggestions Being Committed

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.
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.

```sh
#!/bin/sh
Expand Down