Skip to content

Commit

Permalink
fix(blockwise-visual): handle double-width chars and tabs (#1596)
Browse files Browse the repository at this point in the history
* improve: the selection direction is consistent with nvim

* test: fix blockwise visual test

* refactor: use ternary
  • Loading branch information
xiyaowong committed Nov 12, 2023
1 parent 68557f7 commit c554c06
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 44 deletions.
47 changes: 47 additions & 0 deletions runtime/lua/vscode-neovim/internal.lua
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,51 @@ do
end
end

---display-col to col
---@param line string
---@param discol number
local function discol_to_col(line, discol)
if discol == 0 then
return 0
end

local col = 0
local max_col = vim.fn.strcharlen(line)

local min_distance = math.huge
local nearest_col = 0

while col <= max_col do
local curr_col = math.floor((col + max_col) / 2)
local curr_discol = fn.strdisplaywidth(fn.strcharpart(line, 0, curr_col))
if curr_discol == discol then
return curr_col
elseif curr_discol > discol then
max_col = curr_col - 1
else
col = curr_col + 1
end

local curr_distance = math.abs(curr_discol - discol)
if curr_distance < min_distance then
min_distance = curr_distance
nearest_col = curr_col
elseif curr_distance == min_distance and curr_col < nearest_col then
nearest_col = curr_col
end
end
return nearest_col
end

function M.handle_blockwise_selection(lines, start_discol, end_discol)
local result = {}
for _, line in ipairs(lines) do
table.insert(result, {
discol_to_col(line, start_discol),
discol_to_col(line, end_discol),
})
end
return result
end

return M
105 changes: 63 additions & 42 deletions src/cursor_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
commands,
Disposable,
Position,
Range,
Selection,
TextEditor,
TextEditorCursorStyle,
Expand Down Expand Up @@ -420,7 +421,6 @@ export class CursorManager implements Disposable {
anchor: Position | undefined,
): Promise<Selection[]> => {
const doc = editor.document;

if (!anchor) {
const anchorNvim = await this.client.callFunction("getpos", ["v"]);
anchor = convertVimPositionToEditorPosition(editor, new Position(anchorNvim[1] - 1, anchorNvim[2] - 1));
Expand All @@ -437,50 +437,71 @@ export class CursorManager implements Disposable {
// we hide the real cursor and use a highlight decorator for the fake cursor
switch (mode.visual) {
case "char":
if (anchor.isBeforeOrEqual(active))
return [
new Selection(
anchor,
new Position(active.line, Math.min(active.character + 1, activeLineLength)),
),
];
else
return [
new Selection(
new Position(anchor.line, Math.min(anchor.character + 1, anchorLineLength)),
active,
),
];
return [
anchor.isBeforeOrEqual(active)
? new Selection(
anchor,
new Position(active.line, Math.min(active.character + 1, activeLineLength)),
)
: new Selection(
new Position(anchor.line, Math.min(anchor.character + 1, anchorLineLength)),
active,
),
];
case "line":
if (anchor.line <= active.line) return [new Selection(anchor.line, 0, active.line, activeLineLength)];
else return [new Selection(anchor.line, anchorLineLength, active.line, 0)];
return [
anchor.line <= active.line
? new Selection(anchor.line, 0, active.line, activeLineLength)
: new Selection(anchor.line, anchorLineLength, active.line, 0),
];
case "block": {
const selections: Selection[] = [];
// we want the first selection to be on the cursor line, so that a single-line selection will properly trigger word highlight
const before = anchor.line < active.line;
for (
let line = active.line;
before ? line >= anchor.line : line <= anchor.line;
before ? line-- : line++
) {
// skip lines that don't contain the block selection, except if it contains the cursor
const docLine = doc.lineAt(line);
if (
docLine.range.end.character > Math.min(anchor.character, active.character) ||
line === active.line
) {
// selections go left to right for simplicity, and don't go past the end of the line
selections.push(
new Selection(
line,
Math.min(anchor.character, active.character),
line,
Math.min(Math.max(anchor.character, active.character) + 1, docLine.range.end.character),
),
);
}
const getDisplayWidth = async (...pos: Position[]) => {
const parts = pos.map((p) => {
const textRange = doc.validateRange(new Range(p.line, 0, p.line, p.character));
return doc.getText(textRange);
});
return this.client.lua(
`
return (function(parts)
return vim.tbl_map(function(part) return vim.fn.strdisplaywidth(part) end, parts)
end)(...)
`,
[parts],
) as Promise<number[]>;
};

const [anchorDisCol, activeDisCol] = await getDisplayWidth(anchor, active);
const [startDisCol, endDisCol] = [
Math.min(anchorDisCol, activeDisCol),
Math.max(anchorDisCol, activeDisCol),
];

const lines = [];
const startLine = Math.min(active.line, anchor.line);
const endLine = Math.max(active.line, anchor.line);
for (let line = startLine; line <= endLine; line++) {
lines.push(doc.lineAt(line).text);
}
return selections;
const _code = 'return require"vscode-neovim.internal".handle_blockwise_selection(...)';
const ret = (await this.client.lua(_code, [lines, startDisCol, endDisCol])) as [number, number][];

const ranges: Range[] = [];
ret.forEach(([startChar, endChar], idx) => {
const line = idx + startLine;
const lineRange = doc.lineAt(line).range;
const range = lineRange.intersection(new Range(line, startChar, line, endChar));
if (range && (range.start.isBefore(lineRange.end) || line === active.line)) ranges.push(range);
});

// correct the orientation
const selections = ranges.map((r) => {
const range = doc.validateRange(new Range(r.start, r.end.translate(0, 1)));
return activeDisCol >= anchorDisCol
? new Selection(range.start, range.end)
: new Selection(range.end, range.start);
});
// Make sure word highlighting is triggered correctly
return anchor.isBeforeOrEqual(active) ? selections.reverse() : selections;
}
}
};
Expand Down
4 changes: 2 additions & 2 deletions src/test/suite/visual-modes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ describe("Visual modes test", () => {
await sendVSCodeKeys("0");
await assertContent(
{
vsCodeSelections: [new vscode.Selection(0, 0, 0, 7), new vscode.Selection(1, 0, 1, 7)],
vsCodeSelections: [new vscode.Selection(0, 7, 0, 0), new vscode.Selection(1, 7, 1, 0)],
},
client,
);
Expand Down Expand Up @@ -271,7 +271,7 @@ describe("Visual modes test", () => {
await sendVSCodeKeys("j");
await assertContent(
{
vsCodeSelections: [new vscode.Selection(1, 1, 1, 1), new vscode.Selection(0, 1, 0, 3)],
vsCodeSelections: [new vscode.Selection(1, 1, 1, 1), new vscode.Selection(0, 3, 0, 1)],
},
client,
);
Expand Down

0 comments on commit c554c06

Please sign in to comment.