Skip to content

Commit f6e8fdb

Browse files
committed
Merge branch 'feature/ignore-empty-suggestions' into develop
2 parents d540879 + 405ca3a commit f6e8fdb

5 files changed

Lines changed: 330 additions & 81 deletions

File tree

Core/Sources/CopilotService/CopilotService.swift

Lines changed: 87 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -19,83 +19,92 @@ public protocol CopilotSuggestionServiceType {
1919
cursorPosition: CursorPosition,
2020
tabSize: Int,
2121
indentSize: Int,
22-
usesTabsForIndentation: Bool
22+
usesTabsForIndentation: Bool,
23+
ignoreSpaceOnlySuggestions: Bool
2324
) async throws -> [CopilotCompletion]
2425
func notifyAccepted(_ completion: CopilotCompletion) async
2526
func notifyRejected(_ completions: [CopilotCompletion]) async
2627
}
2728

29+
protocol CopilotLSP {
30+
func sendRequest<E: CopilotRequestType>(_ endpoint: E) async throws -> E.Response
31+
}
32+
2833
public class CopilotBaseService {
2934
let projectRootURL: URL
35+
var server: CopilotLSP
36+
37+
init(designatedServer: CopilotLSP) {
38+
projectRootURL = URL(fileURLWithPath: "/")
39+
server = designatedServer
40+
}
3041

3142
init(projectRootURL: URL) {
3243
self.projectRootURL = projectRootURL
33-
}
44+
server = {
45+
let supportURL = FileManager.default.urls(
46+
for: .applicationSupportDirectory,
47+
in: .userDomainMask
48+
).first!.appendingPathComponent("com.intii.CopilotForXcode")
49+
if !FileManager.default.fileExists(atPath: supportURL.path) {
50+
try? FileManager.default
51+
.createDirectory(at: supportURL, withIntermediateDirectories: false)
52+
}
53+
let executionParams = {
54+
let nodePath = UserDefaults.shared.string(forKey: SettingsKey.nodePath) ?? ""
55+
return Process.ExecutionParameters(
56+
path: "/usr/bin/env",
57+
arguments: [
58+
nodePath.isEmpty ? "node" : nodePath,
59+
Bundle.main.url(
60+
forResource: "agent",
61+
withExtension: "js",
62+
subdirectory: "copilot/dist"
63+
)!.path,
64+
"--stdio",
65+
],
66+
environment: [
67+
"PATH": "/usr/bin:/usr/local/bin",
68+
],
69+
currentDirectoryURL: supportURL
70+
)
71+
}()
72+
let localServer = LocalProcessServer(executionParameters: executionParams)
73+
localServer.logMessages = false
74+
let server = InitializingServer(server: localServer)
75+
server.notificationHandler = { _, respond in
76+
respond(nil)
77+
}
3478

35-
lazy var server: InitializingServer = {
36-
let supportURL = FileManager.default.urls(
37-
for: .applicationSupportDirectory,
38-
in: .userDomainMask
39-
).first!.appendingPathComponent("com.intii.CopilotForXcode")
40-
if !FileManager.default.fileExists(atPath: supportURL.path) {
41-
try? FileManager.default
42-
.createDirectory(at: supportURL, withIntermediateDirectories: false)
43-
}
44-
let executionParams = {
45-
let nodePath = UserDefaults.shared.string(forKey: SettingsKey.nodePath) ?? ""
46-
return Process.ExecutionParameters(
47-
path: "/usr/bin/env",
48-
arguments: [
49-
nodePath.isEmpty ? "node" : nodePath,
50-
Bundle.main.url(
51-
forResource: "agent",
52-
withExtension: "js",
53-
subdirectory: "copilot/dist"
54-
)!.path,
55-
"--stdio",
56-
],
57-
environment: [
58-
"PATH": "/usr/bin:/usr/local/bin",
59-
],
60-
currentDirectoryURL: supportURL
61-
)
62-
}()
63-
let localServer = LocalProcessServer(executionParameters: executionParams)
64-
localServer.logMessages = false
65-
let server = InitializingServer(server: localServer)
66-
server.notificationHandler = { _, respond in
67-
respond(nil)
68-
}
79+
server.initializeParamsProvider = {
80+
let capabilities = ClientCapabilities(
81+
workspace: nil,
82+
textDocument: nil,
83+
window: nil,
84+
general: nil,
85+
experimental: nil
86+
)
6987

70-
let projectRoot = self.projectRootURL
71-
server.initializeParamsProvider = {
72-
let capabilities = ClientCapabilities(
73-
workspace: nil,
74-
textDocument: nil,
75-
window: nil,
76-
general: nil,
77-
experimental: nil
78-
)
79-
80-
return InitializeParams(
81-
processId: Int(ProcessInfo.processInfo.processIdentifier),
82-
clientInfo: .init(name: "Copilot for Xcode"),
83-
locale: nil,
84-
rootPath: projectRoot.path,
85-
rootUri: projectRoot.path,
86-
initializationOptions: nil,
87-
capabilities: capabilities,
88-
trace: nil,
89-
workspaceFolders: nil
90-
)
91-
}
88+
return InitializeParams(
89+
processId: Int(ProcessInfo.processInfo.processIdentifier),
90+
clientInfo: .init(name: "Copilot for Xcode"),
91+
locale: nil,
92+
rootPath: projectRootURL.path,
93+
rootUri: projectRootURL.path,
94+
initializationOptions: nil,
95+
capabilities: capabilities,
96+
trace: nil,
97+
workspaceFolders: nil
98+
)
99+
}
92100

93-
server.notificationHandler = { _, respond in
94-
respond(nil)
95-
}
101+
server.notificationHandler = { _, respond in
102+
respond(nil)
103+
}
96104

97-
return server
98-
}()
105+
return server
106+
}()
107+
}
99108
}
100109

