@@ -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