From 586c7c36c17bfca07e842093190c63ea522b0d8a Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Sun, 31 May 2026 21:20:46 -0400 Subject: [PATCH] ui: Find & Replace, multi-line comments, indent guides, font size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Find & Replace (Ctrl+H): - Two-row find bar: expand › arrow shows replace row (VS Code pattern) - Replace button (Enter): replaces current match, advances to next - Replace All: replaces all occurrences, shows count - Find input searches live; replace row preserved across open/close Multi-line comment toggle (Ctrl+/): - With multi-line selection: checks if ALL lines are commented → if all commented, uncomments all; otherwise comments all - Empty lines in selection are skipped - After toggle: block stays selected (same as VS Code) - Single-line behavior unchanged Indent guides: - CSS repeating-linear-gradient on #vv-editor-inner: 1px guide every 2ch - First guide at indent level 1 (col 2); then 4, 6, 8... matching 2-space indents - Shows through the transparent overlay; scales automatically with font size - Very subtle (0.045 opacity) — visible but not distracting Font size A− / A+ in status bar (9–22px): - Persisted to localStorage, restored on every editor open - Updates textarea, overlay, gutter, and cur-line height together - All scroll calculations use vvLH() (fontSizePx × 1.5) instead of hardcoded 18 — find nav, goto, select-next-occurrence all correct at any font size Also: fixed duplicate id="vv-find-close" in goto bar (now .vv-find-x class) Co-Authored-By: Claude Sonnet 4.6 --- Plugin/unraid/css/varaverk.css | 49 ++++++++-- Plugin/unraid/pages/scheduler.php | 157 ++++++++++++++++++++++++++---- 2 files changed, 176 insertions(+), 30 deletions(-) diff --git a/Plugin/unraid/css/varaverk.css b/Plugin/unraid/css/varaverk.css index 1aded87..774ccff 100644 --- a/Plugin/unraid/css/varaverk.css +++ b/Plugin/unraid/css/varaverk.css @@ -368,24 +368,37 @@ #vv-editor-body::-webkit-scrollbar-thumb { background: #2a2a2a; border-radius: 4px; } #vv-editor-body::-webkit-scrollbar-thumb:hover { background: #444; } -/* Editor find bar */ -#vv-editor-find { display: none; align-items: center; gap: 6px; padding: 5px 8px; +/* Editor find bar — two-row (find + optional replace) */ +#vv-editor-find { display: none; flex-direction: column; gap: 0; padding: 5px 8px; margin-bottom: 5px; background: #161616; border: 1px solid #333; border-radius: 4px; font-size: 12px; } #vv-editor-find.vv-find-open { display: flex; } -#vv-find-input { flex: 1; min-width: 0; background: #0d0d0d; border: 1px solid #333; - color: #ddd; padding: 4px 8px; border-radius: 3px; - font-family: monospace; font-size: 12px; } -#vv-find-input:focus { border-color: #6495ed; outline: none; box-shadow: 0 0 0 1px rgba(100,149,237,0.25); } +.vv-find-row { display: flex; align-items: center; gap: 6px; } +.vv-replace-row { display: none; align-items: center; gap: 6px; + margin-top: 4px; padding-top: 4px; border-top: 1px solid #222; } +.vv-replace-row.vv-repl-open { display: flex; } +.vv-find-expand { font-size: 10px; color: #555; cursor: pointer; flex-shrink: 0; + width: 14px; text-align: center; user-select: none; + transition: transform 0.15s; line-height: 1; } +.vv-find-expand:hover { color: #aaa; } +.vv-find-expand.open { transform: rotate(90deg); color: #888; } +#vv-find-input, #vv-replace-input { + flex: 1; min-width: 0; background: #0d0d0d; border: 1px solid #333; + color: #ddd; padding: 4px 8px; border-radius: 3px; + font-family: monospace; font-size: 12px; } +#vv-find-input:focus, #vv-replace-input:focus { + border-color: #6495ed; outline: none; box-shadow: 0 0 0 1px rgba(100,149,237,0.25); } #vv-find-input.vv-find-no-match { border-color: #c62828; box-shadow: 0 0 0 1px rgba(198,40,40,0.3); } .vv-find-count { font-size: 11px; color: #555; white-space: nowrap; flex-shrink: 0; min-width: 64px; text-align: right; } .vv-find-nav-btn{ padding: 2px 8px !important; } -#vv-find-close { color: #555; cursor: pointer; font-size: 16px; line-height: 1; +.vv-repl-btn { border-color: #444 !important; color: #777 !important; } +.vv-repl-btn:hover { border-color: #777 !important; color: #ddd !important; } +.vv-find-x { color: #555; cursor: pointer; font-size: 16px; line-height: 1; padding: 0 2px; flex-shrink: 0; } -#vv-find-close:hover { color: #aaa; } +.vv-find-x:hover { color: #aaa; } /* Editor status bar */ -#vv-editor-status { display: none; align-items: center; gap: 14px; padding: 3px 10px; +#vv-editor-status { display: none; align-items: center; gap: 10px; padding: 3px 10px; background: #0e0e0e; border: 1px solid #2e2e2e; border-top: none; border-radius: 0 0 4px 4px; font-family: monospace; font-size: 10px; color: #444; user-select: none; flex-shrink: 0; } @@ -393,6 +406,24 @@ .vv-es-pos { color: #666; } .vv-es-sel { color: #555; } .vv-es-lang { margin-left: auto; color: #4a6a7a; } +/* Status bar action buttons */ +.vv-es-btn { background: none; border: none; cursor: pointer; color: #555; + font-size: 10px; padding: 1px 4px; border-radius: 2px; font-family: monospace; } +.vv-es-btn:hover { color: #aaa; background: rgba(255,255,255,0.05); } +.vv-es-btn.active { color: #6495ed; } +#vv-es-fontsize { color: #444; font-size: 10px; min-width: 26px; text-align: center; } + +/* Indent guides — 1px lines every 2 chars, starting at indent level 1 */ +#vv-editor-inner { + position: relative; flex: 1; overflow: hidden; + background-color: #0d0d0d; + background-image: repeating-linear-gradient( + 90deg, + rgba(255,255,255,0.045) 0 1px, + transparent 1px 2ch + ); + background-position: calc(12px + 2ch) 0; +} /* Go to line bar */ #vv-goto-bar { display: none; align-items: center; gap: 8px; padding: 5px 8px; diff --git a/Plugin/unraid/pages/scheduler.php b/Plugin/unraid/pages/scheduler.php index ba8ef36..98d59db 100644 --- a/Plugin/unraid/pages/scheduler.php +++ b/Plugin/unraid/pages/scheduler.php @@ -469,11 +469,13 @@ $runningScripts = array_unique($runningScripts);
  • Editor Keyboard Shortcuts
  • Ctrl+S — save · Ctrl+Z / Ctrl+Y — undo / redo (200 steps)
  • Ctrl+/ — toggle line comment · Tab / Shift+Tab — indent / dedent
  • -
  • Ctrl+F — find · Ctrl+G — go to line · Ctrl+D — select next occurrence
  • -
  • Ctrl+L — select line · Ctrl+Shift+K — delete line
  • +
  • Ctrl+F — find · Ctrl+H — find & replace · Ctrl+G — go to line
  • +
  • Ctrl+D — select next occurrence · Ctrl+L — select line · Ctrl+Shift+K — delete line
  • Alt+↑/↓ — move line · Alt+Shift+↓ — duplicate line below
  • Home — smart (first non-space → col 0)
  • -
  • ( { [ " ` — auto-close pair; wraps selection if text is selected; Backspace deletes both when between them
  • +
  • ( { [ " ` — auto-close pair; wraps selection; Backspace deletes both when between them
  • +
  • A− / A+ in status bar — font size (9–22px, persists across sessions)
  • +
  • Indent guides — faint vertical lines every 2 spaces in the background
  • @@ -883,19 +885,29 @@ Still the same two servers, two households, the same media stack running itself.
    - - - - - × +
    + + + + + + × +
    +
    + + + + +
    Go to line - × + ×
    @@ -912,6 +924,9 @@ Still the same two servers, two households, the same media stack running itself.
    Ln 1, Col 1 + + 12px +
    @@ -997,6 +1012,10 @@ const VV_UNDO_MAX = 200; // max history depth // Auto-close pairs const VV_PAIRS = {'(':')', '[':']', '{':'}', '"':'"', "'":"'", '`':'`'}; const VV_CLOSE = new Set([')', ']', '}', '"', "'", '`']); +// Editor display prefs +let vvFontSizePx = parseInt(localStorage.getItem('vv-editor-fontsize') || '12') || 12; +// Line height helper — used everywhere instead of hardcoded 18 +function vvLH() { return vvFontSizePx * 1.5; } // Setup mode — injected by PHP when navigating from the first-run wizard let vvSetupConf = ; @@ -1613,6 +1632,7 @@ function vvShowEditorMode(title) { document.getElementById('vv-es-lang').textContent = 'Bash'; document.getElementById('vv-undo-btn').style.display = ''; document.getElementById('vv-redo-btn').style.display = ''; + vvRestoreEditorPrefs(); document.getElementById('vv-suggestions').style.display = 'none'; document.getElementById('vv-log-pre').style.display = 'none'; document.getElementById('vv-si-view').style.display = 'none'; @@ -2815,7 +2835,7 @@ function vvUpdateCurLine() { const ta = document.getElementById('vv-editor-body'); const cl = document.getElementById('vv-cur-line'); if (!cl || !ta) return; - const lineH = 18; // font-size(12) × line-height(1.5) + const lineH = vvLH(); const padTop = 10; // overlay padding-top const lineN = ta.value.slice(0, ta.selectionStart).split('\n').length - 1; // 0-indexed cl.style.top = (padTop + lineN * lineH - ta.scrollTop) + 'px'; @@ -2935,7 +2955,7 @@ function vvFindNav(dir) { ta.selectionStart = cur.start; ta.selectionEnd = cur.end; ta.focus(); const lineN = ta.value.slice(0, cur.start).split('\n').length - 1; - ta.scrollTop = Math.max(0, lineN * 18 - 80); + ta.scrollTop = Math.max(0, lineN * vvLH() - 80); vvSyncHlOverlay(); } @@ -2944,6 +2964,85 @@ function vvFindKeydown(e) { if (e.key === 'Escape') { e.preventDefault(); vvFindClose(); } } +// ── Find & Replace ──────────────────────────────────────────────────────────── + +function vvFindToggleReplace(forceOpen) { + const row = document.getElementById('vv-replace-row'); + const btn = document.getElementById('vv-find-expand'); + const show = forceOpen !== undefined ? forceOpen : !row.classList.contains('vv-repl-open'); + row.classList.toggle('vv-repl-open', show); + btn.classList.toggle('open', show); + if (show) document.getElementById('vv-replace-input').focus(); +} + +function vvReplaceOne() { + if (!vvFindMatches.length) return; + const ta = document.getElementById('vv-editor-body'); + const rep = document.getElementById('vv-replace-input').value; + const m = vvFindMatches[vvFindIdx]; + vvUndoCapture(true); + ta.value = ta.value.slice(0, m.start) + rep + ta.value.slice(m.end); + const prevIdx = vvFindIdx; + vvFindSearch(); + if (vvFindMatches.length) { + vvFindIdx = Math.min(prevIdx, vvFindMatches.length - 1); + document.getElementById('vv-find-count').textContent = `${vvFindIdx + 1} / ${vvFindMatches.length}`; + const cur = vvFindMatches[vvFindIdx]; + ta.selectionStart = cur.start; ta.selectionEnd = cur.end; + } + vvSyncHlOverlay(); vvUndoCapture(true); +} + +function vvReplaceAll() { + if (!vvFindTerm) return; + const ta = document.getElementById('vv-editor-body'); + const rep = document.getElementById('vv-replace-input').value; + const re = new RegExp(vvFindTerm.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'), 'gi'); + let n = 0; + vvUndoCapture(true); + ta.value = ta.value.replace(re, () => { n++; return rep; }); + ta.selectionStart = ta.selectionEnd = 0; + vvFindSearch(); + document.getElementById('vv-find-count').textContent = n ? `replaced ${n}` : 'no matches'; + vvUndoCapture(true); +} + +function vvReplaceKeydown(e) { + if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); vvReplaceOne(); } + if (e.key === 'Enter' && e.shiftKey) { e.preventDefault(); vvFindNav(-1); } + if (e.key === 'Escape') { e.preventDefault(); vvFindClose(); } +} + +// ── Font size ───────────────────────────────────────────────────────────────── + +function vvFontSize(delta) { + vvFontSizePx = Math.max(9, Math.min(22, vvFontSizePx + delta)); + localStorage.setItem('vv-editor-fontsize', vvFontSizePx); + const fs = vvFontSizePx + 'px'; + const lh = (vvFontSizePx * 1.5) + 'px'; + const els = ['vv-editor-body', 'vv-hl-overlay', 'vv-ln-gutter']; + els.forEach(id => { + const el = document.getElementById(id); + if (el) { el.style.fontSize = fs; el.style.lineHeight = lh; } + }); + const cl = document.getElementById('vv-cur-line'); + if (cl) cl.style.height = lh; + const fs_el = document.getElementById('vv-es-fontsize'); + if (fs_el) fs_el.textContent = fs; + vvSyncHlOverlay(); vvUpdateCurLine(); vvUpdateEditorStatus(); +} + +function vvRestoreEditorPrefs() { + const saved = parseInt(localStorage.getItem('vv-editor-fontsize') || '12') || 12; + if (saved !== 12) { + vvFontSizePx = 12; // reset so vvFontSize applies the delta correctly + vvFontSize(saved - 12); + } else { + const fs_el = document.getElementById('vv-es-fontsize'); + if (fs_el) fs_el.textContent = '12px'; + } +} + // ── Go to line ──────────────────────────────────────────────────────────────── function vvGotoOpen() { @@ -2969,7 +3068,7 @@ function vvGotoApply() { let pos = 0; for (let i = 0; i < n - 1; i++) pos += lines[i].length + 1; ta.selectionStart = ta.selectionEnd = pos; - ta.scrollTop = Math.max(0, (n - 5) * 18); + ta.scrollTop = Math.max(0, (n - 5) * vvLH()); vvSyncHlOverlay(); vvEditorCursorMoved(); } @@ -3046,7 +3145,7 @@ function vvSelectNextOccurrence() { let m = re.exec(v) ?? (() => { re.lastIndex = 0; return re.exec(v); })(); if (!m || m.index === s) return; ta.selectionStart = m.index; ta.selectionEnd = m.index + word.length; - ta.scrollTop = Math.max(0, (v.slice(0, m.index).split('\n').length - 5) * 18); + ta.scrollTop = Math.max(0, (v.slice(0, m.index).split('\n').length - 5) * vvLH()); vvWordMatch = word.replace(/^\$/, ''); vvSyncHlOverlay(); vvEditorCursorMoved(); } @@ -3054,8 +3153,12 @@ function vvSelectNextOccurrence() { function vvEditorReset() { vvWordMatch = null; vvFindTerm = ''; vvFindMatches = []; vvFindIdx = 0; document.getElementById('vv-editor-find')?.classList.remove('vv-find-open'); + document.getElementById('vv-replace-row')?.classList.remove('vv-repl-open'); + document.getElementById('vv-find-expand')?.classList.remove('open'); const inp = document.getElementById('vv-find-input'); if (inp) { inp.value = ''; inp.classList.remove('vv-find-no-match'); } + const rep = document.getElementById('vv-replace-input'); + if (rep) rep.value = ''; const cnt = document.getElementById('vv-find-count'); if (cnt) cnt.textContent = ''; document.getElementById('vv-goto-bar')?.classList.remove('vv-goto-open'); @@ -3164,6 +3267,7 @@ function vvEditorKeydown(e) { if (e.key === 'y' || (e.key === 'z' && e.shiftKey)) { e.preventDefault(); vvRedo(); return; } if (e.key === 's') { e.preventDefault(); if (vvRawConfFile) vvSaveRawConf(); else vvSaveScript(); return; } if (e.key === 'f') { e.preventDefault(); vvFindOpen(); return; } + if (e.key === 'h') { e.preventDefault(); vvFindOpen(); vvFindToggleReplace(true); return; } if (e.key === 'g') { e.preventDefault(); vvGotoOpen(); return; } if (e.key === 'l' && !e.shiftKey) { // Select current line @@ -3190,14 +3294,25 @@ function vvEditorKeydown(e) { vvSyncHlOverlay(); vvUndoCapture(true); return; } if (e.key === '/') { - // Toggle line comment + // Toggle line comment — single line or entire multi-line selection e.preventDefault(); vvUndoCapture(true); - const ls = val.lastIndexOf('\n', start - 1) + 1; - const le = val.indexOf('\n', start); - const ln = val.slice(ls, le === -1 ? undefined : le); - const nl = /^\s*#/.test(ln) ? ln.replace(/^(\s*)#\s?/, '$1') : ln.replace(/^(\s*)/, '$1# '); - ta.value = val.slice(0, ls) + nl + val.slice(le === -1 ? val.length : le); - ta.selectionStart = ta.selectionEnd = start + (nl.length - ln.length); + const bs = val.lastIndexOf('\n', start - 1) + 1; + const be = end > start ? val.indexOf('\n', end - 1) : val.indexOf('\n', start); + const block = val.slice(bs, be === -1 ? val.length : be); + const lines = block.split('\n'); + const allCommented = lines.every(l => !l.trim() || /^\s*#/.test(l)); + const newBlock = lines.map(l => + !l.trim() ? l + : allCommented ? l.replace(/^(\s*)#\s?/, '$1') + : l.replace(/^(\s*)/, '$1# ') + ).join('\n'); + ta.value = val.slice(0, bs) + newBlock + val.slice(be === -1 ? val.length : be); + if (end > start) { + ta.selectionStart = bs; ta.selectionEnd = bs + newBlock.length; // keep block selected + } else { + const curLineDelta = newBlock.split('\n')[0].length - lines[0].length; + ta.selectionStart = ta.selectionEnd = start + curLineDelta; + } vvSyncHlOverlay(); vvUndoCapture(true); return; } return; // let browser handle other Ctrl combos