@@ -8,6 +8,26 @@ import Logger
88import XcodeInspector
99import ChatAPIService
1010
11+ public enum InsertEditError : LocalizedError {
12+ case missingEditorElement ( file : URL )
13+ case openingApplicationUnavailable
14+ case fileNotOpenedInXcode
15+ case fileURLMismatch ( expected : URL , actual : URL ? )
16+
17+ public var errorDescription : String ? {
18+ switch self {
19+ case . missingEditorElement ( let file ) :
20+ return " Could not find source editor element for file \( file . lastPathComponent ) . "
21+ case . openingApplicationUnavailable :
22+ return " Failed to get the application that opened the file. "
23+ case . fileNotOpenedInXcode :
24+ return " The file is not currently opened in Xcode. "
25+ case . fileURLMismatch ( let expected , let actual ) :
26+ return " The currently focused file URL \( actual ? . lastPathComponent ?? " unknown " ) does not match the expected file URL \( expected . lastPathComponent ) . "
27+ }
28+ }
29+ }
30+
1131public class InsertEditIntoFileTool : ICopilotTool {
1232 public static let name = ToolName . insertEditIntoFile
1333
@@ -89,16 +109,9 @@ public class InsertEditIntoFileTool: ICopilotTool {
89109 contextProvider : any ToolContextProvider ,
90110 xcodeInstance : AppInstanceInspector
91111 ) throws -> String {
92- Thread . sleep ( forTimeInterval : 0.5 )
93- // Get the focused element directly from the app (like XcodeInspector does)
94- guard let focusedElement : AXUIElement = xcodeInstance . appElement . focusedElement
112+ guard let editorElement = Self . getEditorElement ( by : xcodeInstance , for : fileURL )
95113 else {
96- throw NSError ( domain : " Failed to access xcode element " , code : 0 )
97- }
98-
99- // Find the source editor element using XcodeInspector's logic
100- guard let editorElement = focusedElement . findSourceEditorElement ( ) else {
101- throw NSError ( domain : " Could not find source editor element " , code : 0 )
114+ throw InsertEditError . missingEditorElement ( file : fileURL )
102115 }
103116
104117 // Check if element supports kAXValueAttribute before reading
@@ -115,6 +128,8 @@ public class InsertEditIntoFileTool: ICopilotTool {
115128 let lines = value . components ( separatedBy : . newlines )
116129
117130 do {
131+ try Self . checkOpenedFileURL ( for : fileURL , xcodeInstance : xcodeInstance )
132+
118133 try AXHelper ( ) . injectUpdatedCodeWithAccessibilityAPI (
119134 . init (
120135 content : content ,
@@ -133,25 +148,8 @@ public class InsertEditIntoFileTool: ICopilotTool {
133148 throw error
134149 }
135150
136- guard let refreshedFocusedElement : AXUIElement = xcodeInstance . appElement . focusedElement ,
137- let refreshedEditorElement = refreshedFocusedElement . findSourceEditorElement ( )
138- else {
139- throw NSError ( domain : " Failed to access xcode element " , code : 0 )
140- }
141-
142151 // Verify the content was applied by reading it back
143- do {
144- let newContent : String = try refreshedEditorElement . copyValue ( key : kAXValueAttribute )
145- Logger . client . info ( " Successfully read back new content, length: \( newContent . count ) " )
146-
147- return newContent
148- } catch {
149- Logger . client . error ( " Failed to read back new content: \( error ) " )
150- if let axError = error as? AXError {
151- Logger . client . error ( " AX Error code when reading back: \( axError . rawValue ) " )
152- }
153- throw error
154- }
152+ return try Self . getCurrentEditorContent ( for : fileURL , by : xcodeInstance )
155153 }
156154
157155 public static func applyEdit (
@@ -166,15 +164,15 @@ public class InsertEditIntoFileTool: ICopilotTool {
166164
167165 guard let app = app
168166 else {
169- throw NSError ( domain : " Failed to get the app that opens file. " , code : 0 )
167+ throw InsertEditError . openingApplicationUnavailable
170168 }
171169
172170 let appInstanceInspector = AppInstanceInspector ( runningApplication : app )
173171 guard appInstanceInspector . isXcode
174172 else {
175- throw NSError ( domain : " The file is not opened in Xcode. " , code : 0 )
173+ throw InsertEditError . fileNotOpenedInXcode
176174 }
177-
175+
178176 let newContent = try applyEdit (
179177 for : fileURL ,
180178 content : content ,
@@ -192,4 +190,61 @@ public class InsertEditIntoFileTool: ICopilotTool {
192190 }
193191 }
194192 }
193+
194+ /// Get the source editor element with retries for specific file URL
195+ private static func getEditorElement (
196+ by xcodeInstance : AppInstanceInspector ,
197+ for fileURL : URL ,
198+ retryTimes : Int = 6 ,
199+ delay : TimeInterval = 0.5
200+ ) -> AXUIElement ? {
201+ var remainingAttempts = max ( 1 , retryTimes )
202+
203+ while remainingAttempts > 0 {
204+ guard let realtimeURL = xcodeInstance . appElement . realtimeDocumentURL ,
205+ realtimeURL == fileURL ,
206+ let focusedElement = xcodeInstance . appElement . focusedElement ,
207+ let editorElement = focusedElement . findSourceEditorElement ( )
208+ else {
209+ if remainingAttempts > 1 {
210+ Thread . sleep ( forTimeInterval : delay )
211+ }
212+
213+ remainingAttempts -= 1
214+ continue
215+ }
216+
217+ return editorElement
218+ }
219+
220+ Logger . client . error ( " Editor element not found for \( fileURL . lastPathComponent ) after \( retryTimes ) attempts. " )
221+ return nil
222+ }
223+
224+ // Check if current opened file is the target URL
225+ private static func checkOpenedFileURL (
226+ for fileURL : URL ,
227+ xcodeInstance : AppInstanceInspector
228+ ) throws {
229+ let realtimeDocumentURL = xcodeInstance . realtimeDocumentURL
230+
231+ if realtimeDocumentURL != fileURL {
232+ throw InsertEditError . fileURLMismatch ( expected : fileURL , actual : realtimeDocumentURL )
233+ }
234+ }
235+
236+ private static func getCurrentEditorContent ( for fileURL : URL , by xcodeInstance : AppInstanceInspector ) throws -> String {
237+ guard let editorElement = getEditorElement ( by : xcodeInstance , for : fileURL , retryTimes : 1 )
238+ else {
239+ throw InsertEditError . missingEditorElement ( file : fileURL )
240+ }
241+
242+ return try editorElement . copyValue ( key : kAXValueAttribute )
243+ }
244+ }
245+
246+ private extension AppInstanceInspector {
247+ var realtimeDocumentURL : URL ? {
248+ appElement . realtimeDocumentURL
249+ }
195250}
0 commit comments