mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: support shift+enter in terminal (#19021)
It acts the same alt+enter, but is more familiar to users. Closes #18864
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import "jest-canvas-mock";
|
||||
import { waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { API } from "api/api";
|
||||
import WS from "jest-websocket-mock";
|
||||
import { http, HttpResponse } from "msw";
|
||||
@@ -148,4 +149,21 @@ describe("TerminalPage", () => {
|
||||
ws.send(text);
|
||||
await expectTerminalText(container, text);
|
||||
});
|
||||
|
||||
it("supports shift+enter", async () => {
|
||||
const ws = new WS(
|
||||
`ws://localhost/api/v2/workspaceagents/${MockWorkspaceAgent.id}/pty`,
|
||||
);
|
||||
|
||||
const { container } = await renderTerminal();
|
||||
// Ideally we could use ws.connected but that seems to pause React updates.
|
||||
// For now, wait for the initial resize message instead.
|
||||
await ws.nextMessage;
|
||||
|
||||
const msg = ws.nextMessage;
|
||||
const terminal = container.getElementsByClassName("xterm");
|
||||
await userEvent.type(terminal[0], "{Shift>}{Enter}{/Shift}");
|
||||
const req = JSON.parse(new TextDecoder().decode((await msg) as Uint8Array));
|
||||
expect(req.data).toBe("\x1b\r");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -148,6 +148,25 @@ const TerminalPage: FC = () => {
|
||||
}),
|
||||
);
|
||||
|
||||
// Make shift+enter send ^[^M (escaped carriage return). Applications
|
||||
// typically take this to mean to insert a literal newline. There is no way
|
||||
// to remove this handler, so we must attach it once and rely on a ref to
|
||||
// send it to the current socket.
|
||||
const escapedCarriageReturn = "\x1b\r";
|
||||
terminal.attachCustomKeyEventHandler((ev) => {
|
||||
if (ev.shiftKey && ev.key === "Enter") {
|
||||
if (ev.type === "keydown") {
|
||||
websocketRef.current?.send(
|
||||
new TextEncoder().encode(
|
||||
JSON.stringify({ data: escapedCarriageReturn }),
|
||||
),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
terminal.open(terminalWrapperRef.current);
|
||||
|
||||
// We have to fit twice here. It's unknown why, but the first fit will
|
||||
@@ -190,6 +209,7 @@ const TerminalPage: FC = () => {
|
||||
}, [navigate, reconnectionToken, searchParams]);
|
||||
|
||||
// Hook up the terminal through a web socket.
|
||||
const websocketRef = useRef<Websocket>();
|
||||
useEffect(() => {
|
||||
if (!terminal) {
|
||||
return;
|
||||
@@ -270,6 +290,7 @@ const TerminalPage: FC = () => {
|
||||
.withBackoff(new ExponentialBackoff(1000, 6))
|
||||
.build();
|
||||
websocket.binaryType = "arraybuffer";
|
||||
websocketRef.current = websocket;
|
||||
websocket.addEventListener(WebsocketEvent.open, () => {
|
||||
// Now that we are connected, allow user input.
|
||||
terminal.options = {
|
||||
@@ -333,6 +354,7 @@ const TerminalPage: FC = () => {
|
||||
d.dispose();
|
||||
}
|
||||
websocket?.close(1000);
|
||||
websocketRef.current = undefined;
|
||||
};
|
||||
}, [
|
||||
command,
|
||||
|
||||
Reference in New Issue
Block a user