Files
vscode-neovim-vscode-neovim/src/test/integ/multi-byte-chars.test.ts
T
dualfrozandwongxy 5cc37c9f0a fix(cursor): keep the visual selection whole over characters outside the BMP (#2683)
* fix(cursor): keep the visual selection whole over characters outside the BMP

The inclusive Nvim end column was turned into an exclusive VSCode one by
adding a single UTF-16 unit. A character outside the BMP is two of them, so
the selection ended in the middle of a surrogate pair and drifted away from
where Nvim had the cursor: moving right over it took two presses and the
selected region no longer matched.

Step over the whole character instead, using its byte length.

Closes #2535

* test(cursor): add visual selection tests for non-BMP characters

* test(cursor): expect the astral pair to be whole in the mixed-character selection

---------

Co-authored-by: wongxy <xiyao.wong@foxmail.com>
2026-09-23 18:39:53 +08:00

341 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { NeovimClient } from "neovim";
import vscode from "vscode";
import {
attachTestNvimClient,
sendVSCodeKeys,
assertContent,
closeAllActiveEditors,
closeNvimClient,
setCursor,
sendEscapeKey,
openTextDocument,
sendInsertKey,
} from "./integrationUtils";
describe("Multi-width characters", () => {
let client: NeovimClient;
before(async () => {
client = await attachTestNvimClient();
});
after(async () => {
await closeNvimClient(client);
await closeAllActiveEditors();
});
it("Works - 2col width chars", async () => {
await openTextDocument({ content: ["测试微服务", "", "没办法跳转到最后一个"].join("\n") });
await assertContent(
{
content: ["测试微服务", "", "没办法跳转到最后一个"],
cursor: [0, 0],
},
client,
);
await sendVSCodeKeys("ll");
await assertContent(
{
vsCodeCursor: [0, 2],
},
client,
);
await sendVSCodeKeys("x", 500);
await assertContent(
{
content: ["测试服务", "", "没办法跳转到最后一个"],
vsCodeCursor: [0, 2],
},
client,
);
// await sendVSCodeKeys("vll");
// await assertContent(
// {
// vsCodeSelections: [new vscode.Selection(0, 2, 0, 4)],
// },
// client,
// );
await setCursor(2, 5);
await assertContent(
{
vsCodeCursor: [2, 5],
},
client,
);
});
it("Works - 1col-2byte width chars", async () => {
await openTextDocument({ content: ["żżżżżżżż',", "ńńńńńńńń',"].join("\n") });
await assertContent(
{
content: ["żżżżżżżż',", "ńńńńńńńń',"],
cursor: [0, 0],
},
client,
);
await sendVSCodeKeys("lll");
await assertContent({ vsCodeCursor: [0, 3] }, client);
await sendVSCodeKeys("x");
await assertContent(
{
content: ["żżżżżżż',", "ńńńńńńńń',"],
vsCodeCursor: [0, 3],
},
client,
);
});
it("Works - 1col-3byte width chars", async () => {
await openTextDocument({ content: ["1ᵩᵩ123"].join("\n") });
await assertContent(
{
content: ["1ᵩᵩ123"],
cursor: [0, 0],
},
client,
);
await sendVSCodeKeys("ll");
await assertContent({ vsCodeCursor: [0, 2] }, client);
await sendVSCodeKeys("ll");
await assertContent({ vsCodeCursor: [0, 4] }, client);
await sendVSCodeKeys("x");
await assertContent(
{
content: ["1ᵩᵩ13"],
vsCodeCursor: [0, 4],
},
client,
);
});
it("Cursor is ok after exiting insert mode - 2 col chars", async () => {
await openTextDocument({ content: ["测试微服务", "", "没办法跳转到最后一个"].join("\n") });
await sendVSCodeKeys("lll");
await assertContent(
{
vsCodeCursor: [0, 3],
},
client,
);
await sendInsertKey();
await sendEscapeKey();
await assertContent(
{
vsCodeCursor: [0, 2],
},
client,
);
});
it("Cursor is ok after exiting insert mode - 1col-2byte chars", async () => {
await openTextDocument({ content: ["żżżżżżżż',", "ńńńńńńńń',"].join("\n") });
await sendVSCodeKeys("lll");
await assertContent(
{
vsCodeCursor: [0, 3],
},
client,
);
await sendInsertKey();
await sendEscapeKey();
await assertContent(
{
vsCodeCursor: [0, 2],
},
client,
);
});
it("Cursor is ok after exiting insert mode - 1col-3byte width chars", async () => {
await openTextDocument({ content: ["1ᵩᵩ123"].join("\n") });
await assertContent(
{
content: ["1ᵩᵩ123"],
cursor: [0, 0],
},
client,
);
await sendVSCodeKeys("ll");
await assertContent({ vsCodeCursor: [0, 2] }, client);
await sendInsertKey("a");
await sendEscapeKey();
await assertContent({ vsCodeCursor: [0, 2] }, client);
await sendVSCodeKeys("ll");
await sendInsertKey("a");
await sendEscapeKey();
await assertContent({ vsCodeCursor: [0, 4] }, client);
});
it("Cursor is ok after exiting insert mode at end of the line - 1col-3byte width chars", async () => {
await openTextDocument({ content: "ᵩ123" });
await sendInsertKey("A");
await sendEscapeKey();
await assertContent({ vsCodeCursor: [0, 3] }, client);
});
it("Multi byte with tabs", async () => {
await openTextDocument({ content: "\t\t测试\t微服务" });
await sendVSCodeKeys("gg0");
await sendVSCodeKeys("l");
await assertContent(
{
vsCodeCursor: [0, 1],
},
client,
);
await sendVSCodeKeys("l");
await assertContent(
{
vsCodeCursor: [0, 2],
},
client,
);
await sendVSCodeKeys("x");
await assertContent(
{
content: ["\t\t试\t微服务"],
vsCodeCursor: [0, 2],
},
client,
);
await sendVSCodeKeys("ll");
await assertContent(
{
vsCodeCursor: [0, 4],
},
client,
);
await sendVSCodeKeys("x");
await assertContent(
{
content: ["\t\t试\t服务"],
vsCodeCursor: [0, 4],
},
client,
);
});
it("Issue #503", async () => {
await openTextDocument({ content: "ŷaŷbŷcŷd = functionŷ(par1)" });
await sendVSCodeKeys("f(l");
await assertContent(
{
vsCodeCursor: [0, 26],
},
client,
);
await sendVSCodeKeys("ci(");
await assertContent({ vsCodeCursor: [0, 26], content: ["ŷaŷbŷcŷd = functionŷ()"] }, client);
});
it("Works - Emoji chars", async () => {
await openTextDocument({ content: ["🚀🕵️💡🤣", "", "🕵️🕵️🕵️🕵️"].join("\n") });
await assertContent(
{
content: ["🚀🕵️💡🤣", "", "🕵️🕵️🕵️🕵️"],
cursor: [0, 0],
},
client,
);
await sendVSCodeKeys("ll");
await assertContent(
{
vsCodeCursor: [0, 5],
},
client,
);
await sendVSCodeKeys("x");
await assertContent(
{
content: ["🚀🕵️🤣", "", "🕵️🕵️🕵️🕵️"],
vsCodeCursor: [0, 5],
},
client,
);
await sendVSCodeKeys("jjdw");
await assertContent(
{
content: ["🚀🕵️🤣", "", "🕵️🕵️"],
vsCodeCursor: [2, 3],
},
client,
);
});
it("Works - visual mode over chars outside the BMP", async () => {
// 𝕜 is U+1D55C: one Nvim character, but two UTF-16 units for VSCode
await openTextDocument({ content: ["a𝕜b"].join("\n") });
await assertContent({ content: ["a𝕜b"], cursor: [0, 0] }, client);
await sendVSCodeKeys("v");
await assertContent({ vsCodeSelections: [new vscode.Selection(0, 0, 0, 1)] }, client);
await sendVSCodeKeys("l");
await assertContent({ vsCodeSelections: [new vscode.Selection(0, 0, 0, 3)] }, client);
await sendVSCodeKeys("l");
await assertContent({ vsCodeSelections: [new vscode.Selection(0, 0, 0, 4)] }, client);
await sendEscapeKey();
});
it("Works - visual mode over consecutive non-BMP chars", async () => {
await openTextDocument({ content: ["a𝕜𝕝b"].join("\n") });
await sendVSCodeKeys("v");
await assertContent({ vsCodeSelections: [new vscode.Selection(0, 0, 0, 1)] }, client);
await sendVSCodeKeys("ll");
await assertContent({ vsCodeSelections: [new vscode.Selection(0, 0, 0, 5)] }, client);
await sendVSCodeKeys("l");
await assertContent({ vsCodeSelections: [new vscode.Selection(0, 0, 0, 6)] }, client);
await sendEscapeKey();
});
it("Works - visual mode across mixed BMP and non-BMP chars", async () => {
await openTextDocument({ content: ["aᵩ𝕜b"].join("\n") });
await sendVSCodeKeys("v");
await assertContent({ vsCodeSelections: [new vscode.Selection(0, 0, 0, 1)] }, client);
// the cursor sits on the astral char, whose two UTF-16 units both belong to
// the selection: a, the modifier letter and the pair take offsets 0 to 4
await sendVSCodeKeys("ll");
await assertContent({ vsCodeSelections: [new vscode.Selection(0, 0, 0, 4)] }, client);
await sendVSCodeKeys("l");
await assertContent({ vsCodeSelections: [new vscode.Selection(0, 0, 0, 5)] }, client);
await sendEscapeKey();
});
});