101110
public final class CopilotAuthService: CopilotBaseService, CopilotAuthServiceType {
@@ -136,14 +145,19 @@ public final class CopilotSuggestionService: CopilotBaseService, CopilotSuggesti
136145
override public init(projectRootURL: URL = URL(fileURLWithPath: "/")) {
137146
super.init(projectRootURL: projectRootURL)
138147
}
148+
149+
override init(designatedServer: CopilotLSP) {
150+
super.init(designatedServer: designatedServer)
151+
}
139152

140153
public func getCompletions(
141154
fileURL: URL,
142155
content: String,
143156
cursorPosition: CursorPosition,
144157
tabSize: Int,
145158
indentSize: Int,
146-
usesTabsForIndentation: Bool
159+
usesTabsForIndentation: Bool,
160+
ignoreSpaceOnlySuggestions: Bool
147161
) async throws -> [CopilotCompletion] {
148162
guard let languageId = languageIdentifierFromFileURL(fileURL) else { return [] }
149163

@@ -173,7 +187,14 @@ public final class CopilotSuggestionService: CopilotBaseService, CopilotSuggesti
173187
relativePath: relativePath,
174188
languageId: languageId,
175189
position: cursorPosition
176-
))).completions
190+
)))
191+
.completions
192+
.filter { completion in
193+
if ignoreSpaceOnlySuggestions {
194+
return !completion.text.allSatisfy { $0.isWhitespace || $0.isNewline }
195+
}
196+
return true
197+
}
177198

178199
return completions
179200
}
@@ -191,7 +212,7 @@ public final class CopilotSuggestionService: CopilotBaseService, CopilotSuggesti
191212
}
192213
}
193214

