sillytavern-character-memory/editor.js
bal-spec 425df89cd7 feat: add find & replace across all memory editing surfaces
Add find/replace bar to all 5 editing surfaces: Consolidation,
Conversion Preview, Reformat Preview, Data Bank Editor, and
Memory Manager. Includes live match highlighting, case-sensitive
toggle, and undo support in block editor surfaces.

- lib.js: add countMatchesInBlocks() and replaceInBlocks() pure fns
- editor.js: add countMatches() and findAndReplaceAll() with undo
- index.js: buildFindReplaceBar(), wireFindReplaceEvents(), highlightText()
- style.css: find/replace bar, mark highlight, case toggle styles
- test/unit/findreplace.test.js: 19 tests for lib + editor methods

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 22:23:39 -08:00

110 lines
3.3 KiB
JavaScript

import { cloneMemoryBlocks, countMatchesInBlocks, getTimestamp, reindexEditingSet, replaceInBlocks } from './lib.js';
/**
* Create a memory block editor with state management and undo.
* Pure state logic — no DOM. Callers handle rendering and event binding.
*
* @param {object} options
* @param {Array<{chat: string, date: string, bullets: string[]}>} options.blocks - Initial blocks
* @returns {object} Editor API
*/
export function createMemoryEditor({ blocks }) {
let editorBlocks = cloneMemoryBlocks(blocks);
const versionStack = [];
const editingSet = new Set();
function saveVersion() {
versionStack.push(cloneMemoryBlocks(editorBlocks));
}
return {
getBlocks() {
return cloneMemoryBlocks(editorBlocks);
},
deleteBullet(blockIndex, bulletIndex) {
if (!editorBlocks[blockIndex]) return;
saveVersion();
editorBlocks[blockIndex].bullets.splice(bulletIndex, 1);
if (editorBlocks[blockIndex].bullets.length === 0) {
editorBlocks.splice(blockIndex, 1);
reindexEditingSet(editingSet, blockIndex);
}
},
deleteBlock(blockIndex) {
if (!editorBlocks[blockIndex]) return;
saveVersion();
editorBlocks.splice(blockIndex, 1);
reindexEditingSet(editingSet, blockIndex);
},
addBullet(blockIndex) {
if (!editorBlocks[blockIndex]) return;
saveVersion();
editorBlocks[blockIndex].bullets.push('');
editingSet.add(blockIndex);
},
addBlock(timestamp) {
saveVersion();
editorBlocks.push({
chat: 'New Group',
date: timestamp || getTimestamp(),
bullets: [''],
});
editingSet.add(editorBlocks.length - 1);
},
updateBullet(blockIndex, bulletIndex, text) {
if (!editorBlocks[blockIndex]) return;
editorBlocks[blockIndex].bullets[bulletIndex] = text;
},
updateTheme(blockIndex, label) {
if (!editorBlocks[blockIndex]) return;
editorBlocks[blockIndex].chat = label;
},
undo() {
if (versionStack.length === 0) return false;
editorBlocks = versionStack.pop();
return true;
},
canUndo() {
return versionStack.length > 0;
},
replaceAll(newBlocks) {
editorBlocks = cloneMemoryBlocks(newBlocks);
versionStack.length = 0;
editingSet.clear();
},
toggleEdit(blockIndex) {
if (editingSet.has(blockIndex)) {
editingSet.delete(blockIndex);
} else {
editingSet.add(blockIndex);
}
},
isEditing(blockIndex) {
return editingSet.has(blockIndex);
},
getEditingSet() {
return new Set(editingSet);
},
countMatches(find, caseSensitive = false) {
return countMatchesInBlocks(editorBlocks, find, caseSensitive);
},
findAndReplaceAll(find, replace, caseSensitive = false) {
saveVersion();
return { replacements: replaceInBlocks(editorBlocks, find, replace, caseSensitive) };
},
};
}