From b87b0ed992628e68d2c7057bfe9ea225ebf71d0f Mon Sep 17 00:00:00 2001 From: Tobias Melcher Date: Mon, 27 Jul 2026 11:00:06 +0200 Subject: [PATCH 1/2] feat: support ABAP/ADT files in insert_edit_into_file tool Editing ABAP source via the insert_edit_into_file tool did not work correctly for two reasons: - The ABAP source editor does not observe changes to the underlying file, so after writing new contents to disk its editor buffer stayed dirty and out of date. Force the open editor to reload by reverting the text file buffer when it is dirty. - ADT (ABAP Development Tools) files that are locked in a transport-relevant way must be associated with a transport request before they can be edited. Verify this before applying changes and fail with a clear error when no transport request is assigned. The ADT lock result is read reflectively since the com.sap.adt.* classes are not available at compile time. Add the org.eclipse.core.resources.semantic dependency required for ISemanticFile. --- .../META-INF/MANIFEST.MF | 5 +- .../eclipse/ui/chat/tools/EditFileTool.java | 66 +++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF index dc86d49a..ddd18392 100644 --- a/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF @@ -32,7 +32,7 @@ Require-Bundle: com.microsoft.copilot.eclipse.core;bundle-version="0.20.0", org.eclipse.ui.editors;bundle-version="3.17.100", org.eclipse.ui;bundle-version="3.205.0", org.eclipse.ui.navigator;bundle-version="3.12.200", - org.eclipse.jface.text;bundle-version="3.24.200", + org.eclipse.jface.text;bundle-version="3.24.200", org.eclipse.core.runtime;bundle-version="[3.30.0,4.0.0)", org.eclipse.core.expressions, org.eclipse.jdt.annotation;resolution:=optional, @@ -68,4 +68,5 @@ Require-Bundle: com.microsoft.copilot.eclipse.core;bundle-version="0.20.0", org.eclipse.ui.editors, org.eclipse.debug.core;resolution:=optional, org.eclipse.jdt.core;resolution:=optional, - org.eclipse.jdt.debug;resolution:=optional + org.eclipse.jdt.debug;resolution:=optional, + org.eclipse.core.resources.semantic;bundle-version="0.9.0" diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/EditFileTool.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/EditFileTool.java index 1260b0a1..dd24e58d 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/EditFileTool.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/EditFileTool.java @@ -15,11 +15,17 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; +import org.eclipse.core.filebuffers.FileBuffers; +import org.eclipse.core.filebuffers.LocationKind; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.semantic.ISemanticFile; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.core.runtime.Status; import org.eclipse.lsp4j.FileChangeType; +import org.eclipse.swt.widgets.Display; import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.lsp.protocol.InputSchema; @@ -197,6 +203,8 @@ private void applyChangesToFile(String changedContent, IFile file) throws CoreEx if (!validateEdit(file)) { throw new IllegalStateException("File validation failed for " + file.getFullPath()); } + verifyTransportRequestForAdtLock(file); + ByteArrayInputStream inputStream = getInputStream(changedContent, file); // Set the file contents @@ -207,6 +215,64 @@ private void applyChangesToFile(String changedContent, IFile file) throws CoreEx // Close the input stream inputStream.close(); + + var buffer = FileBuffers.getTextFileBufferManager().getTextFileBuffer(file.getFullPath(), LocationKind.IFILE); + if (buffer != null && buffer.isDirty()) { + // Some editors (e.g. the ABAP source editor) do not listen for changes to the underlying file and + // therefore leave a dirty, out-of-date buffer after we have written the new contents to disk. Force + // the buffer to reload from disk by reverting it, so the open editor reflects the edit we just applied. + Display.getDefault().asyncExec(() -> { + try { + buffer.revert(new NullProgressMonitor()); + } catch (CoreException e) { + CopilotCore.LOGGER.error(e); + } + }); + } + } + + private static final String ADT_LOCK_RESULT_CLASS = "com.sap.adt.tools.core.internal.locking.AdtLockResult"; + private static final QualifiedName ADT_LOCK_RESULT_PROPERTY = new QualifiedName("com.sap.adt.tools.filesystem", + "LockResult"); + + /** + * When an ADT (ABAP Development Tools) file is locked in a transport-relevant way, it must be associated with a + * transport request before it can be edited. The lock result is stored as a session property on the semantic file + * and is accessed reflectively, as the ADT classes are not available at compile time. + * + * @param file the file about to be changed + * @throws CoreException if the file is transport-relevant but has no transport request number assigned + */ + private void verifyTransportRequestForAdtLock(IFile file) throws CoreException { + var semanticFile = file.getAdapter(ISemanticFile.class); + if (semanticFile == null) { + return; + } + Object lockResult = semanticFile.getSessionProperty(ADT_LOCK_RESULT_PROPERTY); + if (lockResult == null || !ADT_LOCK_RESULT_CLASS.equals(lockResult.getClass().getCanonicalName())) { + return; + } + Boolean transportRelevant = readField(lockResult, "transportRelevant", Boolean.class); + if (!Boolean.TRUE.equals(transportRelevant)) { + return; + } + String transportRequestNumber = readField(lockResult, "transportRequestNumber", String.class); + if (transportRequestNumber == null || transportRequestNumber.isEmpty()) { + throw new CoreException(Status.error(String.format( + "Cannot edit %s: the file is transport-relevant but no transport request number is assigned.", + file.getFullPath()))); + } + } + + private T readField(Object target, String fieldName, Class type) { + try { + var field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return type.cast(field.get(target)); + } catch (ReflectiveOperationException | SecurityException e) { + CopilotCore.LOGGER.error("Failed to read field '" + fieldName + "' from " + target.getClass().getName(), e); + return null; + } } private ByteArrayInputStream getInputStream(String changedContent, IFile file) { From 40fe27c042d3495b3044d98457eeacea7dd2ce88 Mon Sep 17 00:00:00 2001 From: Tobias Melcher Date: Mon, 27 Jul 2026 12:59:54 +0200 Subject: [PATCH 2/2] fix: distinguish ADT reflection failure from an unlocked file Address PR review feedback on the ADT transport-lock check: - readField no longer swallows reflection failures; it now propagates ReflectiveOperationException so the caller can tell a genuinely-null field apart from a failed read. - verifyTransportRequestForAdtLock enforces the transport request only when the lock fields are read successfully. If the reflective read fails (e.g. the ADT API changed or setAccessible is denied), the check cannot be performed reliably, so the failure is logged and the edit is allowed to continue instead of either bypassing the check silently or aborting the edit. --- .../eclipse/ui/chat/tools/EditFileTool.java | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/EditFileTool.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/EditFileTool.java index dd24e58d..fc6f7e9b 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/EditFileTool.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/EditFileTool.java @@ -240,6 +240,12 @@ private void applyChangesToFile(String changedContent, IFile file) throws CoreEx * transport request before it can be edited. The lock result is stored as a session property on the semantic file * and is accessed reflectively, as the ADT classes are not available at compile time. * + *

