@@ -9,45 +9,252 @@ import XcodeInspector
99public final class ActiveDocumentChatContextCollector : ChatContextCollector {
1010 public init ( ) { }
1111
12+ var activeDocumentContext : ActiveDocumentContext ?
13+
1214 public func generateContext(
1315 history: [ ChatMessage ] ,
1416 scopes: Set < String > ,
1517 content: String
1618 ) -> ChatContext ? {
17- guard scopes. contains ( " file " ) else { return nil }
18- let info = getEditorInformation ( )
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+ }
1964
2065 return . init(
21- systemPrompt: extractSystemPrompt ( info ) ,
22- functions: [ ]
66+ systemPrompt: extractSystemPrompt ( context ) ,
67+ functions: functions
2368 )
2469 }
25-
26- func extractSystemPrompt( _ info: EditorInformation ) -> String {
27- let relativePath = info. documentURL. path
28- . replacingOccurrences ( of: info. projectURL. path, with: " " )
29- let selectionRange = info. editorContent? . selections. first ?? . outOfScope
30- let lineAnnotations = info. editorContent? . lineAnnotations ?? [ ]
31-
32- var result = """
33- Active Document Context:###
34- Document Relative Path: \( relativePath)
35- Language: \( info. language. rawValue)
36- Selection Range [line, character]: \
37- [ \( selectionRange. start. line) , \( selectionRange. start. character) ] - \
38- [ \( selectionRange. end. line) , \( selectionRange. end. character) ]
39- ###
40- """
41-
42- if !lineAnnotations. isEmpty {
43- result += """
44- Line Annotations:
45- \( lineAnnotations. map { " - \( $0) " } . joined ( separator: " \n " ) )
70+
71+ func getActiveDocumentContext( _ info: EditorInformation ) -> ActiveDocumentContext {
72+ var activeDocumentContext = activeDocumentContext ?? . init(
73+ relativePath: " " ,
74+ language: . builtIn( . swift) ,
75+ fileContent: " " ,
76+ lines: [ ] ,
77+ selectedCode: " " ,
78+ selectionRange: . outOfScope,
79+ lineAnnotations: [ ] ,
80+ imports: [ ]
81+ )
82+
83+ activeDocumentContext. update ( info)
84+ return activeDocumentContext
85+ }
86+
87+ func extractSystemPrompt( _ context: ActiveDocumentContext ) -> String {
88+ let start = " User Editing Document Context:### "
89+ let end = " ### "
90+ let relativePath = " Document Relative Path: \( context. relativePath) "
91+ let language = " Language: \( context. language) "
92+
93+ if let focusedContext = context. focusedContext {
94+ let codeContext = " \( focusedContext. contextRange) \( focusedContext. context) "
95+ let codeRange = " Focused Range [line, character]: \( focusedContext. codeRange) "
96+ let code = """
97+ Focused Code (start from line \( focusedContext. codeRange. start. line) ):
98+ ``` \( context. language. rawValue)
99+ \( focusedContext. code)
100+ ```
46101 """
102+ let fileAnnotations = focusedContext. otherLineAnnotations. isEmpty
103+ ? " "
104+ : """
105+ File Annotations:
106+ \( focusedContext. otherLineAnnotations. map { " - \( $0) " } . joined ( separator: " \n " ) )
107+ """
108+ let codeAnnotations = focusedContext. lineAnnotations. isEmpty
109+ ? " "
110+ : """
111+ Code Annotations:
112+ \( focusedContext. lineAnnotations. map { " - \( $0) " } . joined ( separator: " \n " ) )
113+ """
114+ return [
115+ start,
116+ relativePath,
117+ language,
118+ fileAnnotations,
119+ codeContext,
120+ codeRange,
121+ codeAnnotations,
122+ code,
123+ end,
124+ ]
125+ . filter { !$0. isEmpty }
126+ . joined ( separator: " \n " )
127+ } else {
128+ let selectionRange = " Selection Range [line, character]: \( context. selectionRange) "
129+ let lineAnnotations = context. lineAnnotations. isEmpty
130+ ? " "
131+ : """
132+ Line Annotations:
133+ \( context. lineAnnotations. map { " - \( $0) " } . joined ( separator: " \n " ) )
134+ """
135+
136+ return [
137+ start,
138+ relativePath,
139+ language,
140+ lineAnnotations,
141+ selectionRange,
142+ end,
143+ ]
144+ . filter { !$0. isEmpty }
145+ . joined ( separator: " \n " )
47146 }
48-
49- return result
50147 }
51148}
52149
150+ struct ActiveDocumentContext {
151+ var relativePath : String
152+ var language : CodeLanguage
153+ var fileContent : String
154+ var lines : [ String ]
155+ var selectedCode : String
156+ var selectionRange : CursorRange
157+ var lineAnnotations : [ EditorInformation . LineAnnotation ]
158+ var imports : [ String ]
159+
160+ struct FocusedContext {
161+ var context : String
162+ var contextRange : CursorRange
163+ var codeRange : CursorRange
164+ var code : String
165+ var lineAnnotations : [ EditorInformation . LineAnnotation ]
166+ var otherLineAnnotations : [ EditorInformation . LineAnnotation ]
167+ }
168+
169+ var focusedContext : FocusedContext ?
170+
171+ mutating func moveToFocusedCode( ) {
172+ moveToCodeContainingRange ( selectionRange)
173+ }
174+
175+ mutating func moveToCodeAroundLine( _ line: Int ) {
176+ moveToCodeContainingRange ( . init(
177+ start: . init( line: line, character: 0 ) ,
178+ end: . init( line: line, character: 0 )
179+ ) )
180+ }
181+
182+ mutating func expandFocusedRangeToContextRange( ) {
183+ guard let focusedContext else { return }
184+ moveToCodeContainingRange ( focusedContext. contextRange)
185+ }
186+
187+ mutating func moveToCodeContainingRange( _ range: CursorRange ) {
188+ let finder : FocusedCodeFinder = {
189+ switch language {
190+ case . builtIn( . swift) :
191+ return SwiftFocusedCodeFinder ( )
192+ default :
193+ return UnknownLanguageFocusedCodeFinder ( )
194+ }
195+ } ( )
196+
197+ let codeContext = finder. findFocusedCode (
198+ containingRange: range,
199+ activeDocumentContext: self
200+ )
201+
202+ imports = codeContext. imports
203+
204+ let startLine = codeContext. focusedRange. start. line
205+ let endLine = codeContext. focusedRange. end. line
206+ var matchedAnnotations = [ EditorInformation . LineAnnotation] ( )
207+ var otherAnnotations = [ EditorInformation . LineAnnotation] ( )
208+ for annotation in lineAnnotations {
209+ if annotation. line >= startLine && annotation. line <= endLine {
210+ matchedAnnotations. append ( annotation)
211+ } else {
212+ otherAnnotations. append ( annotation)
213+ }
214+ }
215+
216+ focusedContext = . init(
217+ context: {
218+ switch codeContext. scope {
219+ case . file:
220+ return " File "
221+ case . top:
222+ return " Top level of the file "
223+ case let . scope( signature) :
224+ return signature
225+ }
226+ } ( ) ,
227+ contextRange: codeContext. contextRange,
228+ codeRange: codeContext. focusedRange,
229+ code: codeContext. focusedCode,
230+ lineAnnotations: matchedAnnotations,
231+ otherLineAnnotations: otherAnnotations
232+ )
233+ }
234+
235+ mutating func update( _ info: EditorInformation ) {
236+ /// Whenever the file content, relative path, or selection range changes,
237+ /// we should reset the context.
238+ let changed : Bool = {
239+ if info. relativePath != relativePath { return true }
240+ if info. editorContent? . content != fileContent { return true }
241+ if let range = info. editorContent? . selections. first,
242+ range != selectionRange { return true }
243+ return false
244+ } ( )
245+
246+ relativePath = info. relativePath
247+ language = info. language
248+ fileContent = info. editorContent? . content ?? " "
249+ lines = info. editorContent? . lines ?? [ ]
250+ selectedCode = info. selectedContent
251+ selectionRange = info. editorContent? . selections. first ?? . zero
252+ lineAnnotations = info. editorContent? . lineAnnotations ?? [ ]
253+ imports = [ ]
254+
255+ if changed {
256+ moveToFocusedCode ( )
257+ }
258+ }
259+ }
53260
0 commit comments