194-
extension InitializingServer {
215+
extension InitializingServer: CopilotLSP {
195216
func sendRequest<E: CopilotRequestType>(_ endpoint: E) async throws -> E.Response {
196217
try await sendRequest(endpoint.request)
197218
}

Core/Sources/Service/Workspace.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,8 @@ final class Workspace {
197197
cursorPosition: cursorPosition,
198198
tabSize: tabSize,
199199
indentSize: indentSize,
200-
usesTabsForIndentation: usesTabsForIndentation
200+
usesTabsForIndentation: usesTabsForIndentation,
201+
ignoreSpaceOnlySuggestions: true
201202
)
202203

203204
guard filespace.suggestionSourceSnapshot == snapshot else { return nil }

Core/Sources/SuggestionInjector/SuggestionInjector.swift

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public struct SuggestionInjector {
2727
var ranges = [ClosedRange<Int>]()
2828
var suggestionStartIndex = -1
2929

30+
// find ranges of suggestion comments
3031
for (index, line) in content.enumerated() {
3132
if line.hasPrefix(suggestionStart) {
3233
suggestionStartIndex = index
@@ -42,6 +43,7 @@ public struct SuggestionInjector {
4243
extraInfo.modifications.append(contentsOf: reversedRanges.map(Modification.deleted))
4344
extraInfo.didChangeContent = !ranges.isEmpty
4445

46+
// remove the lines from bottom to top
4547
for range in reversedRanges {
4648
for i in stride(from: range.upperBound, through: range.lowerBound, by: -1) {
4749
if i <= cursorPosition.line, cursorPosition.line >= 0 {
@@ -65,17 +67,32 @@ public struct SuggestionInjector {
6567
count: Int,
6668
extraInfo: inout ExtraInfo
6769
) {
70+
// assemble suggestion comment
6871
let start = completion.range.start
6972
let startText = "\(suggestionStart) \(index + 1)/\(count)"
7073
var lines = [startText + "\n"]
7174
lines.append(contentsOf: completion.text.breakLines(appendLineBreakToLastLine: true))
7275
lines.append(suggestionEnd + "\n")
73-
if lines.count <= 2 { return }
7476

77+
// if suggestion is empty, returns without modifying the code
78+
guard lines.count > 2 else { return }
79+
80+
// replace the common prefix of the first line with space and carrot
7581
let existedLine = start.line < content.endIndex ? content[start.line] : nil
7682
let commonPrefix = longestCommonPrefix(of: lines[1], and: existedLine ?? "")
7783

7884
if !commonPrefix.isEmpty {
85+
let replacingText = {
86+
switch (commonPrefix.hasSuffix("\n"), commonPrefix.count) {
87+
case (false, let count):
88+
return String(repeating: " ", count: count - 1) + "^"
89+
case (true, let count) where count > 1:
90+
return String(repeating: " ", count: count - 2) + "^\n"
91+
case (true, _):
92+
return "\n"
93+
}
94+
}()
95+
7996
lines[1].replaceSubrange(
8097
lines[1].startIndex..<(
8198
lines[1].index(
@@ -84,15 +101,20 @@ public struct SuggestionInjector {
84101
limitedBy: lines[1].endIndex
85102
) ?? lines[1].endIndex
86103
),
87-
with: String(repeating: " ", count: commonPrefix.count - 1) + "^"
104+
with: replacingText
88105
)
89106
}
90107

108+
// if the suggestion is only appeding new lines and spaces, return without modification
109+
if completion.text.dropFirst(commonPrefix.count)
110+
.allSatisfy({ $0.isWhitespace || $0.isNewline }) { return }
111+
112+
// determin if it's inserted to the current line or the next line
91113
let lineIndex = start.line + {
92114
guard let existedLine else { return 0 }
93115
if existedLine.isEmptyOrNewLine { return 1 }
94-
if !commonPrefix.isEmpty, commonPrefix.count <= existedLine.count - 1 { return 1 }
95-
return 0
116+
if commonPrefix.isEmpty { return 0 }
117+
return 1
96118
}()
97119
if content.endIndex < lineIndex {
98120
extraInfo.didChangeContent = true
@@ -155,6 +177,7 @@ public struct SuggestionInjector {
155177
}
156178

157179
extension String {
180+
/// Break a string into lines.
158181
func breakLines(appendLineBreakToLastLine: Bool = false) -> [String] {
159182
let lines = split(separator: "\n", omittingEmptySubsequences: false)
160183
var all = [String]()

0 commit comments

Comments
 (0)