@@ -7,12 +7,16 @@ import JSONRPC
77import Logger
88import XcodeInspector
99import ChatAPIService
10+ import SystemUtils
11+ import Workspace
1012
1113public enum InsertEditError : LocalizedError {
1214 case missingEditorElement( file: URL )
1315 case openingApplicationUnavailable
1416 case fileNotOpenedInXcode
1517 case fileURLMismatch( expected: URL , actual: URL ? )
18+ case fileNotAccessible( URL )
19+ case fileHasUnsavedChanges( URL )
1620
1721 public var errorDescription : String ? {
1822 switch self {
@@ -24,6 +28,10 @@ public enum InsertEditError: LocalizedError {
2428 return " The file is not currently opened in Xcode. "
2529 case . fileURLMismatch( let expected, let actual) :
2630 return " The currently focused file URL \( actual? . lastPathComponent ?? " unknown " ) does not match the expected file URL \( expected. lastPathComponent) . "
31+ case . fileNotAccessible( let fileURL) :
32+ return " The file \( fileURL. lastPathComponent) is not accessible. "
33+ case . fileHasUnsavedChanges( let fileURL) :
34+ return " The file \( fileURL. lastPathComponent) seems to have unsaved changes in Xcode. Please save the file and try again. "
2735 }
2836 }
2937}
@@ -50,7 +58,7 @@ public class InsertEditIntoFileTool: ICopilotTool {
5058 let fileURL = URL ( fileURLWithPath: filePath)
5159 let originalContent = try String ( contentsOf: fileURL, encoding: . utf8)
5260
53- InsertEditIntoFileTool . applyEdit ( for: fileURL, content: code, contextProvider : contextProvider ) { newContent, error in
61+ InsertEditIntoFileTool . applyEdit ( for: fileURL, content: code) { newContent, error in
5462 if let error = error {
5563 self . completeResponse (
5664 request,
@@ -106,7 +114,6 @@ public class InsertEditIntoFileTool: ICopilotTool {
106114 public static func applyEdit(
107115 for fileURL: URL ,
108116 content: String ,
109- contextProvider: any ToolContextProvider ,
110117 xcodeInstance: AppInstanceInspector
111118 ) throws -> String {
112119 guard let editorElement = Self . getEditorElement ( by: xcodeInstance, for: fileURL)
@@ -155,39 +162,21 @@ public class InsertEditIntoFileTool: ICopilotTool {
155162 public static func applyEdit(
156163 for fileURL: URL ,
157164 content: String ,
158- contextProvider: any ToolContextProvider ,
159165 completion: ( ( String ? , Error ? ) -> Void ) ? = nil
160166 ) {
161- NSWorkspace . openFileInXcode ( fileURL: fileURL) { app, error in
162- do {
163- if let error = error { throw error }
164-
165- guard let app = app
166- else {
167- throw InsertEditError . openingApplicationUnavailable
168- }
169-
170- let appInstanceInspector = AppInstanceInspector ( runningApplication: app)
171- guard appInstanceInspector. isXcode
172- else {
173- throw InsertEditError . fileNotOpenedInXcode
174- }
175-
176- let newContent = try applyEdit (
177- for: fileURL,
178- content: content,
179- contextProvider: contextProvider,
180- xcodeInstance: appInstanceInspector
181- )
182-
183- Task {
184- await WorkspaceInvocationCoordinator ( ) . invokeFilespaceUpdate ( fileURL: fileURL, content: newContent)
185- if let completion = completion { completion ( newContent, nil ) }
186- }
187- } catch {
188- if let completion = completion { completion ( nil , error) }
189- Logger . client. info ( " Failed to apply edit for file at \( fileURL) , \( error) " )
190- }
167+ if SystemUtils . isDeveloperMode || SystemUtils . isPrereleaseBuild {
168+ /// Experimental solution: Use file system write for better reliability. Only enable in dev mode or prerelease builds.
169+ Self . applyEditWithFileSystem (
170+ for: fileURL,
171+ content: content,
172+ completion: completion
173+ )
174+ } else {
175+ Self . applyEditWithAccessibilityAPI (
176+ for: fileURL,
177+ content: content,
178+ completion: completion
179+ )
191180 }
192181 }
193182
@@ -248,3 +237,72 @@ private extension AppInstanceInspector {
248237 appElement. realtimeDocumentURL
249238 }
250239}
240+
241+ extension InsertEditIntoFileTool {
242+ static func applyEditWithFileSystem(
243+ for fileURL: URL ,
244+ content: String ,
245+ completion: ( ( String ? , Error ? ) -> Void ) ? = nil
246+ ) {
247+ do {
248+ guard let diskFileContent = try ? String ( contentsOf: fileURL) else {
249+ throw InsertEditError . fileNotAccessible ( fileURL)
250+ }
251+
252+ if let focusedElement = XcodeInspector . shared. focusedElement,
253+ focusedElement. isSourceEditor,
254+ focusedElement. value != diskFileContent
255+ {
256+ throw InsertEditError . fileHasUnsavedChanges ( fileURL)
257+ }
258+
259+ // write content to disk
260+ try content. write ( to: fileURL, atomically: true , encoding: . utf8)
261+
262+ Task { @WorkspaceActor in
263+ await WorkspaceInvocationCoordinator ( ) . invokeFilespaceUpdate ( fileURL: fileURL, content: content)
264+ if let completion = completion { completion ( content, nil ) }
265+ }
266+ } catch {
267+ if let completion = completion { completion ( nil , error) }
268+ Logger . client. info ( " Failed to apply edit for file at \( fileURL) , \( error) " )
269+ }
270+ }
271+
272+ static func applyEditWithAccessibilityAPI(
273+ for fileURL: URL ,
274+ content: String ,
275+ completion: ( ( String ? , Error ? ) -> Void ) ? = nil ,
276+ ) {
277+ NSWorkspace . openFileInXcode ( fileURL: fileURL) { app, error in
278+ do {
279+ if let error = error { throw error }
280+
281+ guard let app = app
282+ else {
283+ throw InsertEditError . openingApplicationUnavailable
284+ }
285+
286+ let appInstanceInspector = AppInstanceInspector ( runningApplication: app)
287+ guard appInstanceInspector. isXcode
288+ else {
289+ throw InsertEditError . fileNotOpenedInXcode
290+ }
291+
292+ let newContent = try applyEdit (
293+ for: fileURL,
294+ content: content,
295+ xcodeInstance: appInstanceInspector
296+ )
297+
298+ Task {
299+ await WorkspaceInvocationCoordinator ( ) . invokeFilespaceUpdate ( fileURL: fileURL, content: newContent)
300+ if let completion = completion { completion ( newContent, nil ) }
301+ }
302+ } catch {
303+ if let completion = completion { completion ( nil , error) }
304+ Logger . client. info ( " Failed to apply edit for file at \( fileURL) , \( error) " )
305+ }
306+ }
307+ }
308+ }
0 commit comments