Skip to content

Commit

Permalink
fix(highlight): forward search / matching in long lines (#1976)
Browse files Browse the repository at this point in the history
fix: cursor position after scrolling with incsearch

refactor: remove viewport hack when in cmdline mode
  • Loading branch information
xiyaowong committed May 21, 2024
1 parent 1b6bb7b commit 5fedba5
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 79 deletions.
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,8 @@ HighlightManager listens to the `ext_linegrid` API and renders highlights using
### ViewportManager

ViewportManager is responsible for syncing the viewport (editor window, scroll position) between vscode and nvim. It
listens to the `win_viewport` event, and supplements it with the custom `window-scroll` event triggered by the
`WinScrolled` autocommand. It also keeps track of the cursor position more reliably than `grid_cursor_goto`.
listens to the `win_viewport` event, and supplements it with the custom `viewport-changed` event. It also keeps track of
the cursor position more reliably than `grid_cursor_goto`.

### CursorManager

Expand Down
2 changes: 2 additions & 0 deletions runtime/lua/vscode-neovim.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ local default_optons = require("vscode-neovim.default-options")
local cursor = require("vscode-neovim.cursor")
local highlight = require("vscode-neovim.highlight")
local sync_options = require("vscode-neovim.sync-options")
local viewport = require("vscode-neovim.viewport")

default_optons.setup()
cursor.setup()
highlight.setup()
sync_options.setup()
viewport.setup()

local vscode = {
-- actions
Expand Down
77 changes: 77 additions & 0 deletions runtime/lua/vscode-neovim/viewport.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
local M = {}

local api, fn = vim.api, vim.fn

M.event_group = api.nvim_create_augroup("vscode-neovim.viewport", { clear = true })
M.viewport_changed_ns = api.nvim_create_namespace("vscode-neovim.viewport.changed")

---@class WinView All positions are 0-based
---@field winid integer
---@field bufnr integer
---@field lnum integer
---@field col integer
---@field coladd integer
---@field curswant integer
---@field topline integer
---@field botline integer
---@field topfill integer
---@field leftcol integer
---@field skipcol integer

local function setup_viewport_changed()
---@type table<integer, WinView>
local view_cache = {}

api.nvim_set_decoration_provider(M.viewport_changed_ns, {
on_win = function(_, win, buf, topline, botline)
-- We don't need the first window.
if win == 1000 then
return
end
---@type WinView
local view = api.nvim_win_call(win, fn.winsaveview)
view.winid = win
view.bufnr = buf
view.topline = topline
view.botline = botline
view.lnum = view.lnum - 1

local cache = view_cache[view.winid]
if cache and vim.deep_equal(view, cache) then
return
end
view_cache[view.winid] = view
fn.VSCodeExtensionNotify("viewport-changed", view)
end,
})

-- cleanup cache
api.nvim_create_autocmd({ "WinClosed" }, {
group = M.event_group,
callback = function()
local wins = vim.tbl_keys(view_cache)
for _, win in ipairs(wins) do
if not api.nvim_win_is_valid(win) then
view_cache[win] = nil
end
end
end,
})
end

function M.setup()
-- Highlighting needs to wait for the viewport-changed event to complete.
-- When the UI attaches, there are numerous highlight events (hl_attr_define, grid_line) to process.
--
-- Without delaying the setup, the viewport-changed event will cause frequent
-- pauses in highlight processing, resulting in screen flickering.
api.nvim_create_autocmd({ "UIEnter" }, {
once = true,
callback = function()
-- Don't worry about whether it's set too late.
vim.defer_fn(setup_viewport_changed, 1000)
end,
})
end

return M
7 changes: 5 additions & 2 deletions src/eventBus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,17 @@ type EventsMapping = {
["move-cursor"]: ["top" | "middle" | "bottom"];
scroll: ["page" | "halfPage", "up" | "down"];
["scroll-line"]: ["up" | "down"];
["window-scroll"]: [
number,
["viewport-changed"]: [
{
// All positions are 0-based
winid: number;
bufnr: number;
lnum: number;
col: number;
coladd: number;
curswant: number;
topline: number;
botline: number;
topfill: number;
leftcol: number;
skipcol: number;
Expand Down
106 changes: 86 additions & 20 deletions src/test/integ/extmark.test.ts → src/test/integ/highlights.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import assert from "assert";

import { NeovimClient } from "neovim";
import vscode, { DecorationOptions, Position, window } from "vscode";

import { attachTestNvimClient, closeAllActiveEditors, closeNvimClient, wait } from "./integrationUtils";

describe("Test ext mark", () => {
import vscode, { DecorationOptions, Position, TextEditor, window } from "vscode";

import {
attachTestNvimClient,
closeAllActiveEditors,
closeNvimClient,
sendEscapeKey,
sendNeovimKeys,
wait,
} from "./integrationUtils";

describe("Test highlights", () => {
let client: NeovimClient;
let realWindow: typeof vscode.window | undefined;

Expand All @@ -21,9 +28,14 @@ describe("Test ext mark", () => {
client = await attachTestNvimClient();
});

beforeEach(async () => {
await closeAllActiveEditors();
});

after(async () => {
restoreWindow();
await closeNvimClient(client);
await closeAllActiveEditors();
});

afterEach(() => {
Expand All @@ -37,12 +49,9 @@ describe("Test ext mark", () => {
await wait(500);
await vscode.window.showTextDocument(doc);

const stubTextEditor = new TextEditorStub();
const curEditor = vscode.window.activeTextEditor;
assert.ok(curEditor != null);
stubTextEditor.document = curEditor.document;
stubTextEditor.options = curEditor.options;
stubTextEditor.viewColumn = curEditor.viewColumn!;
const stubTextEditor = new TextEditorStub(curEditor);
realWindow = vscode.window;
vscode.window = {
activeTextEditor: stubTextEditor,
Expand Down Expand Up @@ -74,25 +83,83 @@ describe("Test ext mark", () => {
assert.ok(decoration.renderOptions?.before?.contentText == "j");
assert.ok(decoration.renderOptions?.before?.color == "#ff0000");
});

it("forward search / for long line", async () => {
const doc = await vscode.workspace.openTextDocument({
content: ["hello", " ".repeat(3000), "world", " ".repeat(1000)].join(""),
});
await vscode.window.showTextDocument(doc);

const curEditor = vscode.window.activeTextEditor;
assert.ok(curEditor != null);
const stubTextEditor = new TextEditorStub(curEditor);
realWindow = vscode.window;
vscode.window = {
...realWindow,
activeTextEditor: stubTextEditor,
visibleTextEditors: [stubTextEditor],
createTextEditorDecorationType: window.createTextEditorDecorationType,
} as any;

{
await sendEscapeKey();
await sendNeovimKeys(client, "/orl");
await wait(500);
assert(stubTextEditor.decorationOptionsList.length > 0);
const decoration = stubTextEditor.decorationOptionsList[0][0] as DecorationOptions;
assert.ok(decoration.range.isEqual(new vscode.Range(0, 3006, 0, 3009)));
}
});
});

// https://github.com/VSCodeVim/Vim/blob/master/test/historyTracker.test.ts#L181
// Fake class for testing
/* eslint-disable */
class TextEditorStub implements vscode.TextEditor {
document!: vscode.TextDocument;
selection!: vscode.Selection;
selections!: vscode.Selection[];
visibleRanges!: vscode.Range[];
options!: vscode.TextEditorOptions;
viewColumn!: vscode.ViewColumn;

decorationOptionsList: Array<vscode.Range[] | vscode.DecorationOptions[]> = [];

constructor() {
this.selection = new vscode.Selection(0, 0, 0, 0);
this.selections = [this.selection];
get visibleRanges() {
return this.editor.visibleRanges;
}

get selection() {
return this.editor.selection;
}

set selection(v) {
this.editor.selection = v;
}

get selections() {
return this.editor.selections;
}

set selections(v) {
this.editor.selections = v;
}

get document() {
return this.editor.document;
}

get options() {
return this.editor.options;
}

set options(v) {
this.editor.options = v;
}

get viewColumn() {
return this.editor.viewColumn;
}

get revealRange() {
return this.editor.revealRange;
}

constructor(private editor: TextEditor) {}

async edit(
callback: (editBuilder: vscode.TextEditorEdit) => void,
options?: { undoStopBefore: boolean; undoStopAfter: boolean },
Expand All @@ -113,7 +180,6 @@ class TextEditorStub implements vscode.TextEditor {
) {
this.decorationOptionsList.push(rangesOrOptions);
}
revealRange(range: vscode.Range, revealType?: vscode.TextEditorRevealType) {}
show(column?: vscode.ViewColumn) {}
hide() {}
}
Expand Down
52 changes: 52 additions & 0 deletions src/test/integ/incsearch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { strict as assert } from "assert";
import path from "path";

import { NeovimClient } from "neovim";

import {
assertContent,
attachTestNvimClient,
closeAllActiveEditors,
closeNvimClient,
openTextDocument,
sendNeovimKeys,
sendVSCodeKeys,
wait,
} from "./integrationUtils";

describe("Test incsearch", () => {
let client: NeovimClient;
before(async () => {
client = await attachTestNvimClient();
});
after(async () => {
await closeNvimClient(client);
await closeAllActiveEditors();
});
afterEach(async () => {
await closeAllActiveEditors();
});
it("Cursor is ok for incsearch after scroll", async () => {
const e = await openTextDocument(path.join(__dirname, "../../../test_fixtures/incsearch-scroll.ts"));

await sendVSCodeKeys("gg");
await sendVSCodeKeys("/bla");
await assertContent({ cursor: [115, 19] }, client);
assert.ok(e.visibleRanges[0].start.line <= 115);
});

it("Cursor is ok for incsearch even if register / is not empty", async function () {
this.retries(0);
await openTextDocument(path.join(__dirname, "../../../test_fixtures/incsearch-scroll.ts"));

await sendVSCodeKeys("gg");
await sendVSCodeKeys("/bla");
await assertContent({ cursor: [115, 19] }, client);
await sendNeovimKeys(client, "<cr>");
await assertContent({ cursor: [115, 16] }, client);
await sendNeovimKeys(client, "/h2");
await client.command("mode");
await wait(500);
await assertContent({ cursor: [170, 21] }, client);
});
});
11 changes: 0 additions & 11 deletions src/test/integ/vscode-integration-specific.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,17 +306,6 @@ describe("VSCode integration specific stuff", () => {
);
});

it("Cursor is ok for incsearch after scroll", async () => {
const e = await openTextDocument(path.join(__dirname, "../../../test_fixtures/incsearch-scroll.ts"));

await sendVSCodeKeys("gg");
await wait(500);
await sendVSCodeKeys("/bla");
await wait(500);
await assertContent({ cursor: [115, 19] }, client);
assert.ok(e.visibleRanges[0].start.line <= 115);
});

// !Passes only when the runner is in foreground
it("Cursor is preserved if same doc is opened in two editor columns", async () => {
const doc = (
Expand Down
Loading

0 comments on commit 5fedba5

Please sign in to comment.