repl: up/down arrow walks per-tab history, draft preserved at the bottom
This commit is contained in:
parent
98b1b5dccd
commit
e34fb1f7dc
2 changed files with 172 additions and 0 deletions
|
|
@ -296,6 +296,57 @@ function renderAll() {
|
|||
});
|
||||
}
|
||||
|
||||
// ─── History navigation (readline-style up/down) ────────────────────
|
||||
// Each tab owns its own history; the active tab's draft is preserved so
|
||||
// that walking back into history doesn't eat what the user was typing.
|
||||
const history = { idx: null, draft: "" };
|
||||
|
||||
function historyEntries() {
|
||||
// Transcript's `input` fields, deduplicated against the immediate
|
||||
// predecessor — pressing up shouldn't make you hit the same line
|
||||
// twice in a row when you just submitted it.
|
||||
const tab = activeTab();
|
||||
if (!tab) return [];
|
||||
const out = [];
|
||||
for (const e of tab.transcript) {
|
||||
if (out.length && out[out.length - 1] === e.input) continue;
|
||||
out.push(e.input);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function historyPrev() {
|
||||
const entries = historyEntries();
|
||||
if (entries.length === 0) return;
|
||||
if (history.idx === null) {
|
||||
history.draft = inputEl.value;
|
||||
history.idx = entries.length - 1;
|
||||
} else if (history.idx > 0) {
|
||||
history.idx--;
|
||||
}
|
||||
inputEl.value = entries[history.idx];
|
||||
inputEl.setSelectionRange(inputEl.value.length, inputEl.value.length);
|
||||
}
|
||||
|
||||
function historyNext() {
|
||||
const entries = historyEntries();
|
||||
if (history.idx === null) return;
|
||||
if (history.idx >= entries.length - 1) {
|
||||
history.idx = null;
|
||||
inputEl.value = history.draft;
|
||||
history.draft = "";
|
||||
} else {
|
||||
history.idx++;
|
||||
inputEl.value = entries[history.idx];
|
||||
}
|
||||
inputEl.setSelectionRange(inputEl.value.length, inputEl.value.length);
|
||||
}
|
||||
|
||||
function resetHistory() {
|
||||
history.idx = null;
|
||||
history.draft = "";
|
||||
}
|
||||
|
||||
// ─── Input handling ─────────────────────────────────────────────────
|
||||
async function sendInput() {
|
||||
const src = inputEl.value.trim();
|
||||
|
|
@ -307,6 +358,7 @@ async function sendInput() {
|
|||
const entry = { input: src, results: [], kind: "ok" };
|
||||
tab.transcript.push(entry);
|
||||
inputEl.value = "";
|
||||
resetHistory();
|
||||
sendBtn.disabled = true;
|
||||
cancelBtn.disabled = false;
|
||||
renderAll();
|
||||
|
|
@ -360,6 +412,40 @@ inputEl.addEventListener("keydown", (e) => {
|
|||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendInput();
|
||||
return;
|
||||
}
|
||||
// Up/Down navigate per-tab history when the caret is in the only
|
||||
// (or first/last) row — otherwise they belong to the textarea's
|
||||
// natural multi-line navigation.
|
||||
if (e.key === "ArrowUp") {
|
||||
const before = inputEl.value.substring(0, inputEl.selectionStart);
|
||||
if (!before.includes("\n")) {
|
||||
e.preventDefault();
|
||||
historyPrev();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.key === "ArrowDown") {
|
||||
const after = inputEl.value.substring(inputEl.selectionStart);
|
||||
if (!after.includes("\n")) {
|
||||
e.preventDefault();
|
||||
historyNext();
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
inputEl.addEventListener("input", () => {
|
||||
// Any keystroke that isn't an arrow drops history navigation —
|
||||
// edits are now the user's own draft, not the historical entry.
|
||||
if (history.idx !== null && document.activeElement === inputEl) {
|
||||
// We can't reliably distinguish arrow-induced updates here, so
|
||||
// we only invalidate when the buffer has actually diverged from
|
||||
// the historical entry the cursor was on.
|
||||
const entries = historyEntries();
|
||||
if (entries[history.idx] !== inputEl.value) {
|
||||
history.draft = inputEl.value;
|
||||
history.idx = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
sendBtn.addEventListener("click", sendInput);
|
||||
|
|
|
|||
|
|
@ -296,6 +296,57 @@ function renderAll() {
|
|||
});
|
||||
}
|
||||
|
||||
// ─── History navigation (readline-style up/down) ────────────────────
|
||||
// Each tab owns its own history; the active tab's draft is preserved so
|
||||
// that walking back into history doesn't eat what the user was typing.
|
||||
const history = { idx: null, draft: "" };
|
||||
|
||||
function historyEntries() {
|
||||
// Transcript's `input` fields, deduplicated against the immediate
|
||||
// predecessor — pressing up shouldn't make you hit the same line
|
||||
// twice in a row when you just submitted it.
|
||||
const tab = activeTab();
|
||||
if (!tab) return [];
|
||||
const out = [];
|
||||
for (const e of tab.transcript) {
|
||||
if (out.length && out[out.length - 1] === e.input) continue;
|
||||
out.push(e.input);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function historyPrev() {
|
||||
const entries = historyEntries();
|
||||
if (entries.length === 0) return;
|
||||
if (history.idx === null) {
|
||||
history.draft = inputEl.value;
|
||||
history.idx = entries.length - 1;
|
||||
} else if (history.idx > 0) {
|
||||
history.idx--;
|
||||
}
|
||||
inputEl.value = entries[history.idx];
|
||||
inputEl.setSelectionRange(inputEl.value.length, inputEl.value.length);
|
||||
}
|
||||
|
||||
function historyNext() {
|
||||
const entries = historyEntries();
|
||||
if (history.idx === null) return;
|
||||
if (history.idx >= entries.length - 1) {
|
||||
history.idx = null;
|
||||
inputEl.value = history.draft;
|
||||
history.draft = "";
|
||||
} else {
|
||||
history.idx++;
|
||||
inputEl.value = entries[history.idx];
|
||||
}
|
||||
inputEl.setSelectionRange(inputEl.value.length, inputEl.value.length);
|
||||
}
|
||||
|
||||
function resetHistory() {
|
||||
history.idx = null;
|
||||
history.draft = "";
|
||||
}
|
||||
|
||||
// ─── Input handling ─────────────────────────────────────────────────
|
||||
async function sendInput() {
|
||||
const src = inputEl.value.trim();
|
||||
|
|
@ -307,6 +358,7 @@ async function sendInput() {
|
|||
const entry = { input: src, results: [], kind: "ok" };
|
||||
tab.transcript.push(entry);
|
||||
inputEl.value = "";
|
||||
resetHistory();
|
||||
sendBtn.disabled = true;
|
||||
cancelBtn.disabled = false;
|
||||
renderAll();
|
||||
|
|
@ -360,6 +412,40 @@ inputEl.addEventListener("keydown", (e) => {
|
|||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendInput();
|
||||
return;
|
||||
}
|
||||
// Up/Down navigate per-tab history when the caret is in the only
|
||||
// (or first/last) row — otherwise they belong to the textarea's
|
||||
// natural multi-line navigation.
|
||||
if (e.key === "ArrowUp") {
|
||||
const before = inputEl.value.substring(0, inputEl.selectionStart);
|
||||
if (!before.includes("\n")) {
|
||||
e.preventDefault();
|
||||
historyPrev();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.key === "ArrowDown") {
|
||||
const after = inputEl.value.substring(inputEl.selectionStart);
|
||||
if (!after.includes("\n")) {
|
||||
e.preventDefault();
|
||||
historyNext();
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
inputEl.addEventListener("input", () => {
|
||||
// Any keystroke that isn't an arrow drops history navigation —
|
||||
// edits are now the user's own draft, not the historical entry.
|
||||
if (history.idx !== null && document.activeElement === inputEl) {
|
||||
// We can't reliably distinguish arrow-induced updates here, so
|
||||
// we only invalidate when the buffer has actually diverged from
|
||||
// the historical entry the cursor was on.
|
||||
const entries = historyEntries();
|
||||
if (entries[history.idx] !== inputEl.value) {
|
||||
history.draft = inputEl.value;
|
||||
history.idx = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
sendBtn.addEventListener("click", sendInput);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue