дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
Multi-file refactorings could drop LSP document versions, overwrite content changed after a preview, or leave earlier files saved after a later write failed. Workspace edits now keep server versions and preview fingerprints, are validated on a staging copy, snapshot disk and open buffers, and roll back on commit failure (retaining recovery files if rollback fails). The whole edit is one Undo/Redo step in editors and in the notification. Version reads and transaction work run off the async workers. Closes #461. Co-authored-by: shohruh-genius <[email protected]> Co-Authored-By: Claude Opus 5.5 <[email protected]>
89 рядки
2.8 KiB
TypeScript
89 рядки
2.8 KiB
TypeScript
/**
|
|
* Documents shown by views outside the tab system (the editable side of a
|
|
* diff): local edits flow view → model → backend and other editors; edits
|
|
* from elsewhere are mirrored into the view. See `attachDocument` in editor/api.
|
|
*/
|
|
import { Transaction, type Annotation, type ChangeSet } from "@codemirror/state";
|
|
import { EditorView, ViewPlugin } from "@codemirror/view";
|
|
import {
|
|
_addEditor,
|
|
_removeEditor,
|
|
documentSynced,
|
|
isExternalChange,
|
|
remoteChange,
|
|
type DocumentAttachment,
|
|
type TextEditorHandle,
|
|
} from "@/editor/api";
|
|
import type { DocAttachmentTarget, DocumentManager } from "./document";
|
|
|
|
class AttachedView implements DocAttachmentTarget {
|
|
view: EditorView | null = null;
|
|
|
|
applyExternal(changes: ChangeSet, annotations: Annotation<unknown>[]) {
|
|
this.view?.dispatch({ changes, annotations });
|
|
}
|
|
}
|
|
|
|
export async function attachDocument(manager: DocumentManager, path: string): Promise<DocumentAttachment> {
|
|
const model = await manager.acquire(path);
|
|
const target = new AttachedView();
|
|
model.attach(target);
|
|
let released = false;
|
|
const historyTarget = {
|
|
path,
|
|
state: () => target.view!.state,
|
|
dispatch: (tr: Transaction) => target.view?.dispatch(tr),
|
|
};
|
|
|
|
const plugin = ViewPlugin.define((view) => {
|
|
target.view = view;
|
|
let detachHistory = () => {};
|
|
let destroyed = false;
|
|
queueMicrotask(() => {
|
|
if (!destroyed) detachHistory = model.manager.workspaceHistory.attach(historyTarget);
|
|
});
|
|
const handle: TextEditorHandle = { view, ctx: model.ctx() };
|
|
_addEditor(handle);
|
|
// The model may have moved on between `text()` and the view's creation.
|
|
const current = model.sync.localText;
|
|
if (!view.state.doc.eq(current))
|
|
queueMicrotask(() => {
|
|
if (target.view !== view) return;
|
|
view.dispatch({
|
|
changes: { from: 0, to: view.state.doc.length, insert: current },
|
|
annotations: [remoteChange.of(true), Transaction.addToHistory.of(false)],
|
|
});
|
|
});
|
|
return {
|
|
destroy() {
|
|
destroyed = true;
|
|
detachHistory();
|
|
_removeEditor(handle);
|
|
if (target.view === view) target.view = null;
|
|
},
|
|
};
|
|
});
|
|
|
|
return {
|
|
path,
|
|
text: () => model.sync.localText.toString(),
|
|
readOnly: model.getMeta().readOnly || model.virtual,
|
|
extension: [
|
|
model.manager.workspaceHistory.extension(historyTarget),
|
|
plugin,
|
|
documentSynced.of(() => model.sync.whenIdle()),
|
|
EditorView.updateListener.of((u) => {
|
|
if (released || model.closed) return;
|
|
for (const tr of u.transactions) if (tr.docChanged && !isExternalChange(tr)) model.onLocal(tr.changes, target);
|
|
}),
|
|
],
|
|
save: () => model.save("explicit"),
|
|
release: () => {
|
|
if (released) return;
|
|
released = true;
|
|
target.view = null;
|
|
model.detach(target);
|
|
},
|
|
};
|
|
}
|