Skip to content

Commit c7b5cbd

Browse files
committed
Merge branch 'feature/suggestion-presentation-strategy' into develop
2 parents c9df874 + 600a8da commit c7b5cbd

15 files changed

Lines changed: 548 additions & 453 deletions

Core/Sources/CopilotModel/CopilotCompletion.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Foundation
22

3-
public struct CopilotCompletion: Codable {
3+
public struct CopilotCompletion: Codable, Equatable {
44
public init(
55
text: String,
66
position: CursorPosition,

Core/Sources/Service/Helpers.swift

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,6 @@ func runAppleScript(_ appleScript: String) async throws -> String {
4040
}
4141
}
4242

43-
extension XPCService {
44-
@ServiceActor
45-
func fetchOrCreateWorkspaceIfNeeded(fileURL: URL) async throws -> Workspace {
46-
let projectURL = try await Environment.fetchCurrentProjectRootURL(fileURL)
47-
let workspaceURL = projectURL ?? fileURL
48-
let workspace = workspaces[workspaceURL] ?? Workspace(projectRootURL: workspaceURL)
49-
workspaces[workspaceURL] = workspace
50-
return workspace
51-
}
52-
}
53-
5443
extension NSError {
5544
static func from(_ error: Error) -> NSError {
5645
if let error = error as? ServerError {

Core/Sources/Service/RealtimeSuggestionController.swift

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ public actor RealtimeSuggestionController {
141141
}
142142
}
143143

144-
func cancelInFlightTasks() async {
144+
func cancelInFlightTasks(excluding: Task<Void, Never>? = nil) async {
145145
inflightPrefetchTask?.cancel()
146146

147147
// cancel in-flight tasks
@@ -153,17 +153,24 @@ public actor RealtimeSuggestionController {
153153
}
154154
group.addTask {
155155
await { @ServiceActor in
156-
inflightRealtimeSuggestionsTasks.forEach { $0.cancel() }
156+
inflightRealtimeSuggestionsTasks.forEach {
157+
if $0 == excluding { return }
158+
$0.cancel()
159+
}
157160
inflightRealtimeSuggestionsTasks.removeAll()
161+
if let excluded = excluding {
162+
inflightRealtimeSuggestionsTasks.insert(excluded)
163+
}
158164
}()
159165
}
160166
}
161167
}
162168

169+
#warning("TODO: Find a better way to prevent that from happening!")
163170
/// Prevent prefetch to be triggered by commands. Quick and dirty.
164-
func cancelInFlightTasksAndIgnoreTriggerForAWhile() async {
171+
func cancelInFlightTasksAndIgnoreTriggerForAWhile(excluding: Task<Void, Never>? = nil) async {
165172
ignoreUntil = Date(timeIntervalSinceNow: 5)
166-
await cancelInFlightTasks()
173+
await cancelInFlightTasks(excluding: excluding)
167174
}
168175
}
169176

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import CopilotModel
2+
import Foundation
3+
import SuggestionInjector
4+
import XPCShared
5+
6+
@ServiceActor
7+
struct CommentBaseCommandHandler: SuggestionCommandHanlder {
8+
nonisolated init() {}
9+
10+
func presentSuggestions(editor: EditorContent) async throws -> UpdatedContent? {
11+
let fileURL = try await Environment.fetchCurrentFileURL()
12+
let (workspace, _) = try await Workspace.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
13+
try await workspace.generateSuggestions(
14+
forFileAt: fileURL,
15+
content: editor.content,
16+
lines: editor.lines,
17+
cursorPosition: editor.cursorPosition,
18+
tabSize: editor.tabSize,
19+
indentSize: editor.indentSize,
20+
usesTabsForIndentation: editor.usesTabsForIndentation
21+
)
22+
23+
guard let filespace = workspace.filespaces[fileURL] else { return nil }
24+
let presenter = PresentInCommentSuggestionPresenter()
25+
return try await presenter.presentSuggestion(
26+
for: filespace,
27+
in: workspace,
28+
originalContent: editor.content,
29+
lines: editor.lines,
30+
cursorPosition: editor.cursorPosition
31+
)
32+
}
33+
34+
func presentNextSuggestion(editor: EditorContent) async throws -> UpdatedContent? {
35+
let fileURL = try await Environment.fetchCurrentFileURL()
36+
let (workspace, _) = try await Workspace.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
37+
workspace.selectNextSuggestion(
38+
forFileAt: fileURL,
39+
content: editor.content,
40+
lines: editor.lines
41+
)
42+
43+
guard let filespace = workspace.filespaces[fileURL] else { return nil }
44+
let presenter = PresentInCommentSuggestionPresenter()
45+
return try await presenter.presentSuggestion(
46+
for: filespace,
47+
in: workspace,
48+
originalContent: editor.content,
49+
lines: editor.lines,
50+
cursorPosition: editor.cursorPosition
51+
)
52+
}
53+
54+
func presentPreviousSuggestion(editor: EditorContent) async throws -> UpdatedContent? {
55+
let fileURL = try await Environment.fetchCurrentFileURL()
56+
let (workspace, _) = try await Workspace.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
57+
workspace.selectPreviousSuggestion(
58+
forFileAt: fileURL,
59+
content: editor.content,
60+
lines: editor.lines
61+
)
62+
63+
guard let filespace = workspace.filespaces[fileURL] else { return nil }
64+
let presenter = PresentInCommentSuggestionPresenter()
65+
return try await presenter.presentSuggestion(
66+
for: filespace,
67+
in: workspace,
68+
originalContent: editor.content,
69+
lines: editor.lines,
70+
cursorPosition: editor.cursorPosition
71+
)
72+
}
73+
74+
func rejectSuggestion(editor: EditorContent) async throws -> UpdatedContent? {
75+
let fileURL = try await Environment.fetchCurrentFileURL()
76+
let (workspace, _) = try await Workspace.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
77+
workspace.rejectSuggestion(forFileAt: fileURL)
78+
79+
guard let filespace = workspace.filespaces[fileURL] else { return nil }
80+
let presenter = PresentInCommentSuggestionPresenter()
81+
return try await presenter.discardSuggestion(
82+
for: filespace,
83+
in: workspace,
84+
originalContent: editor.content,
85+
lines: editor.lines,
86+
cursorPosition: editor.cursorPosition
87+
)
88+
}
89+
90+
func acceptSuggestion(editor: EditorContent) async throws -> UpdatedContent? {
91+
let fileURL = try await Environment.fetchCurrentFileURL()
92+
let (workspace, _) = try await Workspace.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
93+
94+
guard let acceptedSuggestion = workspace.acceptSuggestion(forFileAt: fileURL)
95+
else { return nil }
96+
97+
let injector = SuggestionInjector()
98+
var lines = editor.lines
99+
var cursorPosition = editor.cursorPosition
100+
var extraInfo = SuggestionInjector.ExtraInfo()
101+
injector.rejectCurrentSuggestions(
102+
from: &lines,
103+
cursorPosition: &cursorPosition,
104+
extraInfo: &extraInfo
105+
)
106+
injector.acceptSuggestion(
107+
intoContentWithoutSuggestion: &lines,
108+
cursorPosition: &cursorPosition,
109+
completion: acceptedSuggestion,
110+
extraInfo: &extraInfo
111+
)
112+
113+
return .init(
114+
content: String(lines.joined(separator: "")),
115+
newCursor: cursorPosition,
116+
modifications: extraInfo.modifications
117+
)
118+
}
119+
120+
func presentRealtimeSuggestions(editor: EditorContent) async throws -> UpdatedContent? {
121+
let fileURL = try await Environment.fetchCurrentFileURL()
122+
let (workspace, filespace) = try await Workspace
123+
.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
124+
125+
try Task.checkCancellation()
126+
127+
let snapshot = Filespace.Snapshot(
128+
linesHash: editor.lines.hashValue,
129+
cursorPosition: editor.cursorPosition
130+
)
131+
132+
// If the generated suggestions are for this editor content, present it.
133+
guard filespace.suggestionSourceSnapshot == snapshot else { return nil }
134+
135+
let presenter = PresentInCommentSuggestionPresenter()
136+
return try await presenter.presentSuggestion(
137+
for: filespace,
138+
in: workspace,
139+
originalContent: editor.content,
140+
lines: editor.lines,
141+
cursorPosition: editor.cursorPosition
142+
)
143+
}
144+
145+
func generateRealtimeSuggestions(editor: EditorContent) async throws -> UpdatedContent? {
146+
let fileURL = try await Environment.fetchCurrentFileURL()
147+
let (workspace, filespace) = try await Workspace
148+
.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
149+
150+
try Task.checkCancellation()
151+
152+
let snapshot = Filespace.Snapshot(
153+
linesHash: editor.lines.hashValue,
154+
cursorPosition: editor.cursorPosition
155+
)
156+
157+
// There is no need to regenerate suggestions for the same editor content.
158+
guard filespace.suggestionSourceSnapshot != snapshot else { return nil }
159+
160+
let suggestions = try await workspace.generateSuggestions(
161+
forFileAt: fileURL,
162+
content: editor.content,
163+
lines: editor.lines,
164+
cursorPosition: editor.cursorPosition,
165+
tabSize: editor.tabSize,
166+
indentSize: editor.indentSize,
167+
usesTabsForIndentation: editor.usesTabsForIndentation
168+
)
169+
170+
try Task.checkCancellation()
171+
172+
// If there is a suggestion available, call another command to present it.
173+
guard !suggestions.isEmpty else { return nil }
174+
try await Environment.triggerAction("Real-time Suggestions")
175+
176+
return nil
177+
}
178+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import CopilotModel
2+
import XPCShared
3+
4+
protocol SuggestionCommandHanlder {
5+
@ServiceActor
6+
func presentSuggestions(editor: EditorContent) async throws -> UpdatedContent?
7+
@ServiceActor
8+
func presentNextSuggestion(editor: EditorContent) async throws -> UpdatedContent?
9+
@ServiceActor
10+
func presentPreviousSuggestion(editor: EditorContent) async throws -> UpdatedContent?
11+
@ServiceActor
12+
func rejectSuggestion(editor: EditorContent) async throws -> UpdatedContent?
13+
@ServiceActor
14+
func acceptSuggestion(editor: EditorContent) async throws -> UpdatedContent?
15+
@ServiceActor
16+
func presentRealtimeSuggestions(editor: EditorContent) async throws -> UpdatedContent?
17+
@ServiceActor
18+
func generateRealtimeSuggestions(editor: EditorContent) async throws -> UpdatedContent?
19+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import CopilotModel
2+
import Foundation
3+
import SuggestionInjector
4+
import XPCShared
5+
6+
struct PresentInCommentSuggestionPresenter {
7+
func presentSuggestion(
8+
for filespace: Filespace,
9+
in workspace: Workspace,
10+
originalContent: String,
11+
lines: [String],
12+
cursorPosition: CursorPosition
13+
) async throws -> UpdatedContent? {
14+
let injector = SuggestionInjector()
15+
var lines = lines
16+
var cursorPosition = cursorPosition
17+
var extraInfo = SuggestionInjector.ExtraInfo()
18+
19+
injector.rejectCurrentSuggestions(
20+
from: &lines,
21+
cursorPosition: &cursorPosition,
22+
extraInfo: &extraInfo
23+
)
24+
25+
guard let completion = await filespace.presentingSuggestion else {
26+
return .init(
27+
content: originalContent,
28+
newCursor: cursorPosition,
29+
modifications: extraInfo.modifications
30+
)
31+
}
32+
33+
await injector.proposeSuggestion(
34+
intoContentWithoutSuggestion: &lines,
35+
completion: completion,
36+
index: filespace.suggestionIndex,
37+
count: filespace.suggestions.count,
38+
extraInfo: &extraInfo
39+
)
40+
41+
return .init(
42+
content: String(lines.joined(separator: "")),
43+
newCursor: cursorPosition,
44+
modifications: extraInfo.modifications
45+
)
46+
}
47+
48+
func discardSuggestion(
49+
for filespace: Filespace,
50+
in workspace: Workspace,
51+
originalContent: String,
52+
lines: [String],
53+
cursorPosition: CursorPosition
54+
) async throws -> UpdatedContent? {
55+
let injector = SuggestionInjector()
56+
var lines = lines
57+
var cursorPosition = cursorPosition
58+
var extraInfo = SuggestionInjector.ExtraInfo()
59+
60+
injector.rejectCurrentSuggestions(
61+
from: &lines,
62+
cursorPosition: &cursorPosition,
63+
extraInfo: &extraInfo
64+
)
65+
66+
return .init(
67+
content: String(lines.joined(separator: "")),
68+
newCursor: cursorPosition,
69+
modifications: extraInfo.modifications
70+
)
71+
}
72+
}

0 commit comments

Comments
 (0)