Skip to content

Commit b82b5ed

Browse files
committed
Merge branch 'feature/file-scope-ast' into develop
2 parents adeeba3 + 0f39281 commit b82b5ed

39 files changed

Lines changed: 2684 additions & 192 deletions

Copilot for Xcode.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Core/Package.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ let package = Package(
5151
url: "https://github.com/pointfreeco/swift-composable-architecture",
5252
from: "0.55.0"
5353
),
54+
.package(url: "https://github.com/apple/swift-syntax.git", branch: "main"),
5455
].pro,
5556
targets: [
5657
// MARK: - Main
@@ -178,6 +179,7 @@ let package = Package(
178179

179180
// context collectors
180181
"WebChatContextCollector",
182+
"ActiveDocumentChatContextCollector",
181183

182184
.product(name: "AppMonitoring", package: "Tool"),
183185
.product(name: "Environment", package: "Tool"),
@@ -350,6 +352,25 @@ let package = Package(
350352
],
351353
path: "Sources/ChatContextCollectors/WebChatContextCollector"
352354
),
355+
356+
.target(
357+
name: "ActiveDocumentChatContextCollector",
358+
dependencies: [
359+
"ChatContextCollector",
360+
.product(name: "LangChain", package: "Tool"),
361+
.product(name: "OpenAIService", package: "Tool"),
362+
.product(name: "Preferences", package: "Tool"),
363+
.product(name: "ASTParser", package: "Tool"),
364+
.product(name: "SwiftSyntax", package: "swift-syntax"),
365+
.product(name: "SwiftParser", package: "swift-syntax"),
366+
],
367+
path: "Sources/ChatContextCollectors/ActiveDocumentChatContextCollector"
368+
),
369+
370+
.testTarget(
371+
name: "ActiveDocumentChatContextCollectorTests",
372+
dependencies: ["ActiveDocumentChatContextCollector"]
373+
),
353374
]
354375
)
355376

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
import ASTParser
2+
import ChatContextCollector
3+
import Foundation
4+
import OpenAIService
5+
import Preferences
6+
import SuggestionModel
7+
import XcodeInspector
8+
9+
public final class ActiveDocumentChatContextCollector: ChatContextCollector {
10+
public init() {}
11+
12+
var activeDocumentContext: ActiveDocumentContext?
13+
14+
public func generateContext(
15+
history: [ChatMessage],
16+
scopes: Set<String>,
17+
content: String
18+
) -> ChatContext? {
19+
guard let info = getEditorInformation() else { return nil }
20+
let context = getActiveDocumentContext(info)
21+
activeDocumentContext = context
22+
23+
guard scopes.contains("code") || scopes.contains("c") else {
24+
if scopes.contains("file") || scopes.contains("f") {
25+
var removedCode = context
26+
removedCode.focusedContext = nil
27+
return .init(
28+
systemPrompt: extractSystemPrompt(removedCode),
29+
functions: []
30+
)
31+
}
32+
return nil
33+
}
34+
35+
var functions = [any ChatGPTFunction]()
36+
37+
// When the bot is already focusing on a piece of code, it can expand the range.
38+
39+
if context.focusedContext != nil {
40+
functions.append(ExpandFocusRangeFunction(contextCollector: self))
41+
}
42+
43+
// When the bot is not focusing on any code, or the focusing area is not the user's
44+
// selection, it can move the focus back to the user's selection.
45+
46+
if context.focusedContext == nil ||
47+
!(context.focusedContext?.codeRange.contains(context.selectionRange) ?? false)
48+
{
49+
functions.append(MoveToFocusedCodeFunction(contextCollector: self))
50+
}
51+
52+
// When there is a line annotation not in the focused area, the bot can move the focus area
53+
// to the code covering the line of the annotation.
54+
55+
if let focusedContext = context.focusedContext,
56+
!focusedContext.otherLineAnnotations.isEmpty
57+
{
58+
functions.append(MoveToCodeAroundLineFunction(contextCollector: self))
59+
}
60+
61+
if context.focusedContext == nil, !context.lineAnnotations.isEmpty {
62+
functions.append(MoveToCodeAroundLineFunction(contextCollector: self))
63+
}
64+
65+
return .init(
66+
systemPrompt: extractSystemPrompt(context),
67+
functions: functions
68+
)
69+
}
70+
71+
func getActiveDocumentContext(_ info: EditorInformation) -> ActiveDocumentContext {
72+
var activeDocumentContext = activeDocumentContext ?? .init(
73+
filePath: "",
74+
relativePath: "",
75+
language: .builtIn(.swift),
76+
fileContent: "",
77+
lines: [],
78+
selectedCode: "",
79+
selectionRange: .outOfScope,
80+
lineAnnotations: [],
81+
imports: []
82+
)
83+
84+
activeDocumentContext.update(info)
85+
return activeDocumentContext
86+
}
87+
88+
func extractSystemPrompt(_ context: ActiveDocumentContext) -> String {
89+
let start = """
90+
## File and Code Scope
91+
92+
You can use the following context to answer user's questions about the editing document or code. The context shows only a part of the code in the editing document, and will change during the conversation, so it may not match our conversation.
93+
94+
User Editing Document Context: ###
95+
"""
96+
let end = "###"
97+
let relativePath = "Document Relative Path: \(context.relativePath)"
98+
let language = "Language: \(context.language.rawValue)"
99+
100+
if let focusedContext = context.focusedContext {
101+
let codeContext = focusedContext.context.isEmpty
102+
? ""
103+
: """
104+
Focused Context:
105+
```
106+
\(focusedContext.context.joined(separator: "\n"))
107+
```
108+
"""
109+
110+
let codeRange = "Focused Range [line, character]: \(focusedContext.codeRange)"
111+
112+
let code = """
113+
Focused Code (start from line \(
114+
focusedContext.codeRange.start
115+
.line
116+
)):
117+
```\(context.language.rawValue)
118+
\(focusedContext.code)
119+
```
120+
"""
121+
122+
let fileAnnotations = focusedContext.otherLineAnnotations.isEmpty
123+
? ""
124+
: """
125+
Other Annotations:\"""
126+
(They are not inside the focused code. You don't known how to handle them until you get the code at the line)
127+
\(
128+
focusedContext.otherLineAnnotations
129+
.map(convertAnnotationToText)
130+
.joined(separator: "\n")
131+
)
132+
\"""
133+
"""
134+
135+
let codeAnnotations = focusedContext.lineAnnotations.isEmpty
136+
? ""
137+
: """
138+
Annotations Inside Focused Range:\"""
139+
\(
140+
focusedContext.lineAnnotations
141+
.map(convertAnnotationToText)
142+
.joined(separator: "\n")
143+
)
144+
\"""
145+
"""
146+
147+
return [
148+
start,
149+
relativePath,
150+
language,
151+
codeContext,
152+
codeRange,
153+
code,
154+
codeAnnotations,
155+
fileAnnotations,
156+
end,
157+
]
158+
.filter { !$0.isEmpty }
159+
.joined(separator: "\n\n")
160+
} else {
161+
let selectionRange = "Selection Range [line, character]: \(context.selectionRange)"
162+
let lineAnnotations = context.lineAnnotations.isEmpty
163+
? ""
164+
: """
165+
Line Annotations:\"""
166+
\(context.lineAnnotations.map(convertAnnotationToText).joined(separator: "\n"))
167+
\"""
168+
"""
169+
170+
return [
171+
start,
172+
relativePath,
173+
language,
174+
lineAnnotations,
175+
selectionRange,
176+
end,
177+
]
178+
.filter { !$0.isEmpty }
179+
.joined(separator: "\n")
180+
}
181+
}
182+
183+
func convertAnnotationToText(_ annotation: EditorInformation.LineAnnotation) -> String {
184+
return "- Line \(annotation.line), \(annotation.type): \(annotation.message)"
185+
}
186+
}
187+
188+
struct ActiveDocumentContext {
189+
var filePath: String
190+
var relativePath: String
191+
var language: CodeLanguage
192+
var fileContent: String
193+
var lines: [String]
194+
var selectedCode: String
195+
var selectionRange: CursorRange
196+
var lineAnnotations: [EditorInformation.LineAnnotation]
197+
var imports: [String]
198+
199+
struct FocusedContext {
200+
var context: [String]
201+
var contextRange: CursorRange
202+
var codeRange: CursorRange
203+
var code: String
204+
var lineAnnotations: [EditorInformation.LineAnnotation]
205+
var otherLineAnnotations: [EditorInformation.LineAnnotation]
206+
}
207+
208+
var focusedContext: FocusedContext?
209+
210+
mutating func moveToFocusedCode() {
211+
moveToCodeContainingRange(selectionRange)
212+
}
213+
214+
mutating func moveToCodeAroundLine(_ line: Int) {
215+
moveToCodeContainingRange(.init(
216+
start: .init(line: line, character: 0),
217+
end: .init(line: line, character: 0)
218+
))
219+
}
220+
221+
mutating func expandFocusedRangeToContextRange() {
222+
guard let focusedContext else { return }
223+
moveToCodeContainingRange(focusedContext.contextRange)
224+
}
225+
226+
mutating func moveToCodeContainingRange(_ range: CursorRange) {
227+
let finder: FocusedCodeFinder = {
228+
switch language {
229+
case .builtIn(.swift):
230+
return SwiftFocusedCodeFinder()
231+
default:
232+
return UnknownLanguageFocusedCodeFinder(proposedSearchRange: 5)
233+
}
234+
}()
235+
236+
let codeContext = finder.findFocusedCode(
237+
containingRange: range,
238+
activeDocumentContext: self
239+
)
240+
241+
imports = codeContext.imports
242+
243+
let startLine = codeContext.focusedRange.start.line
244+
let endLine = codeContext.focusedRange.end.line
245+
var matchedAnnotations = [EditorInformation.LineAnnotation]()
246+
var otherAnnotations = [EditorInformation.LineAnnotation]()
247+
for annotation in lineAnnotations {
248+
if annotation.line >= startLine, annotation.line <= endLine {
249+
matchedAnnotations.append(annotation)
250+
} else {
251+
otherAnnotations.append(annotation)
252+
}
253+
}
254+
255+
focusedContext = .init(
256+
context: codeContext.scopeSignatures,
257+
contextRange: codeContext.contextRange,
258+
codeRange: codeContext.focusedRange,
259+
code: codeContext.focusedCode,
260+
lineAnnotations: matchedAnnotations,
261+
otherLineAnnotations: otherAnnotations
262+
)
263+
}
264+
265+
mutating func update(_ info: EditorInformation) {
266+
/// Whenever the file content, relative path, or selection range changes,
267+
/// we should reset the context.
268+
let changed: Bool = {
269+
if info.relativePath != relativePath { return true }
270+
if info.editorContent?.content != fileContent { return true }
271+
if let range = info.editorContent?.selections.first,
272+
range != selectionRange { return true }
273+
return false
274+
}()
275+
276+
filePath = info.documentURL.path
277+
relativePath = info.relativePath
278+
language = info.language
279+
fileContent = info.editorContent?.content ?? ""
280+
lines = info.editorContent?.lines ?? []
281+
selectedCode = info.selectedContent
282+
selectionRange = info.editorContent?.selections.first ?? .zero
283+
lineAnnotations = info.editorContent?.lineAnnotations ?? []
284+
imports = []
285+
286+
if changed {
287+
moveToFocusedCode()
288+
}
289+
}
290+
}
291+

0 commit comments

Comments
 (0)