ui: premium editor overhaul — selection, word highlight, find, status bar
Selection: - ::selection fix: text selection now visible (bright blue) even when textarea color is transparent — the #1 complaint - Overlay background changed transparent → #vv-editor-inner provides bg, allowing #vv-cur-line to show through Current line highlight: - #vv-cur-line div positioned absolutely behind overlay, tracks cursor line on every keyup/mouseup/click/scroll - Gutter: current line number highlighted in bright white + bold Word/variable occurrence highlight: - Click or position cursor on any identifier → all occurrences highlighted in yellow throughout the overlay (whole-word, case-sensitive) - Select a word → same treatment for the selection text - Clears when cursor moves off the word or find bar opens Find in file (Ctrl+F): - Find bar slides in above editor, pre-filled with selected word - Highlights all matches orange, current match in bright amber - ↑/↓ buttons + Enter/Shift+Enter to navigate between matches - Live match count "N / M" - Red border + "no matches" feedback when term not found - Escape or × closes, returns focus to editor Status bar: - Persistent bar below editor: Ln N, Col N | selection info | language - Shows "Bash" or "Bash / Config" depending on file type - Hides when editor is not open Visual polish: - Editor wrap: unified border + border-radius 4px 4px 0 0, overflow:hidden clips gutter and inner to rounded corners as one block - Status bar border-radius 0 0 4px 4px — completes the block - Gutter: 46px (was 42) — room for 3-digit line numbers - Slim 8px scrollbar with styled track and hover state - vvEditorReset() clears all editor state on open/close Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
753f99ac0c
commit
a250d590ca
@@ -324,24 +324,75 @@
|
||||
border: 1px solid #333; border-radius: 4px; padding: 10px 12px; resize: none;
|
||||
line-height: 1.5; scroll-behavior: auto; tab-size: 2; width: 100%; box-sizing: border-box; }
|
||||
|
||||
/* Editor layout: gutter + inner area */
|
||||
#vv-editor-wrap { display: flex; }
|
||||
#vv-ln-gutter { width: 42px; min-width: 42px; flex-shrink: 0;
|
||||
/* Editor layout: gutter + inner area — unified bordered block */
|
||||
#vv-editor-wrap { display: flex; border: 1px solid #2e2e2e; border-radius: 4px 4px 0 0;
|
||||
overflow: hidden; background: #0d0d0d; }
|
||||
#vv-ln-gutter { width: 46px; min-width: 46px; flex-shrink: 0;
|
||||
background: #0a0a0a; border-right: 1px solid #1c1c1c;
|
||||
font-family: monospace; font-size: 12px; line-height: 1.5; tab-size: 2;
|
||||
color: #3a3a3a; text-align: right; padding: 10px 8px 10px 0;
|
||||
overflow: hidden; user-select: none; white-space: pre; }
|
||||
#vv-editor-inner { position: relative; flex: 1; overflow: hidden; }
|
||||
.vv-gln-cur { color: #ccc; font-weight: bold; }
|
||||
#vv-editor-inner { position: relative; flex: 1; overflow: hidden; background: #0d0d0d; }
|
||||
|
||||
/* Syntax-highlight overlay sits over the transparent textarea */
|
||||
#vv-hl-overlay { display: none; position: absolute; top: 1px; left: 1px; right: 1px; bottom: 1px;
|
||||
/* Current line highlight — sits behind overlay */
|
||||
#vv-cur-line { position: absolute; left: 0; right: 0; height: 18px;
|
||||
background: rgba(255,255,255,0.04);
|
||||
border-top: 1px solid rgba(255,255,255,0.03);
|
||||
border-bottom: 1px solid rgba(255,255,255,0.03);
|
||||
pointer-events: none; display: none; }
|
||||
|
||||
/* Syntax-highlight overlay — transparent bg so cur-line shows through */
|
||||
#vv-hl-overlay { display: none; position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
||||
margin: 0; padding: 10px 12px; box-sizing: border-box;
|
||||
font-family: monospace; font-size: 12px; line-height: 1.5; tab-size: 2;
|
||||
white-space: pre-wrap; word-break: break-all; overflow: hidden;
|
||||
pointer-events: none; user-select: none;
|
||||
background: #0d0d0d; border: none; border-radius: 3px; }
|
||||
background: transparent; border: none; border-radius: 0; }
|
||||
.vv-editor-hl #vv-hl-overlay { display: block; }
|
||||
.vv-editor-hl #vv-editor-body { color: transparent; caret-color: #ddd; background: transparent; }
|
||||
.vv-editor-hl #vv-editor-body { color: transparent; caret-color: #ddd; background: transparent;
|
||||
border: none !important; border-radius: 0 !important; }
|
||||
|
||||
/* Visible selection — works even when text is transparent */
|
||||
.vv-editor-hl #vv-editor-body::selection { background: rgba(100,149,237,0.35); color: transparent; }
|
||||
.vv-editor-hl #vv-editor-body::-moz-selection{ background: rgba(100,149,237,0.35); color: transparent; }
|
||||
|
||||
/* Word match and search marks in the overlay */
|
||||
.vv-word-mark { background: rgba(255,220,60,0.14); outline: 1px solid rgba(255,220,60,0.35); border-radius: 2px; }
|
||||
.vv-search-mark { background: rgba(255,140,20,0.22); outline: 1px solid rgba(255,140,20,0.45); border-radius: 2px; }
|
||||
.vv-search-mark-current{ background: rgba(255,90,0,0.40); outline: 2px solid rgba(255,100,0,0.7); border-radius: 2px; }
|
||||
|
||||
/* Slim scrollbar for editor */
|
||||
#vv-editor-body::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
#vv-editor-body::-webkit-scrollbar-track { background: #0a0a0a; }
|
||||
#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;
|
||||
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-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;
|
||||
padding: 0 2px; flex-shrink: 0; }
|
||||
#vv-find-close:hover { color: #aaa; }
|
||||
|
||||
/* Editor status bar */
|
||||
#vv-editor-status { display: none; align-items: center; gap: 14px; 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; }
|
||||
#vv-editor-status.vv-ed-active { display: flex; }
|
||||
.vv-es-pos { color: #666; }
|
||||
.vv-es-sel { color: #555; }
|
||||
.vv-es-lang { margin-left: auto; color: #4a6a7a; }
|
||||
|
||||
/* Suggestions panel — accordion */
|
||||
#vv-suggestions { overflow-y: auto; }
|
||||
|
||||
@@ -872,15 +872,31 @@ Still the same two servers, two households, the same media stack running itself.
|
||||
<span style="color:#888;font-size:12px;flex-shrink:0;">Name:</span>
|
||||
<input type="text" id="vv-editor-name" class="vv-cron" style="flex:1" placeholder="script_name (no .sh)">
|
||||
</div>
|
||||
<div id="vv-editor-find">
|
||||
<input type="text" id="vv-find-input" placeholder="Find…"
|
||||
oninput="vvFindSearch()" onkeydown="vvFindKeydown(event)">
|
||||
<span class="vv-find-count" id="vv-find-count"></span>
|
||||
<button class="vv-btn-sm vv-find-nav-btn" onclick="vvFindNav(-1)" title="Previous (Shift+Enter)">↑</button>
|
||||
<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-editor-wrap">
|
||||
<pre id="vv-ln-gutter" aria-hidden="true"></pre>
|
||||
<div id="vv-editor-inner">
|
||||
<div id="vv-cur-line"></div>
|
||||
<pre id="vv-hl-overlay" aria-hidden="true"></pre>
|
||||
<textarea id="vv-editor-body" class="vv-editor-body" spellcheck="false"
|
||||
oninput="vvSyncHlOverlay()" onscroll="vvSyncHlScroll()"
|
||||
onkeydown="vvEditorKeydown(event)"></textarea>
|
||||
onkeydown="vvEditorKeydown(event)"
|
||||
onkeyup="vvEditorCursorMoved()" onmouseup="vvEditorCursorMoved()"
|
||||
onclick="vvEditorCursorMoved()"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div id="vv-editor-status">
|
||||
<span class="vv-es-pos" id="vv-es-pos">Ln 1, Col 1</span>
|
||||
<span class="vv-es-sel" id="vv-es-sel"></span>
|
||||
<span class="vv-es-lang" id="vv-es-lang"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Arrange workspace (shown when arrange mode is active) -->
|
||||
@@ -951,6 +967,11 @@ let vvConfId = null;
|
||||
let vvSelectedRow = null;
|
||||
let vvCurrentSiId = null;
|
||||
let vvCurrentSiHdr = null;
|
||||
// Editor state
|
||||
let vvWordMatch = null; // word under cursor — highlighted everywhere in overlay
|
||||
let vvFindTerm = ''; // active find-bar search string
|
||||
let vvFindMatches = []; // [{start,end}] in raw text
|
||||
let vvFindIdx = 0; // current match index
|
||||
|
||||
// Setup mode — injected by PHP when navigating from the first-run wizard
|
||||
let vvSetupConf = <?= json_encode($vv_setup_conf) ?>;
|
||||
@@ -1092,6 +1113,8 @@ function vvRestoreInvert() {
|
||||
}
|
||||
|
||||
function vvBackToSuggestions() {
|
||||
vvEditorReset();
|
||||
document.getElementById('vv-editor-status').classList.remove('vv-ed-active');
|
||||
if (vvActiveId) {
|
||||
const old = document.querySelector('[data-id="' + CSS.escape(vvActiveId) + '"]');
|
||||
if (old) old.querySelector('.vv-job-row').classList.remove('vv-row-selected');
|
||||
@@ -1520,6 +1543,7 @@ function vvAddScript() {
|
||||
document.getElementById('vv-editor-body').value = '#!/bin/bash\n\n';
|
||||
vvShowEditorMode('New Script');
|
||||
vvSyncHlOverlay();
|
||||
vvEditorCursorMoved();
|
||||
nameEl.focus();
|
||||
}
|
||||
|
||||
@@ -1543,6 +1567,7 @@ function vvEditScript(id) {
|
||||
.then(d => {
|
||||
document.getElementById('vv-editor-body').value = d.ok ? d.content : '# Error loading script';
|
||||
vvSyncHlOverlay();
|
||||
vvEditorCursorMoved();
|
||||
})
|
||||
.catch(() => {
|
||||
document.getElementById('vv-editor-body').value = '# Error loading script';
|
||||
@@ -1553,7 +1578,10 @@ function vvEditScript(id) {
|
||||
}
|
||||
|
||||
function vvShowEditorMode(title) {
|
||||
vvEditorReset();
|
||||
document.getElementById('vv-editor').classList.add('vv-editor-hl');
|
||||
document.getElementById('vv-editor-status').classList.add('vv-ed-active');
|
||||
document.getElementById('vv-es-lang').textContent = 'Bash';
|
||||
document.getElementById('vv-suggestions').style.display = 'none';
|
||||
document.getElementById('vv-log-pre').style.display = 'none';
|
||||
document.getElementById('vv-si-view').style.display = 'none';
|
||||
@@ -2588,12 +2616,16 @@ function vvEditRawConf(file) {
|
||||
document.getElementById('vv-editor-body').value = d.ok ? d.content : '# Error loading file';
|
||||
document.getElementById('vv-editor').classList.add('vv-editor-hl');
|
||||
vvSyncHlOverlay();
|
||||
vvEditorCursorMoved();
|
||||
requestAnimationFrame(vvFitRight);
|
||||
})
|
||||
.catch(() => { document.getElementById('vv-editor-body').value = '# Load failed'; });
|
||||
}
|
||||
|
||||
function vvShowRawConfMode(title) {
|
||||
vvEditorReset();
|
||||
document.getElementById('vv-editor-status').classList.add('vv-ed-active');
|
||||
document.getElementById('vv-es-lang').textContent = 'Bash / Config';
|
||||
document.getElementById('vv-suggestions').style.display = 'none';
|
||||
document.getElementById('vv-log-pre').style.display = 'none';
|
||||
document.getElementById('vv-si-view').style.display = 'none';
|
||||
@@ -2671,14 +2703,52 @@ function vvSaveRawConf() {
|
||||
|
||||
// ── Highlighted conf editor overlay ──────────────────────────────────────────
|
||||
|
||||
// ── Overlay helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
// Annotate text occurrences in already-rendered HTML (text between > and < only).
|
||||
// Returns modified HTML with <span class=cls> wrapping each whole-word match.
|
||||
function vvMarkInHtml(html, word, cls, caseInsensitive) {
|
||||
if (!word || word.length < 1) return html;
|
||||
const hw = word.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
const esc = hw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const fl = caseInsensitive ? 'gi' : 'g';
|
||||
const re = new RegExp('(?<![\\w$])' + esc + '(?![\\w])', fl);
|
||||
return html.replace(/>((?:[^<])*)</g, (_, txt) =>
|
||||
'>' + txt.replace(re, `<span class="${cls}">$&</span>`) + '<'
|
||||
);
|
||||
}
|
||||
|
||||
// Like vvMarkInHtml but marks the curIdx-th match differently.
|
||||
function vvMarkSearchInHtml(html, term, curIdx) {
|
||||
if (!term) return html;
|
||||
const hw = term.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
const esc = hw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const re = new RegExp(esc, 'gi');
|
||||
let idx = 0;
|
||||
return html.replace(/>((?:[^<])*)</g, (_, txt) => {
|
||||
const repl = txt.replace(re, m => {
|
||||
const cls = idx++ === curIdx ? 'vv-search-mark-current' : 'vv-search-mark';
|
||||
return `<span class="${cls}">${m}</span>`;
|
||||
});
|
||||
return '>' + repl + '<';
|
||||
});
|
||||
}
|
||||
|
||||
function vvSyncHlOverlay() {
|
||||
const ov = document.getElementById('vv-hl-overlay');
|
||||
if (!ov || !document.getElementById('vv-editor').classList.contains('vv-editor-hl')) return;
|
||||
const ta = document.getElementById('vv-editor-body');
|
||||
ov.innerHTML = vvHl(ta.value, false) + '\n';
|
||||
let html = vvHl(ta.value, false) + '\n';
|
||||
if (vvFindTerm) {
|
||||
html = vvMarkSearchInHtml(html, vvFindTerm, vvFindIdx);
|
||||
} else if (vvWordMatch) {
|
||||
html = vvMarkInHtml(html, vvWordMatch, 'vv-word-mark', false);
|
||||
}
|
||||
ov.innerHTML = html;
|
||||
ov.scrollTop = ta.scrollTop;
|
||||
ov.scrollLeft = ta.scrollLeft;
|
||||
vvUpdateLineNums();
|
||||
vvUpdateCurLine();
|
||||
}
|
||||
|
||||
function vvSyncHlScroll() {
|
||||
@@ -2689,17 +2759,165 @@ function vvSyncHlScroll() {
|
||||
ov.scrollLeft = ta.scrollLeft;
|
||||
const gutter = document.getElementById('vv-ln-gutter');
|
||||
if (gutter) gutter.scrollTop = ta.scrollTop;
|
||||
vvUpdateCurLine();
|
||||
}
|
||||
|
||||
function vvUpdateLineNums() {
|
||||
const gutter = document.getElementById('vv-ln-gutter');
|
||||
const ta = document.getElementById('vv-editor-body');
|
||||
if (!gutter || !ta) return;
|
||||
const count = ta.value.split('\n').length;
|
||||
let nums = '';
|
||||
for (let i = 1; i <= count; i++) nums += i + '\n';
|
||||
gutter.textContent = nums;
|
||||
gutter.scrollTop = ta.scrollTop;
|
||||
const lines = ta.value.split('\n').length;
|
||||
const curLn = ta.value.slice(0, ta.selectionStart).split('\n').length;
|
||||
let html = '';
|
||||
for (let i = 1; i <= lines; i++) {
|
||||
html += i === curLn ? `<span class="vv-gln-cur">${i}</span>\n` : `${i}\n`;
|
||||
}
|
||||
gutter.innerHTML = html;
|
||||
gutter.scrollTop = ta.scrollTop;
|
||||
}
|
||||
|
||||
// Position the current-line highlight strip behind the overlay
|
||||
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 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';
|
||||
cl.style.height = lineH + 'px';
|
||||
cl.style.display = '';
|
||||
}
|
||||
|
||||
// Called on cursor move (keyup, mouseup, click) — update word match, status bar, cur-line
|
||||
function vvEditorCursorMoved() {
|
||||
vvUpdateCurLine();
|
||||
vvUpdateEditorStatus();
|
||||
|
||||
if (vvFindTerm) { vvSyncHlOverlay(); return; } // find mode owns the overlay
|
||||
|
||||
const ta = document.getElementById('vv-editor-body');
|
||||
const start = ta.selectionStart;
|
||||
const end = ta.selectionEnd;
|
||||
const val = ta.value;
|
||||
|
||||
// If user selected a word (not whitespace, at least 2 chars) → highlight all occurrences
|
||||
if (end > start) {
|
||||
const sel = val.slice(start, end);
|
||||
const word = /^[\w$]{2,}$/.test(sel) ? sel : null;
|
||||
if (word !== vvWordMatch) { vvWordMatch = word; vvSyncHlOverlay(); }
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the identifier word at cursor position
|
||||
const before = val.slice(0, start);
|
||||
const after = val.slice(start);
|
||||
const wBefore = before.match(/[$A-Za-z_][\w]*$/) ?? [''];
|
||||
const wAfter = after.match(/^[\w]+/) ?? [''];
|
||||
const word = wBefore[0].replace(/^\$/, '') + wAfter[0]; // strip leading $ for matching
|
||||
const newWord = word.length >= 2 ? word : null;
|
||||
if (newWord !== vvWordMatch) { vvWordMatch = newWord; vvSyncHlOverlay(); }
|
||||
}
|
||||
|
||||
function vvUpdateEditorStatus() {
|
||||
const ta = document.getElementById('vv-editor-body');
|
||||
const pos = document.getElementById('vv-es-pos');
|
||||
const sel = document.getElementById('vv-es-sel');
|
||||
if (!ta || !pos) return;
|
||||
const start = ta.selectionStart;
|
||||
const end = ta.selectionEnd;
|
||||
const val = ta.value;
|
||||
const before = val.slice(0, start);
|
||||
const lines = before.split('\n');
|
||||
pos.textContent = `Ln ${lines.length}, Col ${lines[lines.length - 1].length + 1}`;
|
||||
if (end > start) {
|
||||
const chars = end - start;
|
||||
const lns = val.slice(start, end).split('\n').length;
|
||||
sel.textContent = lns > 1 ? `(${chars} chars · ${lns} lines)` : `(${chars} chars)`;
|
||||
} else {
|
||||
sel.textContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Find bar ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function vvFindOpen() {
|
||||
const bar = document.getElementById('vv-editor-find');
|
||||
bar.classList.add('vv-find-open');
|
||||
const inp = document.getElementById('vv-find-input');
|
||||
// Pre-fill with selected word if any
|
||||
const ta = document.getElementById('vv-editor-body');
|
||||
const sel = ta.value.slice(ta.selectionStart, ta.selectionEnd);
|
||||
if (sel && /^\w+$/.test(sel)) { inp.value = sel; }
|
||||
inp.focus(); inp.select();
|
||||
vvFindSearch();
|
||||
}
|
||||
|
||||
function vvFindClose() {
|
||||
document.getElementById('vv-editor-find').classList.remove('vv-find-open');
|
||||
document.getElementById('vv-find-input').value = '';
|
||||
document.getElementById('vv-find-input').classList.remove('vv-find-no-match');
|
||||
document.getElementById('vv-find-count').textContent = '';
|
||||
vvFindTerm = ''; vvFindMatches = []; vvFindIdx = 0;
|
||||
document.getElementById('vv-editor-body').focus();
|
||||
vvSyncHlOverlay();
|
||||
}
|
||||
|
||||
function vvFindSearch() {
|
||||
const inp = document.getElementById('vv-find-input');
|
||||
const cnt = document.getElementById('vv-find-count');
|
||||
vvFindTerm = inp.value;
|
||||
if (!vvFindTerm) {
|
||||
vvFindMatches = []; vvFindIdx = 0;
|
||||
cnt.textContent = '';
|
||||
inp.classList.remove('vv-find-no-match');
|
||||
vvSyncHlOverlay(); return;
|
||||
}
|
||||
const ta = document.getElementById('vv-editor-body');
|
||||
const re = new RegExp(vvFindTerm.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'), 'gi');
|
||||
vvFindMatches = [];
|
||||
let m;
|
||||
while ((m = re.exec(ta.value)) !== null) vvFindMatches.push({start: m.index, end: m.index + m[0].length});
|
||||
if (!vvFindMatches.length) {
|
||||
vvFindIdx = 0;
|
||||
cnt.textContent = 'no matches';
|
||||
inp.classList.add('vv-find-no-match');
|
||||
} else {
|
||||
vvFindIdx = Math.min(vvFindIdx, vvFindMatches.length - 1);
|
||||
cnt.textContent = `${vvFindIdx + 1} / ${vvFindMatches.length}`;
|
||||
inp.classList.remove('vv-find-no-match');
|
||||
const cur = vvFindMatches[vvFindIdx];
|
||||
ta.selectionStart = cur.start; ta.selectionEnd = cur.end;
|
||||
}
|
||||
vvSyncHlOverlay();
|
||||
}
|
||||
|
||||
function vvFindNav(dir) {
|
||||
if (!vvFindMatches.length) return;
|
||||
vvFindIdx = (vvFindIdx + dir + vvFindMatches.length) % vvFindMatches.length;
|
||||
document.getElementById('vv-find-count').textContent = `${vvFindIdx + 1} / ${vvFindMatches.length}`;
|
||||
const ta = document.getElementById('vv-editor-body');
|
||||
const cur = vvFindMatches[vvFindIdx];
|
||||
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);
|
||||
vvSyncHlOverlay();
|
||||
}
|
||||
|
||||
function vvFindKeydown(e) {
|
||||
if (e.key === 'Enter') { e.preventDefault(); vvFindNav(e.shiftKey ? -1 : 1); }
|
||||
if (e.key === 'Escape') { e.preventDefault(); vvFindClose(); }
|
||||
}
|
||||
|
||||
function vvEditorReset() {
|
||||
vvWordMatch = null; vvFindTerm = ''; vvFindMatches = []; vvFindIdx = 0;
|
||||
const bar = document.getElementById('vv-editor-find');
|
||||
if (bar) { bar.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 = '';
|
||||
}
|
||||
|
||||
function vvEditorKeydown(e) {
|
||||
@@ -2715,6 +2933,21 @@ function vvEditorKeydown(e) {
|
||||
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+/ — toggle line comment
|
||||
if (e.key === '/' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
|
||||
Reference in New Issue
Block a user