mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-09 15:58:34 +00:00
73b27ad7c4
* fix(autocomplete): sync ghost text to live input on every keystroke Ghost text was displayed based on whatever input was passed to GhostTextAddon.show() at fetch time. Between a user's keystroke and the next debounced fetchSuggestions firing (~100ms), the on-screen line had already advanced one character but ghost.getGhostText() still returned the pre-update tail. Pressing → during that window pasted the stale tail on top of the new char — e.g. type "do", suggestion shows "cker ls"; type "c", accept immediately → "doc" + "cker ls" lands as "doccker ls" instead of the expected "docker ls". Two-layer fix: 1. New GhostTextAddon.adjustToInput(newInput) that re-renders the ghost against a fresh input without waiting for a new fetch: shrinks / grows the tail if the suggestion still prefix-matches, hides otherwise. Called from handleInput after every buffer mutation (printable, backspace, Ctrl-W, paste tail) when the buffer is reliable. Unreliable-buffer paths skip the call to avoid making the ghost lie. 2. Defense-in-depth at both ghost-accept sites (→ and Ctrl-→): recompute the tail against the live typed buffer instead of trusting getGhostText's show()-time state. If the suggestion no longer prefixes the live buffer, hide without writing. Ctrl-→ additionally resyncs ghost.show() to the live buffer before picking the next word so getNextWord operates on an up-to-date tail. * fix(autocomplete): defer ghost text updates to the next xterm render The previous pass made adjustToInput re-show the ghost synchronously on every keystroke, but xterm hasn't echoed the triggering char yet at that moment — cursorX is still the pre-keystroke position. Painting the shrunken tail there left it visibly overlapping with the char xterm was about to draw, and the ghost only snapped to the right column on the next onRender tick. That one-frame overlap is the "jitter" the reporter still saw. Switch adjustToInput to a defer-and-reapply pattern: - On every keystroke that should re-align the ghost, stash the desired input in pendingInput and hide the element immediately. The transient blank frame is preferable to an overlap glyph. - The existing term.onRender listener now checks for a pending update first: by that tick xterm has processed the echo, cursorX has advanced, and we can paint the new tail at the correct column via applyInputUpdate. - New isActive() exposes "has a live suggestion even if hidden waiting for render" so a fast "type + →" / "type + Ctrl-→" sequence in the hide-until-render gap still hits the accept branch and grabs the recomputed tail from the live buffer. show() and hide() clear pendingInput so an explicit state change supersedes any queued adjust. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(autocomplete): restore ghost text, predict-anchor-shift on each keystroke The previous refactor broke inline completion entirely: 1. useTerminalAutocomplete force-disabled showGhostText whenever showPopupMenu was on — and both are true by default, so ghost never rendered. 2. GhostTextAddon put its overlay container *under* xterm's screen via insertBefore + no z-index. xterm's default renderer paints theme.background across every cell including empty ones, so the ghost was fully occluded by the canvas even when the hook *did* call show(). Fixes both issues and lands the correct per-keystroke strategy the jitter report was asking for: - Drop the showGhostText-vs-showPopupMenu gate; respect user settings. - Put the ghost container back on top of the screen (appendChild + z-index 1). - Track anchorInputLength at show() time. adjustToInput now advances the ghost's left by (newInput.length - anchorInputLength) cells *synchronously* — i.e. it predicts where xterm's cursor will land once the echo arrives, instead of re-reading the live cursorX that hasn't advanced yet. textContent is trimmed in the same call, so ghost + real-input stay aligned across SSH echo latency with no one-frame overlap or blank gap. - Updated GhostTextAddon.test.ts expectations for the new behavior (and cast the fake-document through unknown to fix the pre-existing TS error). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(autocomplete): address ghost text review feedback Follow-ups on the predict-anchor-shift from the previous commit, based on a code-reviewer pass: - Backspace / Ctrl-W de-sync: updatePosition's Math.max(0, ...) was clamping the delta to zero when newInput shrank below the show-time input length. The ghost then stayed pinned at the original anchor column while the real cursor walked back left, leaving a gap between the cursor and the ghost. Let the delta go negative so the ghost tracks the cursor backwards; clamp the resulting left at 0 instead of clamping the delta. - Resize staleness: onResize now also resets lastLeft/lastTop and re-renders, so the dedup cache in updatePosition doesn't hide a now-stale pixel coordinate after xterm recomputes cell dims. - Added a regression test for the backspace path covering both the step-back-below-anchor case and the clamp-at-0-on-overshoot case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(autocomplete): don't accept whole suggestion when buffer is unreliable Codex flagged (#815 P1 ×2) that the live-buffer recompute on → and Ctrl-→ falls into a degenerate path when typedBufferReliableRef is false. My previous cut used live = "" as the fallback, but fullSuggestion.startsWith("") is always true — so: - → would write the entire suggestion over whatever is on the line (post history-recall ↑, Ctrl-R reverse search, etc.). - Ctrl-→ would reanchor the ghost at the start and getNextWord would hand back the first token, duplicating leading content on top of the recalled command. When the buffer is unreliable, empty buffer ≠ empty line — the line has content we're not tracking. Fall back to the ghost's own cached state instead of recomputing: - → reliable: recompute tail vs live buffer, flip buffer to the accepted suggestion, reliability back on. - → unreliable: use ghost.getGhostText() (shown-at-show-time tail) and don't touch the buffer/reliability flag. - Ctrl-→ reliable: resync ghost to live, then proceed as before. - Ctrl-→ unreliable: skip the resync, derive the shrink baseline from fullSuggestion - current-ghost-tail so the next-word logic still works off whatever the ghost was actually showing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(autocomplete): hide ghost on single-byte cursor/recall control chars Reviewer caught that Ctrl-P / Ctrl-N / Ctrl-R / Ctrl-A / Ctrl-E and friends flip typedBufferReliableRef to false but don't hide the ghost — leaving it rendering a tail tied to the pre-recall line. The previous commit's unreliable-→ fallback then reads that stale tail via ghost.getGhostText() and writes it onto the recalled line, reproducing the very duplication class the fallback was meant to prevent (just triggered by Ctrl-P instead of ↑). Mirror what the escape-sequence branch already does: clearState() + return. Once the ghost is hidden, ghost.isActive() is false at the → and Ctrl-→ gates, so the accept-path doesn't fire at all until a fresh fetchSuggestions re-anchors it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(autocomplete): drop accepted-command cache on cursor/recall keys Reviewer pointed out that the early returns in the single-byte ctrl-char and escape-sequence branches leave lastAcceptedCommandRef untouched. If the user accepts a suggestion via → and then immediately hits Ctrl-R or ↑ to pick a different command, the fast Enter path (lines ~611-612) still reads the cached accepted command and records it — logging the old suggestion instead of whichever command the reverse-search or history-recall actually ran. Null lastAcceptedCommandRef at the top of both branches (same place we hide the ghost and flip reliability off) so accept + recall + Enter records the recalled command, not the stale accept. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(autocomplete): also null accepted-command cache on Ctrl-C / Ctrl-U Reviewer flagged this class of bug is still reachable via Ctrl-C / Ctrl-U. The branch handling those kills the zle line, but the early return leaves lastAcceptedCommandRef pointing at a command that is no longer on the line: accept "git status" via → → Ctrl-C to abandon → type "ls" → Enter logs "git status" via the fast path instead of "ls". Same one-liner as the other early-return branches: null the cache alongside clearState(). Now the cache's lifetime truly ends at any event that invalidates the accept. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(autocomplete): null accepted-command cache on bracketed paste too Fifth-pass reviewer caught the last symmetric gap: the bracketed-paste branch appends pasted bytes to the buffer but leaves lastAcceptedCommandRef set. Accept "git status" via → then bracketed-paste " --short" (no embedded newline), press Enter — the fast path at line 611 still reads "git status" and logs that instead of "git status --short". Mirror the non-bracketed paste branch: null the cache before clearState() returns. All handleInput paths that extend or invalidate the line now consistently end the cache's lifetime. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(autocomplete): predict ghost column by cell width + wrap at EOL Review caught two geometry bugs in GhostTextAddon.updatePosition that only surfaced outside the ASCII happy path: - CJK / fullwidth / emoji glyphs occupy two xterm cells but the predictor advanced by one char-length per code unit, so ghost drifted one cell left for every wide char typed and visibly overlapped the user's glyph. - When the predicted column crossed term.cols the real cursor wrapped to the next row, but the predictor just piled more pixels onto `left` — ghost walked off the right edge instead of following onto the next line. Fix both by switching from code-unit count to a small EAW-style width classifier, then applying row wrapping via col = (anchorX + cellDelta) % cols rowOffset = Math.floor((anchorX + cellDelta) / cols) against the current term.cols. Fake terminal in the test suite now exposes cols/rows so the unit tests can exercise both invariants: - "advances the anchor by two cells when a CJK glyph is typed" - "wraps the ghost to the next row when the predicted column crosses cols" Known limitation the review already flagged: on backspace-after-wide we don't have per-grapheme widths to reverse exactly, so the negative delta falls back to code-unit width on the deleted slice. The slice is `currentSuggestion[currentInput.length..anchorInputLength]` which is the same text the user would have typed, so it's correct when only ASCII edits; wide-char backspace can still drift by one cell. Fixing this cleanly needs a per-grapheme buffer and is out of scope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(autocomplete): honor showGhostText toggle while a ghost is on screen Codex flagged (#815 P2) that fetchSuggestions gates new ghost shows on settingsRef.current.showGhostText, but handleInput's adjustToInput call had no such guard. A ghost that was already active at the moment the user turned showGhostText off would keep tracking the typed buffer via adjustToInput on every keystroke, so the "disabled" setting only took hold after some unrelated path called clearState(). Two-part fix: - Add a useEffect watching settings.showGhostText. When it flips false, hide the active ghost immediately so the disabled setting applies to whatever was already on screen. - Gate the adjustToInput call in handleInput behind settingsRef.current.showGhostText too, so subsequent keystrokes under the disabled setting don't try to move or re-show a ghost. Codex's earlier P2 about wrap-at-EOL on line 236 is already resolved bye61f0e8b(predict-column-with-wrap + CJK width); that comment is against an older commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(autocomplete): self-heal stale anchor + handle backward-wrap on delete Codex flagged two real geometry gaps in the predict-anchor-shift math: 1. Stale anchor on high-latency shells. show() captures cursorX from xterm at debounce-fire time, but under SSH round-trip latency the user's latest keystroke may not have echoed yet — cursorX is still the pre-echo column. With updatePosition now purely anchor-based (no longer reading live cursorX on every render), that stale anchor becomes frozen; the ghost stays one-plus cells off for the whole suggestion session until another show() rebuilds it. 2. Backspace crossing a wrapped row boundary. Math.max(0, ...) clamped targetCol at zero, so deletions past column 0 stayed pinned to the current row instead of wrapping back to the previous row — exactly the symmetric case the forward wrap added ine61f0e8bhandles. Fixes: - Self-heal in updatePosition: while no adjustToInput has moved us from the show-time baseline (currentInput.length === anchorInputLength), re-read live cursorX/Y each render tick. Once the user starts typing the anchor is frozen and delta math takes over. - Normalize the wrap for negative targetCol: `col = targetCol % cols` plus `if (col < 0) col += cols`, `rowOffset = Math.floor(targetCol/cols)` naturally yielding -1 on underflow. Clamp `top` at row 0 so a runaway negative doesn't render above the terminal. Two new tests cover both invariants: - "self-heals a stale anchor on render while no adjustToInput has fired" - "wraps the ghost to the previous row when deletion crosses a row boundary" Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(autocomplete): restore ghost/popup mutual-exclusivity guard in hook Codex flagged (#815 P2) that dropping the popup-wins-over-ghost normalization inside useTerminalAutocomplete weakens the hook's own defensive invariant. The repo enforces mutual exclusivity in two places already — SettingsTerminalTab toggles one off when the other turns on, and domain/models.ts normalizes stored settings so autocompletePopupMenu === true forces autocompleteGhostText to false — so on the normal Terminal.tsx → store path only one of the two arrives as true. But the hook's own defaults (DEFAULT_AUTOCOMPLETE_SETTINGS) have both flags true, and any caller that builds settings directly from those defaults (tests, future embedders) would end up rendering popup + inline ghost simultaneously against the repo-wide contract. Restore the guard, comment it as defensive rather than load-bearing so future readers don't mistake it for the hiding-invisible-ghost bug I was fixing last time (that was really the insertBefore / z-index issue in GhostTextAddon.ts, not this normalization). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
326 lines
9.3 KiB
TypeScript
326 lines
9.3 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { GhostTextAddon } from "./autocomplete/GhostTextAddon.ts";
|
|
|
|
type RenderListener = () => void;
|
|
type ResizeListener = () => void;
|
|
|
|
class FakeElement {
|
|
public readonly style: Record<string, string> = {};
|
|
public textContent = "";
|
|
public className = "";
|
|
public children: FakeElement[] = [];
|
|
|
|
appendChild(child: FakeElement): FakeElement {
|
|
this.children.push(child);
|
|
return child;
|
|
}
|
|
|
|
insertBefore(child: FakeElement, referenceNode: FakeElement | null): FakeElement {
|
|
if (!referenceNode) {
|
|
this.children.push(child);
|
|
return child;
|
|
}
|
|
const index = this.children.indexOf(referenceNode);
|
|
if (index < 0) {
|
|
this.children.push(child);
|
|
return child;
|
|
}
|
|
this.children.splice(index, 0, child);
|
|
return child;
|
|
}
|
|
|
|
remove(): void {
|
|
// No-op for tests.
|
|
}
|
|
|
|
querySelector(selector: string): FakeElement | null {
|
|
if (selector === ".xterm-screen") {
|
|
return this.children.find((child) => child.className === "xterm-screen") ?? null;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function installFakeDocument(): () => void {
|
|
const previousDocument = globalThis.document;
|
|
const fakeDocument = {
|
|
createElement() {
|
|
return new FakeElement();
|
|
},
|
|
} as unknown as Document;
|
|
Object.defineProperty(globalThis, "document", {
|
|
configurable: true,
|
|
value: fakeDocument,
|
|
});
|
|
return () => {
|
|
if (previousDocument === undefined) {
|
|
delete (globalThis as { document?: Document }).document;
|
|
return;
|
|
}
|
|
Object.defineProperty(globalThis, "document", {
|
|
configurable: true,
|
|
value: previousDocument,
|
|
});
|
|
};
|
|
}
|
|
|
|
function createFakeTerm() {
|
|
const renderListeners: RenderListener[] = [];
|
|
const resizeListeners: ResizeListener[] = [];
|
|
const element = new FakeElement();
|
|
const screen = new FakeElement();
|
|
screen.className = "xterm-screen";
|
|
element.appendChild(screen);
|
|
|
|
const term = {
|
|
element,
|
|
cols: 80,
|
|
rows: 24,
|
|
options: {
|
|
fontSize: 14,
|
|
fontFamily: "monospace",
|
|
},
|
|
buffer: {
|
|
active: {
|
|
cursorX: 2,
|
|
cursorY: 0,
|
|
},
|
|
},
|
|
_core: {
|
|
_renderService: {
|
|
dimensions: {
|
|
css: {
|
|
cell: {
|
|
width: 9,
|
|
height: 18,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
onRender(listener: RenderListener) {
|
|
renderListeners.push(listener);
|
|
return {
|
|
dispose() {
|
|
const index = renderListeners.indexOf(listener);
|
|
if (index >= 0) renderListeners.splice(index, 1);
|
|
},
|
|
};
|
|
},
|
|
onResize(listener: ResizeListener) {
|
|
resizeListeners.push(listener);
|
|
return {
|
|
dispose() {
|
|
const index = resizeListeners.indexOf(listener);
|
|
if (index >= 0) resizeListeners.splice(index, 1);
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
return {
|
|
term,
|
|
ghostElement: () => screen.children[0]?.children[0] ?? null,
|
|
fireRender() {
|
|
for (const listener of [...renderListeners]) listener();
|
|
},
|
|
};
|
|
}
|
|
|
|
test("shifts ghost to predicted cursor column as matching input is typed", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
addon.show("docker", "do");
|
|
|
|
const ghost = ghostElement();
|
|
assert.ok(ghost);
|
|
assert.equal(ghost.style.display, "block");
|
|
assert.equal(ghost.textContent, "cker");
|
|
// show() anchored at cursorX=2, cell width=9 → left=18.
|
|
assert.equal(ghost.style.left, "18px");
|
|
|
|
addon.adjustToInput("doc");
|
|
|
|
// After one matching char, the ghost predicts the cursor has moved
|
|
// to column 3 and trims "c" from the tail so the next char starts
|
|
// where the echo will land. Not waiting for xterm's render keeps
|
|
// ghost + real input aligned across SSH echo latency.
|
|
assert.equal(ghost.style.display, "block");
|
|
assert.equal(ghost.textContent, "ker");
|
|
assert.equal(ghost.style.left, "27px");
|
|
assert.equal(addon.getGhostText(), "ker");
|
|
} finally {
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("walks the anchor column backwards on backspace so the ghost re-aligns", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
addon.show("docker", "do");
|
|
|
|
const ghost = ghostElement();
|
|
assert.ok(ghost);
|
|
|
|
addon.adjustToInput("doc");
|
|
assert.equal(ghost.textContent, "ker");
|
|
assert.equal(ghost.style.left, "27px");
|
|
|
|
// Backspace below the anchor input — the ghost should shift *left*,
|
|
// not stay pinned at the show-time anchor column. Pinning would
|
|
// leave a visual gap between the real cursor and the ghost.
|
|
addon.adjustToInput("d");
|
|
assert.equal(ghost.textContent, "ocker");
|
|
// anchor was cursorX=2 captured at show(); "d" is 1 char below
|
|
// anchorInputLength=2 → predicted cursor column = 1.
|
|
assert.equal(ghost.style.left, "9px");
|
|
|
|
// Backspace past the anchor back to empty: left is clamped at 0.
|
|
addon.adjustToInput("");
|
|
assert.equal(ghost.textContent, "docker");
|
|
assert.equal(ghost.style.left, "0px");
|
|
} finally {
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("advances the anchor by two cells when a CJK glyph is typed", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
// Suggestion starts with a CJK char so the prefix-match survives
|
|
// the next keystroke.
|
|
addon.show("你好世界", "");
|
|
const ghost = ghostElement();
|
|
assert.ok(ghost);
|
|
// show() anchored at cursorX=2. Input length 0 → delta 0 → left=18.
|
|
assert.equal(ghost.style.left, "18px");
|
|
|
|
addon.adjustToInput("你");
|
|
|
|
// One CJK char = 2 cells. Predicted col = 2 + 2 = 4 → left 36px.
|
|
assert.equal(ghost.textContent, "好世界");
|
|
assert.equal(ghost.style.left, "36px");
|
|
} finally {
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("wraps the ghost to the next row when the predicted column crosses cols", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
// Shrink the terminal to 10 cols to keep the math obvious. Anchor at
|
|
// col 8 with 5 ASCII chars to type → predicted col = 13, which should
|
|
// wrap to col 3 of row 1.
|
|
term.cols = 10;
|
|
term.buffer.active.cursorX = 8;
|
|
addon.activate(term as never);
|
|
addon.show("abcdefghij", "ab");
|
|
const ghost = ghostElement();
|
|
assert.ok(ghost);
|
|
assert.equal(ghost.style.top, "0px");
|
|
|
|
addon.adjustToInput("abcde");
|
|
|
|
// Predicted col = 8 + (5-2) = 11 → wraps to col 1 on next row.
|
|
// cellWidth=9, cellHeight=18.
|
|
assert.equal(ghost.textContent, "fghij");
|
|
assert.equal(ghost.style.left, "9px");
|
|
assert.equal(ghost.style.top, "18px");
|
|
} finally {
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("self-heals a stale anchor on render while no adjustToInput has fired", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement, fireRender } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
// show() captures cursorX=2 — simulate this firing during the
|
|
// keystroke→echo gap by later advancing the live cursor and
|
|
// verifying the ghost anchor snaps to the echoed position.
|
|
addon.show("docker", "do");
|
|
const ghost = ghostElement();
|
|
assert.ok(ghost);
|
|
assert.equal(ghost.style.left, "18px");
|
|
|
|
term.buffer.active.cursorX = 5;
|
|
fireRender();
|
|
|
|
// Input hasn't moved from the show-time baseline, so updatePosition
|
|
// re-reads live cursor: new left = 5 * 9 = 45px.
|
|
assert.equal(ghost.style.left, "45px");
|
|
} finally {
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("wraps the ghost to the previous row when deletion crosses a row boundary", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
term.cols = 10;
|
|
term.buffer.active.cursorX = 1;
|
|
term.buffer.active.cursorY = 1;
|
|
addon.activate(term as never);
|
|
// Anchored at row 1 col 1 with 5 chars already typed.
|
|
addon.show("abcdefghij", "abcde");
|
|
const ghost = ghostElement();
|
|
assert.ok(ghost);
|
|
|
|
// Backspace back to 2 chars — delta = -3 across a row boundary.
|
|
addon.adjustToInput("ab");
|
|
|
|
// targetCol = 1 - 3 = -2 → col = 8 (wrapped) on row 0.
|
|
assert.equal(ghost.textContent, "cdefghij");
|
|
assert.equal(ghost.style.left, "72px");
|
|
assert.equal(ghost.style.top, "0px");
|
|
} finally {
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("hides ghost immediately when input no longer matches suggestion", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
addon.show("docker", "do");
|
|
|
|
const ghost = ghostElement();
|
|
assert.ok(ghost);
|
|
assert.equal(ghost.style.display, "block");
|
|
|
|
addon.adjustToInput("dox");
|
|
|
|
assert.equal(ghost.style.display, "none");
|
|
assert.equal(ghost.textContent, "");
|
|
assert.equal(addon.isActive(), false);
|
|
} finally {
|
|
restoreDocument();
|
|
}
|
|
});
|