Skip to content

Commit b159ee2

Browse files
authored
fix: jetbrains by using dec instead of ansi for save/restore cursor (#427)
Signed-off-by: cpendery <cpendery@vt.edu>
1 parent baba3c2 commit b159ee2

4 files changed

Lines changed: 93 additions & 5 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import ansi from "ansi-escapes";
5+
6+
import type { ISTerm } from "../../isterm/pty.js";
7+
import * as dec from "../../utils/dec.js";
8+
import { SuggestionRenderer } from "../../ui/suggestionRenderer.js";
9+
import type { SuggestionManager } from "../../ui/suggestionManager.js";
10+
11+
const originalTerminalEmulator = process.env.TERMINAL_EMULATOR;
12+
13+
afterEach(() => {
14+
if (originalTerminalEmulator == null) {
15+
delete process.env.TERMINAL_EMULATOR;
16+
} else {
17+
process.env.TERMINAL_EMULATOR = originalTerminalEmulator;
18+
}
19+
});
20+
21+
const createRenderer = (writeOutput: (data: string) => void) => {
22+
const term = {
23+
cols: 80,
24+
getCommandState: () => ({ commandText: "g", cursorTerminated: true, hasOutput: false }),
25+
getCursorState: () => ({ cursorX: 1, cursorY: 0, remainingLines: 28, hidden: false, shift: 0 }),
26+
getPatch: () => "patch",
27+
isAlternateBuffer: () => false,
28+
} as unknown as ISTerm;
29+
const suggestions = {
30+
render: () => [{ startX: 1, length: 5, data: "item" }],
31+
suspend: async () => {},
32+
} as unknown as SuggestionManager;
33+
34+
return new SuggestionRenderer(term, suggestions, writeOutput);
35+
};
36+
37+
test("uses ANSI cursor position sequences by default", async () => {
38+
delete process.env.TERMINAL_EMULATOR;
39+
const output: string[] = [];
40+
const renderer = createRenderer((data) => output.push(data));
41+
42+
renderer.renderSuggestionUpdate(false);
43+
await renderer.handleBufferChange("alternate");
44+
45+
expect(output).toHaveLength(2);
46+
output.forEach((data) => {
47+
expect(data).toContain(ansi.cursorSavePosition);
48+
expect(data).toContain(ansi.cursorRestorePosition);
49+
expect(data).not.toContain(dec.cursorSavePosition);
50+
expect(data).not.toContain(dec.cursorRestorePosition);
51+
});
52+
});
53+
54+
test("uses DEC cursor position sequences in JetBrains terminals", async () => {
55+
process.env.TERMINAL_EMULATOR = "JetBrains-JediTerm";
56+
const output: string[] = [];
57+
const renderer = createRenderer((data) => output.push(data));
58+
59+
renderer.renderSuggestionUpdate(false);
60+
await renderer.handleBufferChange("alternate");
61+
62+
expect(output).toHaveLength(2);
63+
output.forEach((data) => {
64+
expect(data).toContain(dec.cursorSavePosition);
65+
expect(data).toContain(dec.cursorRestorePosition);
66+
expect(data).not.toContain(ansi.cursorSavePosition);
67+
expect(data).not.toContain(ansi.cursorRestorePosition);
68+
});
69+
});

src/ui/suggestionRenderer.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import ansi from "ansi-escapes";
55

66
import type { ISTerm } from "../isterm/pty.js";
7-
import { eraseViewport } from "../utils/ansi.js";
7+
import { eraseViewport, shouldFallbackToDec } from "../utils/ansi.js";
8+
import * as dec from "../utils/dec.js";
89
import { endTiming, startTiming } from "../utils/performance.js";
910
import { getMaxLines, type SuggestionManager } from "./suggestionManager.js";
1011

@@ -24,10 +25,16 @@ type RenderCache = {
2425
clear?: string;
2526
};
2627

28+
type CursorPositionSequences = {
29+
cursorSavePosition: string;
30+
cursorRestorePosition: string;
31+
};
32+
2733
export class SuggestionRenderer {
2834
readonly #term: ISTerm;
2935
readonly #suggestions: SuggestionManager;
3036
readonly #writeOutput: (data: string) => void;
37+
readonly #cursorPositionSequences: CursorPositionSequences;
3138
readonly #cache: RenderCache = {};
3239
#visible = false;
3340
#direction: Direction;
@@ -39,6 +46,7 @@ export class SuggestionRenderer {
3946
this.#term = term;
4047
this.#suggestions = suggestions;
4148
this.#writeOutput = writeOutput;
49+
this.#cursorPositionSequences = shouldFallbackToDec() ? dec : ansi;
4250
this.#direction = this.#snapshot().layout.direction;
4351
}
4452

@@ -169,10 +177,11 @@ export class SuggestionRenderer {
169177
cursor: { hidden: cursorHidden },
170178
} = snapshot;
171179
const showCursor = cursorHidden ? "" : ansi.cursorShow;
180+
const { cursorSavePosition, cursorRestorePosition } = this.#cursorPositionSequences;
172181
if (direction === "above") {
173-
return data + ansi.cursorHide + ansi.cursorSavePosition + ansi.cursorPrevLine.repeat(lines) + patch + ansi.cursorRestorePosition + showCursor;
182+
return data + ansi.cursorHide + cursorSavePosition + ansi.cursorPrevLine.repeat(lines) + patch + cursorRestorePosition + showCursor;
174183
}
175-
return ansi.cursorHide + ansi.cursorSavePosition + ansi.cursorNextLine + patch + ansi.cursorRestorePosition + showCursor + data;
184+
return ansi.cursorHide + cursorSavePosition + ansi.cursorNextLine + patch + cursorRestorePosition + showCursor + data;
176185
}
177186

178187
#clearOutput(direction: Direction, snapshot: RenderSnapshot, bufferType: PatchBuffer = "active"): string {
@@ -182,11 +191,12 @@ export class SuggestionRenderer {
182191
const lines = direction === "above" ? Math.min(getMaxLines(), cursorY) : Math.min(getMaxLines(), remainingLines);
183192
const patch = this.#term.getPatch(lines, [], direction, bufferType);
184193
const showCursor = cursorHidden ? "" : ansi.cursorShow;
194+
const { cursorSavePosition, cursorRestorePosition } = this.#cursorPositionSequences;
185195

186196
if (direction === "above") {
187-
return ansi.cursorHide + ansi.cursorSavePosition + ansi.cursorPrevLine.repeat(lines) + patch + ansi.cursorRestorePosition + showCursor;
197+
return ansi.cursorHide + cursorSavePosition + ansi.cursorPrevLine.repeat(lines) + patch + cursorRestorePosition + showCursor;
188198
}
189-
return ansi.cursorHide + ansi.cursorSavePosition + ansi.cursorNextLine + patch + ansi.cursorRestorePosition + showCursor;
199+
return ansi.cursorHide + cursorSavePosition + ansi.cursorNextLine + patch + cursorRestorePosition + showCursor;
190200
}
191201

192202
#clearPreviousDirection(snapshot: RenderSnapshot): void {

src/utils/ansi.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ const CSI = ESC + "[";
66
const OSC = "\u001B]";
77
const BEL = "\u0007";
88

9+
export const shouldFallbackToDec = () => process.env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
10+
911
export const IsTermOscPs = 6973;
1012
const IS_OSC = OSC + IsTermOscPs + ";";
1113

src/utils/dec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
const ESC = "\u001B";
5+
6+
export const cursorSavePosition = ESC + "7";
7+
export const cursorRestorePosition = ESC + "8";

0 commit comments

Comments
 (0)