Skip to content

Commit 3a80091

Browse files
committed
Add CommentBaseCommandHandler and PresentInCommentSuggestionPresenter
1 parent 88169d6 commit 3a80091

File tree

5 files changed

+331
-164
lines changed

5 files changed

+331
-164
lines changed

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 {
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
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+
let presenter = PresentInCommentSuggestionPresenter()
114+
return .init(
115+
content: String(lines.joined(separator: "")),
116+
newCursor: cursorPosition,
117+
modifications: extraInfo.modifications
118+
)
119+
}
120+
121+
func presentRealtimeSuggestions(editor: EditorContent) async throws -> UpdatedContent? {
122+
let fileURL = try await Environment.fetchCurrentFileURL()
123+
let workspace = try await Workspace.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
124+
guard let filespace = workspace.filespaces[fileURL] else { return nil }
125+
126+
try Task.checkCancellation()
127+
128+
let snapshot = Filespace.Snapshot(
129+
linesHash: editor.lines.hashValue,
130+
cursorPosition: editor.cursorPosition
131+
)
132+
133+
// If the generated suggestions are for this editor content, present it.
134+
guard filespace.suggestionSourceSnapshot == snapshot else { return nil }
135+
136+
let presenter = PresentInCommentSuggestionPresenter()
137+
return try await presenter.presentSuggestion(
138+
for: filespace,
139+
in: workspace,
140+
originalContent: editor.content,
141+
lines: editor.lines,
142+
cursorPosition: editor.cursorPosition
143+
)
144+
}
145+
146+
func generateRealtimeSuggestions(editor: EditorContent) async throws -> UpdatedContent? {
147+
// We don't need to wait for this.
148+
Task { @ServiceActor in
149+
let fileURL = try await Environment.fetchCurrentFileURL()
150+
let workspace = try await Workspace.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
151+
guard let filespace = workspace.filespaces[fileURL] else { return }
152+
153+
try Task.checkCancellation()
154+
155+
let snapshot = Filespace.Snapshot(
156+
linesHash: editor.lines.hashValue,
157+
cursorPosition: editor.cursorPosition
158+
)
159+
160+
// There is no need to regenerate suggestions for the same editor content.
161+
guard filespace.suggestionSourceSnapshot != snapshot else { return }
162+
163+
let suggestions = try await workspace.generateSuggestions(
164+
forFileAt: fileURL,
165+
content: editor.content,
166+
lines: editor.lines,
167+
cursorPosition: editor.cursorPosition,
168+
tabSize: editor.tabSize,
169+
indentSize: editor.indentSize,
170+
usesTabsForIndentation: editor.usesTabsForIndentation
171+
)
172+
173+
try Task.checkCancellation()
174+
175+
// If there is a suggestion available, call another command to present it.
176+
guard !suggestions.isEmpty else { return }
177+
try await Environment.triggerAction("Real-time Suggestions")
178+
}
179+
180+
return nil
181+
}
182+
}
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)