+ * A distinction is made between the lock information being read successfully (in which case the transport + * request is enforced) and the reflective read failing, e.g. because the ADT API changed. In the latter case + * the check cannot be performed reliably, so the failure is logged and the edit is allowed to continue rather + * than blocking the user. + * * @param file the file about to be changed * @throws CoreException if the file is transport-relevant but has no transport request number assigned */ @@ -252,27 +258,30 @@ private void verifyTransportRequestForAdtLock(IFile file) throws CoreException { if (lockResult == null || !ADT_LOCK_RESULT_CLASS.equals(lockResult.getClass().getCanonicalName())) { return; } - Boolean transportRelevant = readField(lockResult, "transportRelevant", Boolean.class); - if (!Boolean.TRUE.equals(transportRelevant)) { - return; - } - String transportRequestNumber = readField(lockResult, "transportRequestNumber", String.class); - if (transportRequestNumber == null || transportRequestNumber.isEmpty()) { - throw new CoreException(Status.error(String.format( - "Cannot edit %s: the file is transport-relevant but no transport request number is assigned.", - file.getFullPath()))); + try { + Boolean transportRelevant = readField(lockResult, "transportRelevant", Boolean.class); + if (!Boolean.TRUE.equals(transportRelevant)) { + return; + } + String transportRequestNumber = readField(lockResult, "transportRequestNumber", String.class); + if (transportRequestNumber == null || transportRequestNumber.isEmpty()) { + throw new CoreException(Status.error(String.format( + "Cannot edit %s: the file is transport-relevant but no transport request number is assigned.", + file.getFullPath()))); + } + } catch (ReflectiveOperationException | RuntimeException e) { + // The lock result is a genuine AdtLockResult, but its fields could not be read reflectively (e.g. the ADT + // API changed or setAccessible was denied). The transport check cannot be performed reliably, so log the + // failure and allow the edit to continue instead of blocking the user. + CopilotCore.LOGGER.error("Could not verify ADT transport lock for " + file.getFullPath() + + "; allowing the edit to continue.", e); } } - private T readField(Object target, String fieldName, Class type) { - try { - var field = target.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - return type.cast(field.get(target)); - } catch (ReflectiveOperationException | SecurityException e) { - CopilotCore.LOGGER.error("Failed to read field '" + fieldName + "' from " + target.getClass().getName(), e); - return null; - } + private T readField(Object target, String fieldName, Class type) throws ReflectiveOperationException { + var field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return type.cast(field.get(target)); } private ByteArrayInputStream getInputStream(String changedContent, IFile file) {