feat: v0.8.0 edit and save files with conflict detection
- devdesk attach --fs-write SESSION PATH BYTES BASESHA|new: streamed like uploads, temp file in the same folder, sha re-check (conflict reply instead of overwriting), mode/owner kept, atomic mv; new files with folders, never outside the session folder or through a symlink out of it. - Editor: editable CodeMirror for UTF-8 text, dirty tracking, Ctrl+S, Save/Revert, conflict dialog with a disk-vs-yours diff, changed-on-disk banner, drafts in localStorage (flushed on pagehide), close/leave guards, New file. - Upload streaming factored into streamIn(), shared by uploads and saves. Closes #20 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+30
-2
@@ -4,7 +4,7 @@
|
||||
// the first time the Files pane opens, so the page itself stays small.
|
||||
import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter, drawSelection, highlightSpecialChars } from '@codemirror/view';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
||||
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
|
||||
import { syntaxHighlighting, HighlightStyle, StreamLanguage, bracketMatching, foldGutter, foldKeymap } from '@codemirror/language';
|
||||
import { search, searchKeymap, highlightSelectionMatches } from '@codemirror/search';
|
||||
import { markdownLanguage } from '@codemirror/lang-markdown';
|
||||
@@ -135,12 +135,40 @@ function baseExtensions(path, { readOnly = true, wrap = false } = {}) {
|
||||
];
|
||||
}
|
||||
|
||||
// Read-only (v1) view of a file.
|
||||
// Read-only view of a file.
|
||||
export function viewer(parent, { doc, path, wrap }) {
|
||||
const view = new EditorView({ parent, state: EditorState.create({ doc, extensions: baseExtensions(path, { wrap }) }) });
|
||||
return { view, destroy: () => view.destroy() };
|
||||
}
|
||||
|
||||
// Editable file (devdesk#20): onChange(text) after every edit, onSave() on
|
||||
// Ctrl/Cmd+S (which never reaches the browser or the terminal).
|
||||
export function editor(parent, { doc, path, wrap, onChange, onSave }) {
|
||||
const view = new EditorView({
|
||||
parent,
|
||||
state: EditorState.create({
|
||||
doc,
|
||||
extensions: [
|
||||
keymap.of([{ key: 'Mod-s', preventDefault: true, run: () => { onSave?.(); return true; } }, indentWithTab]),
|
||||
baseExtensions(path, { readOnly: false, wrap }),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) onChange?.(update.state.doc.toString());
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
return {
|
||||
view,
|
||||
text: () => view.state.doc.toString(),
|
||||
// Replace the whole document (reload from disk) keeping the cursor roughly where it was.
|
||||
setText: (text) => {
|
||||
const pos = Math.min(view.state.selection.main.head, text.length);
|
||||
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: text }, selection: { anchor: pos } });
|
||||
},
|
||||
destroy: () => view.destroy(),
|
||||
};
|
||||
}
|
||||
|
||||
// Unified diff of the HEAD version against the working copy.
|
||||
export function diff(parent, { original, modified, path, wrap }) {
|
||||
const view = new EditorView({
|
||||
|
||||
Reference in New Issue
Block a user