ui: editor QoL — line ops, auto-close, goto, smart home, Ctrl+D
Line operations:
- Alt+↑/↓ — move current line up / down (swaps with adjacent line)
- Alt+Shift+↓/↑ — duplicate line below / above
- Ctrl+Shift+K — delete current line
- Ctrl+L — select entire current line (including newline)
Auto-close brackets and quotes:
- ( [ { " ' ` — inserts matching close, cursor placed inside
- Wrap selection — if text is selected, typing an opener wraps it
- Skip-over — typing a close char when it's already next → jumps past it
- Smart backspace — deletes BOTH chars when cursor sits between a matched pair
- Guards: skip ' auto-close after word chars (contractions); skip auto-close
when next char is a word char (prevents disrupting existing text)
Navigation:
- Ctrl+G — Go to Line bar (pre-filled with current line, shows "of N")
- Smart Home — first press → first non-whitespace; second → col 0
- Ctrl+D — first press: select word at cursor; subsequent: cycle through
all occurrences (wraps around), integrates with word highlight overlay
Keyboard shortcuts panel:
- Added "Editor Keyboard Shortcuts" section to the How do I use this panel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
1ea3f595cc
commit
beac98427d
@@ -394,6 +394,16 @@
|
||||
.vv-es-sel { color: #555; }
|
||||
.vv-es-lang { margin-left: auto; color: #4a6a7a; }
|
||||
|
||||
/* Go to line bar */
|
||||
#vv-goto-bar { display: none; align-items: center; gap: 8px; padding: 5px 8px;
|
||||
margin-bottom: 5px; background: #161616; border: 1px solid #333;
|
||||
border-radius: 4px; font-size: 12px; }
|
||||
#vv-goto-bar.vv-goto-open { display: flex; }
|
||||
#vv-goto-input { width: 68px; background: #0d0d0d; border: 1px solid #333; color: #ddd;
|
||||
padding: 4px 8px; border-radius: 3px; font-family: monospace; font-size: 12px; }
|
||||
#vv-goto-input:focus { border-color: #6495ed; outline: none; }
|
||||
.vv-goto-info { font-size: 11px; color: #555; white-space: nowrap; flex-shrink: 0; }
|
||||
|
||||
/* Undo / Redo buttons */
|
||||
.vv-undo-redo-btn { border-color: #444 !important; color: #666 !important; }
|
||||
.vv-undo-redo-btn:not([disabled]):hover { border-color: #777 !important; color: #ccc !important; background: #1e1e1e !important; }
|
||||
|
||||
@@ -466,6 +466,14 @@ $runningScripts = array_unique($runningScripts);
|
||||
<li><strong>Child (orch OFF)</strong> — enter a cron to run it standalone</li>
|
||||
<li><strong>Rsync badge</strong> — toggle writes TIER_RSYNC_ENABLED directly to master.conf</li>
|
||||
<li><strong>Rsync (orch OFF)</strong> — fill location + standalone cron + Save; both required for independent firing</li>
|
||||
<li class="vv-info-sep">Editor Keyboard Shortcuts</li>
|
||||
<li><strong>Ctrl+S</strong> — save · <strong>Ctrl+Z / Ctrl+Y</strong> — undo / redo (200 steps)</li>
|
||||
<li><strong>Ctrl+/</strong> — toggle line comment · <strong>Tab / Shift+Tab</strong> — indent / dedent</li>
|
||||
<li><strong>Ctrl+F</strong> — find · <strong>Ctrl+G</strong> — go to line · <strong>Ctrl+D</strong> — select next occurrence</li>
|
||||
<li><strong>Ctrl+L</strong> — select line · <strong>Ctrl+Shift+K</strong> — delete line</li>
|
||||
<li><strong>Alt+↑/↓</strong> — move line · <strong>Alt+Shift+↓</strong> — duplicate line below</li>
|
||||
<li><strong>Home</strong> — smart (first non-space → col 0)</li>
|
||||
<li><strong>( { [ " ` </strong> — auto-close pair; wraps selection if text is selected; <strong>Backspace</strong> deletes both when between them</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -882,6 +890,13 @@ Still the same two servers, two households, the same media stack running itself.
|
||||
<button class="vv-btn-sm vv-find-nav-btn" onclick="vvFindNav(1)" title="Next (Enter)">↓</button>
|
||||
<span id="vv-find-close" onclick="vvFindClose()" title="Close (Esc)">×</span>
|
||||
</div>
|
||||
<div id="vv-goto-bar">
|
||||
<span style="color:#666;font-size:12px;flex-shrink:0">Go to line</span>
|
||||
<input type="number" id="vv-goto-input" min="1" placeholder="#"
|
||||
oninput="vvGotoApply()" onkeydown="vvGotoKeydown(event)">
|
||||
<span class="vv-goto-info" id="vv-goto-info"></span>
|
||||
<span id="vv-find-close" onclick="vvGotoClose()" title="Close (Esc)">×</span>
|
||||
</div>
|
||||
<div id="vv-editor-wrap">
|
||||
<pre id="vv-ln-gutter" aria-hidden="true"></pre>
|
||||
<div id="vv-editor-inner">
|
||||
@@ -979,6 +994,9 @@ let vvUndoStack = []; // [{v,ss,se}] snapshots — v=value, ss=selStart, s
|
||||
let vvRedoStack = [];
|
||||
let vvUndoTimer = null;
|
||||
const VV_UNDO_MAX = 200; // max history depth
|
||||
// Auto-close pairs
|
||||
const VV_PAIRS = {'(':')', '[':']', '{':'}', '"':'"', "'":"'", '`':'`'};
|
||||
const VV_CLOSE = new Set([')', ']', '}', '"', "'", '`']);
|
||||
|
||||
// Setup mode — injected by PHP when navigating from the first-run wizard
|
||||
let vvSetupConf = <?= json_encode($vv_setup_conf) ?>;
|
||||
@@ -2926,14 +2944,121 @@ function vvFindKeydown(e) {
|
||||
if (e.key === 'Escape') { e.preventDefault(); vvFindClose(); }
|
||||
}
|
||||
|
||||
// ── Go to line ────────────────────────────────────────────────────────────────
|
||||
|
||||
function vvGotoOpen() {
|
||||
const bar = document.getElementById('vv-goto-bar');
|
||||
if (!bar) return;
|
||||
bar.classList.add('vv-goto-open');
|
||||
const ta = document.getElementById('vv-editor-body');
|
||||
const inp = document.getElementById('vv-goto-input');
|
||||
inp.value = ta.value.slice(0, ta.selectionStart).split('\n').length;
|
||||
document.getElementById('vv-goto-info').textContent = 'of ' + ta.value.split('\n').length;
|
||||
inp.focus(); inp.select();
|
||||
}
|
||||
|
||||
function vvGotoClose() {
|
||||
document.getElementById('vv-goto-bar').classList.remove('vv-goto-open');
|
||||
document.getElementById('vv-editor-body').focus();
|
||||
}
|
||||
|
||||
function vvGotoApply() {
|
||||
const ta = document.getElementById('vv-editor-body');
|
||||
const lines = ta.value.split('\n');
|
||||
const n = Math.max(1, Math.min(parseInt(document.getElementById('vv-goto-input').value) || 1, lines.length));
|
||||
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);
|
||||
vvSyncHlOverlay();
|
||||
vvEditorCursorMoved();
|
||||
}
|
||||
|
||||
function vvGotoKeydown(e) {
|
||||
if (e.key === 'Enter') { e.preventDefault(); vvGotoApply(); vvGotoClose(); }
|
||||
if (e.key === 'Escape') { e.preventDefault(); vvGotoClose(); }
|
||||
}
|
||||
|
||||
// ── Line operations ───────────────────────────────────────────────────────────
|
||||
|
||||
function vvMoveLine(dir) {
|
||||
const ta = document.getElementById('vv-editor-body');
|
||||
const v = ta.value, s = ta.selectionStart;
|
||||
vvUndoCapture(true);
|
||||
const ls = v.lastIndexOf('\n', s - 1) + 1;
|
||||
const le = v.indexOf('\n', s);
|
||||
if (dir === -1) {
|
||||
if (ls === 0) return;
|
||||
const pls = v.lastIndexOf('\n', ls - 2) + 1;
|
||||
const prev = v.slice(pls, ls - 1), cur = v.slice(ls, le === -1 ? v.length : le);
|
||||
ta.value = v.slice(0, pls) + cur + '\n' + prev + v.slice(le === -1 ? v.length : le);
|
||||
ta.selectionStart = ta.selectionEnd = pls + (s - ls);
|
||||
} else {
|
||||
if (le === -1) return;
|
||||
const nle = v.indexOf('\n', le + 1);
|
||||
const next = v.slice(le + 1, nle === -1 ? v.length : nle), cur = v.slice(ls, le);
|
||||
ta.value = v.slice(0, ls) + next + '\n' + cur + v.slice(nle === -1 ? v.length : nle);
|
||||
ta.selectionStart = ta.selectionEnd = ls + next.length + 1 + (s - ls);
|
||||
}
|
||||
vvSyncHlOverlay(); vvUndoCapture(true); vvEditorCursorMoved();
|
||||
}
|
||||
|
||||
function vvDuplicateLine(above) {
|
||||
const ta = document.getElementById('vv-editor-body');
|
||||
const v = ta.value, s = ta.selectionStart;
|
||||
vvUndoCapture(true);
|
||||
const ls = v.lastIndexOf('\n', s - 1) + 1;
|
||||
const le = v.indexOf('\n', s);
|
||||
const cur = v.slice(ls, le === -1 ? v.length : le);
|
||||
if (above) {
|
||||
ta.value = v.slice(0, ls) + cur + '\n' + v.slice(ls);
|
||||
ta.selectionStart = ta.selectionEnd = ls + cur.length + 1 + (s - ls);
|
||||
} else {
|
||||
const ins = le === -1 ? v.length : le;
|
||||
ta.value = v.slice(0, ins) + '\n' + cur + v.slice(ins);
|
||||
ta.selectionStart = ta.selectionEnd = ins + 1 + (s - ls);
|
||||
}
|
||||
vvSyncHlOverlay(); vvUndoCapture(true); vvEditorCursorMoved();
|
||||
}
|
||||
|
||||
// ── Select next occurrence (Ctrl+D) ──────────────────────────────────────────
|
||||
|
||||
function vvSelectNextOccurrence() {
|
||||
const ta = document.getElementById('vv-editor-body');
|
||||
const v = ta.value, s = ta.selectionStart, e = ta.selectionEnd;
|
||||
let word;
|
||||
if (e > s) {
|
||||
word = v.slice(s, e);
|
||||
} else {
|
||||
const wb = v.slice(0, s).match(/[\w$]+$/) ?? [''];
|
||||
const wa = v.slice(s).match(/^[\w]+/) ?? [''];
|
||||
word = wb[0] + wa[0];
|
||||
if (word.length < 1) return;
|
||||
// First call: just select the word at cursor
|
||||
const ws = s - wb[0].length;
|
||||
ta.selectionStart = ws; ta.selectionEnd = ws + word.length;
|
||||
vvWordMatch = word.replace(/^\$/, '');
|
||||
vvSyncHlOverlay(); vvEditorCursorMoved(); return;
|
||||
}
|
||||
if (!word) return;
|
||||
const re = new RegExp(word.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'), 'g');
|
||||
re.lastIndex = e;
|
||||
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);
|
||||
vvWordMatch = word.replace(/^\$/, '');
|
||||
vvSyncHlOverlay(); vvEditorCursorMoved();
|
||||
}
|
||||
|
||||
function vvEditorReset() {
|
||||
vvWordMatch = null; vvFindTerm = ''; vvFindMatches = []; vvFindIdx = 0;
|
||||
const bar = document.getElementById('vv-editor-find');
|
||||
if (bar) { bar.classList.remove('vv-find-open'); }
|
||||
document.getElementById('vv-editor-find')?.classList.remove('vv-find-open');
|
||||
const inp = document.getElementById('vv-find-input');
|
||||
if (inp) { inp.value = ''; inp.classList.remove('vv-find-no-match'); }
|
||||
const cnt = document.getElementById('vv-find-count');
|
||||
if (cnt) cnt.textContent = '';
|
||||
document.getElementById('vv-goto-bar')?.classList.remove('vv-goto-open');
|
||||
vvUndoReset();
|
||||
}
|
||||
|
||||
@@ -3033,64 +3158,91 @@ function vvEditorKeydown(e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl/Cmd+Z — undo
|
||||
if (e.key === 'z' && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
||||
e.preventDefault(); vvUndo(); return;
|
||||
}
|
||||
// Ctrl/Cmd+Y or Ctrl/Cmd+Shift+Z — redo
|
||||
if ((e.key === 'y' && (e.ctrlKey || e.metaKey)) ||
|
||||
(e.key === 'z' && (e.ctrlKey || e.metaKey) && e.shiftKey)) {
|
||||
e.preventDefault(); vvRedo(); return;
|
||||
}
|
||||
|
||||
// Ctrl/Cmd+F — open find bar
|
||||
if (e.key === 'f' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
vvFindOpen();
|
||||
return;
|
||||
}
|
||||
|
||||
// Escape — close find bar if open
|
||||
if (e.key === 'Escape') {
|
||||
const bar = document.getElementById('vv-editor-find');
|
||||
if (bar && bar.classList.contains('vv-find-open')) {
|
||||
e.preventDefault(); vvFindClose(); return;
|
||||
// ── Ctrl/Cmd shortcuts ────────────────────────────────────────────────────
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
if (e.key === 'z' && !e.shiftKey) { e.preventDefault(); vvUndo(); return; }
|
||||
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 === 'g') { e.preventDefault(); vvGotoOpen(); return; }
|
||||
if (e.key === 'l' && !e.shiftKey) {
|
||||
// Select current line
|
||||
e.preventDefault();
|
||||
const ls = val.lastIndexOf('\n', start - 1) + 1;
|
||||
const le = val.indexOf('\n', start);
|
||||
ta.selectionStart = ls;
|
||||
ta.selectionEnd = le === -1 ? val.length : le + 1;
|
||||
vvEditorCursorMoved(); return;
|
||||
}
|
||||
if (e.key === 'd' && !e.shiftKey) { e.preventDefault(); vvSelectNextOccurrence(); return; }
|
||||
if (e.key === 'k' && e.shiftKey) {
|
||||
// Delete current line
|
||||
e.preventDefault(); vvUndoCapture(true);
|
||||
const ls = val.lastIndexOf('\n', start - 1) + 1;
|
||||
const le = val.indexOf('\n', start);
|
||||
if (le === -1) {
|
||||
ta.value = ls > 0 ? val.slice(0, ls - 1) : '';
|
||||
ta.selectionStart = ta.selectionEnd = Math.max(0, ls - 1);
|
||||
} else {
|
||||
ta.value = val.slice(0, ls) + val.slice(le + 1);
|
||||
ta.selectionStart = ta.selectionEnd = ls;
|
||||
}
|
||||
vvSyncHlOverlay(); vvUndoCapture(true); return;
|
||||
}
|
||||
if (e.key === '/') {
|
||||
// Toggle line comment
|
||||
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);
|
||||
vvSyncHlOverlay(); vvUndoCapture(true); return;
|
||||
}
|
||||
return; // let browser handle other Ctrl combos
|
||||
}
|
||||
|
||||
// Ctrl/Cmd+/ — toggle line comment
|
||||
if (e.key === '/' && (e.ctrlKey || e.metaKey)) {
|
||||
// ── Escape ─────────────────────────────────────────────────────────────────
|
||||
if (e.key === 'Escape') {
|
||||
const fb = document.getElementById('vv-editor-find');
|
||||
if (fb?.classList.contains('vv-find-open')) { e.preventDefault(); vvFindClose(); return; }
|
||||
const gb = document.getElementById('vv-goto-bar');
|
||||
if (gb?.classList.contains('vv-goto-open')) { e.preventDefault(); vvGotoClose(); return; }
|
||||
}
|
||||
|
||||
// ── Alt shortcuts (line moves) ─────────────────────────────────────────────
|
||||
if (e.altKey) {
|
||||
if (e.key === 'ArrowUp' && !e.shiftKey) { e.preventDefault(); vvMoveLine(-1); return; }
|
||||
if (e.key === 'ArrowDown' && !e.shiftKey) { e.preventDefault(); vvMoveLine(1); return; }
|
||||
if (e.key === 'ArrowDown' && e.shiftKey) { e.preventDefault(); vvDuplicateLine(false); return; }
|
||||
if (e.key === 'ArrowUp' && e.shiftKey) { e.preventDefault(); vvDuplicateLine(true); return; }
|
||||
}
|
||||
|
||||
// ── Smart Home ─────────────────────────────────────────────────────────────
|
||||
if (e.key === 'Home' && !e.altKey && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
vvUndoCapture(true); // snapshot before edit
|
||||
const lineStart = val.lastIndexOf('\n', start - 1) + 1;
|
||||
const lineEnd = val.indexOf('\n', start);
|
||||
const line = val.slice(lineStart, lineEnd === -1 ? undefined : lineEnd);
|
||||
const newLine = /^\s*#/.test(line)
|
||||
? line.replace(/^(\s*)#\s?/, '$1')
|
||||
: line.replace(/^(\s*)/, '$1# ');
|
||||
const delta = newLine.length - line.length;
|
||||
ta.value = val.slice(0, lineStart) + newLine + val.slice(lineEnd === -1 ? val.length : lineEnd);
|
||||
ta.selectionStart = ta.selectionEnd = start + delta;
|
||||
vvSyncHlOverlay(); vvUndoCapture(true); return;
|
||||
const ls = val.lastIndexOf('\n', start - 1) + 1;
|
||||
const fns = ls + val.slice(ls).match(/^(\s*)/)[1].length;
|
||||
ta.selectionStart = ta.selectionEnd = start === fns ? ls : fns;
|
||||
vvEditorCursorMoved(); return;
|
||||
}
|
||||
|
||||
// Tab / Shift+Tab — indent / dedent (2 spaces)
|
||||
// ── Tab / Shift+Tab ────────────────────────────────────────────────────────
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
vvUndoCapture(true); // snapshot before edit
|
||||
e.preventDefault(); vvUndoCapture(true);
|
||||
if (end > start && val.slice(start, end).includes('\n')) {
|
||||
const blockStart = val.lastIndexOf('\n', start - 1) + 1;
|
||||
const blockEnd = val.indexOf('\n', end - 1);
|
||||
const block = val.slice(blockStart, blockEnd === -1 ? val.length : blockEnd);
|
||||
const newBlock = e.shiftKey ? block.replace(/^ /gm, '') : block.replace(/^/gm, ' ');
|
||||
ta.value = val.slice(0, blockStart) + newBlock + val.slice(blockEnd === -1 ? val.length : blockEnd);
|
||||
ta.selectionStart = blockStart;
|
||||
ta.selectionEnd = blockStart + newBlock.length;
|
||||
const bs = val.lastIndexOf('\n', start - 1) + 1;
|
||||
const be = val.indexOf('\n', end - 1);
|
||||
const blk = val.slice(bs, be === -1 ? val.length : be);
|
||||
const nb = e.shiftKey ? blk.replace(/^ /gm, '') : blk.replace(/^/gm, ' ');
|
||||
ta.value = val.slice(0, bs) + nb + val.slice(be === -1 ? val.length : be);
|
||||
ta.selectionStart = bs; ta.selectionEnd = bs + nb.length;
|
||||
} else if (e.shiftKey) {
|
||||
const lineStart = val.lastIndexOf('\n', start - 1) + 1;
|
||||
if (val.slice(lineStart, lineStart + 2) === ' ') {
|
||||
ta.value = val.slice(0, lineStart) + val.slice(lineStart + 2);
|
||||
ta.selectionStart = ta.selectionEnd = Math.max(lineStart, start - 2);
|
||||
const ls = val.lastIndexOf('\n', start - 1) + 1;
|
||||
if (val.slice(ls, ls + 2) === ' ') {
|
||||
ta.value = val.slice(0, ls) + val.slice(ls + 2);
|
||||
ta.selectionStart = ta.selectionEnd = Math.max(ls, start - 2);
|
||||
}
|
||||
} else {
|
||||
ta.value = val.slice(0, start) + ' ' + val.slice(end);
|
||||
@@ -3099,19 +3251,51 @@ function vvEditorKeydown(e) {
|
||||
vvSyncHlOverlay(); vvUndoCapture(true); return;
|
||||
}
|
||||
|
||||
// Enter — auto-indent
|
||||
// ── Enter auto-indent ──────────────────────────────────────────────────────
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
vvUndoCapture(true); // snapshot before edit
|
||||
const lineStart = val.lastIndexOf('\n', start - 1) + 1;
|
||||
const currentLine = val.slice(lineStart, start);
|
||||
const indent = currentLine.match(/^(\s*)/)[1];
|
||||
const extra = /\{\s*$/.test(currentLine) ? ' ' : '';
|
||||
const insert = '\n' + indent + extra;
|
||||
ta.value = val.slice(0, start) + insert + val.slice(end);
|
||||
ta.selectionStart = ta.selectionEnd = start + insert.length;
|
||||
e.preventDefault(); vvUndoCapture(true);
|
||||
const ls = val.lastIndexOf('\n', start - 1) + 1;
|
||||
const curL = val.slice(ls, start);
|
||||
const ind = curL.match(/^(\s*)/)[1];
|
||||
const xtra = /\{\s*$/.test(curL) ? ' ' : '';
|
||||
const ins = '\n' + ind + xtra;
|
||||
ta.value = val.slice(0, start) + ins + val.slice(end);
|
||||
ta.selectionStart = ta.selectionEnd = start + ins.length;
|
||||
vvSyncHlOverlay(); vvUndoCapture(true); return;
|
||||
}
|
||||
|
||||
// ── Auto-close brackets and quotes (no modifier keys) ─────────────────────
|
||||
if (!e.altKey) {
|
||||
// Skip over closing char if already there
|
||||
if (VV_CLOSE.has(e.key) && start === end && val[start] === e.key) {
|
||||
e.preventDefault();
|
||||
ta.selectionStart = ta.selectionEnd = start + 1;
|
||||
vvEditorCursorMoved(); return;
|
||||
}
|
||||
// Smart backspace: delete matched pair
|
||||
if (e.key === 'Backspace' && start === end && start > 0 && VV_PAIRS[val[start-1]] === val[start]) {
|
||||
e.preventDefault(); vvUndoCapture(true);
|
||||
ta.value = val.slice(0, start - 1) + val.slice(start + 1);
|
||||
ta.selectionStart = ta.selectionEnd = start - 1;
|
||||
vvSyncHlOverlay(); vvUndoCapture(true); return;
|
||||
}
|
||||
// Auto-close opening bracket/quote
|
||||
if (e.key in VV_PAIRS) {
|
||||
if (e.key === "'" && /\w/.test(val[start - 1] ?? '')) return; // skip contractions
|
||||
if (end === start && /\w/.test(val[start] ?? '')) return; // skip if next is word char
|
||||
e.preventDefault(); vvUndoCapture(true);
|
||||
const cl = VV_PAIRS[e.key];
|
||||
if (end > start) {
|
||||
// Wrap selection in pair
|
||||
ta.value = val.slice(0, start) + e.key + val.slice(start, end) + cl + val.slice(end);
|
||||
ta.selectionStart = start + 1; ta.selectionEnd = end + 1;
|
||||
} else {
|
||||
ta.value = val.slice(0, start) + e.key + cl + val.slice(end);
|
||||
ta.selectionStart = ta.selectionEnd = start + 1;
|
||||
}
|
||||
vvSyncHlOverlay(); vvUndoCapture(true); return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Script browser tree ───────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user