From 292dc3cade353ae14e5e45688617f9eeed2618c0 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Thu, 5 Nov 2026 22:04:49 +0800 Subject: [PATCH] fix(editor): log why an edit was rejected before the editor drops it (#1569) When the backend rejects an edit for any reason other than a stale version, the editor replaces its unsaved text with the file's content, but the reason was only printed to the webview console. The backend now logs the rejected edit with its path, and the editor writes the sync error and the discarded edits to the log file, so reports of edits that roll back (#1530, #1522) can be diagnosed from the log. --- crates/ide-workspace/src/documents.rs | 8 +++++++- src/editor/core/model/document.ts | 9 ++++++--- src/editor/core/sync/DocSync.test.ts | 3 ++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/crates/ide-workspace/src/documents.rs b/crates/ide-workspace/src/documents.rs index 7bb741991..7ecd16a38 100644 --- a/crates/ide-workspace/src/documents.rs +++ b/crates/ide-workspace/src/documents.rs @@ -537,9 +537,15 @@ impl Documents { pub fn edit(&self, doc: DocId, base_version: u32, changes: ChangeSet) -> IdeResult { self.with_doc(doc, |st| { if st.read_only { + tracing::warn!("edit rejected: {} is read-only", st.path.display()); return Err(read_only_error(&st.path)); } - let (applied, edits) = st.doc.apply_detailed(base_version, &changes)?; + let (applied, edits) = st.doc.apply_detailed(base_version, &changes).inspect_err(|e| { + // A stale version is routine (the UI rebases); anything else makes the editor drop the edit. + if e.kind != ErrorKind::OutOfSync { + tracing::warn!("edit of {} rejected: {e}", st.path.display()); + } + })?; self.set_dirty(doc, st, true); self.notify_changed(doc, st, &applied, &edits); Ok(st.doc.version()) diff --git a/src/editor/core/model/document.ts b/src/editor/core/model/document.ts index b017024d9..95abb33b8 100644 --- a/src/editor/core/model/document.ts +++ b/src/editor/core/model/document.ts @@ -62,6 +62,7 @@ import { import { setMethodLines, softWrapActive } from "../cm/prefs"; import type { EditorSlot } from "./slot"; import { tx } from "@/platform/i18n"; +import { logError } from "@/platform/errorLog"; import { WorkspaceHistory } from "./workspaceHistory"; export type Banner = "binary" | "tooLarge" | "readOnly" | "library" | null; @@ -263,13 +264,15 @@ export class DocModel { }, { applyRemote: (cs, grouped) => this.applyRemote(cs, grouped), - onDiscarded: () => + onDiscarded: (reason) => { + logError(`document ${this.path}: unsaved edits replaced by the file's content`, reason); manager.notify({ level: "warning", title: "Editor resynchronised", body: tx`Some unsaved edits to ${this.path} could not be applied and were replaced by the file's current content.`, - }), - onError: (e) => console.warn(`document ${this.path}: sync error`, e), + }); + }, + onError: (e) => logError(`document ${this.path}: sync error`, e), }, manager.scheduler, ); diff --git a/src/editor/core/sync/DocSync.test.ts b/src/editor/core/sync/DocSync.test.ts index 2fa7933df..7b0e53c0f 100644 --- a/src/editor/core/sync/DocSync.test.ts +++ b/src/editor/core/sync/DocSync.test.ts @@ -226,7 +226,8 @@ describe("DocSync", () => { await server.processAll(); await flushMicrotasks(); expect(view.text.toString()).toBe("abc"); - expect(host.onDiscarded).toHaveBeenCalled(); + // The rejection reaches the host, which writes it to the log file. + expect(host.onDiscarded).toHaveBeenCalledWith(expect.objectContaining({ kind: "permissionDenied" })); expect(sync.idle).toBe(true); });