-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
👍 Add
popup
module as a compatibility layer for popup window in Vim…
… and Neovim.
- Loading branch information
1 parent
188d796
commit 85ad739
Showing
5 changed files
with
668 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
/** | ||
* A module to provide compatibility layer for popup window in Vim and Neovim. | ||
* | ||
* ```typescript | ||
* import type { Denops } from "https://deno.land/x/denops_std@$MODULE_VERSION/mod.ts"; | ||
* import * as buffer from "https://deno.land/x/denops_std@$MODULE_VERSION/buffer/mod.ts"; | ||
* import * as fn from "https://deno.land/x/denops_std@$MODULE_VERSION/function/mod.ts"; | ||
* import * as popup from "https://deno.land/x/denops_std@$MODULE_VERSION/popup/mod.ts"; | ||
* | ||
* export async function main(denops: Denops): Promise<void> { | ||
* // Create a new buffer | ||
* const bufnr = await fn.bufadd(denops, ""); | ||
* await fn.bufload(denops, bufnr); | ||
* | ||
* // Write some text to the buffer | ||
* await buffer.replace(denops, bufnr, ["Hello, world!"]); | ||
* | ||
* // Open a popup window showing the buffer | ||
* const popupWindow = await popup.open(denops, { | ||
* bufnr, | ||
* relative: "editor", | ||
* width: 20, | ||
* height: 20, | ||
* row: 1, | ||
* col: 1, | ||
* }); | ||
* | ||
* // Wiat 3 seconds | ||
* await new Promise((resolve) => setTimeout(resolve, 3000)); | ||
* | ||
* // Close the popup window | ||
* await popupWindow.close(); | ||
* } | ||
* ``` | ||
* | ||
* Or with `await using` statement: | ||
* | ||
* ```typescript | ||
* import type { Denops } from "https://deno.land/x/denops_std@$MODULE_VERSION/mod.ts"; | ||
* import * as buffer from "https://deno.land/x/denops_std@$MODULE_VERSION/buffer/mod.ts"; | ||
* import * as fn from "https://deno.land/x/denops_std@$MODULE_VERSION/function/mod.ts"; | ||
* import * as popup from "https://deno.land/x/denops_std@$MODULE_VERSION/popup/mod.ts"; | ||
* | ||
* export async function main(denops: Denops): Promise<void> { | ||
* // Create a new buffer | ||
* const bufnr = await fn.bufadd(denops, ""); | ||
* await fn.bufload(denops, bufnr); | ||
* | ||
* // Write some text to the buffer | ||
* await buffer.replace(denops, bufnr, ["Hello, world!"]); | ||
* | ||
* // Open a popup window showing the buffer | ||
* await using popupWindow = await popup.open(denops, { | ||
* bufnr, | ||
* relative: "editor", | ||
* width: 20, | ||
* height: 20, | ||
* row: 1, | ||
* col: 1, | ||
* }); | ||
* | ||
* // Wiat 3 seconds | ||
* await new Promise((resolve) => setTimeout(resolve, 3000)); | ||
* | ||
* // The popup window is automatically closed, due to `await using` statement | ||
* } | ||
* ``` | ||
* | ||
* Note that this module does NOT work with `batch.collect()`. | ||
* | ||
* @module | ||
*/ | ||
|
||
import type { Denops } from "../mod.ts"; | ||
import * as fn from "../function/mod.ts"; | ||
|
||
import type { OpenOptions, PopupWindow } from "./types.ts"; | ||
import { | ||
closePopup as closePopupVim, | ||
openPopup as openPopupVim, | ||
} from "./vim.ts"; | ||
import { | ||
closePopup as closePopupNvim, | ||
openPopup as openPopupNvim, | ||
} from "./nvim.ts"; | ||
|
||
/** | ||
* Open a popup window showing the buffer in Vim/Neovim compatible way. | ||
* | ||
* ```typescript | ||
* import type { Denops } from "https://deno.land/x/denops_std@$MODULE_VERSION/mod.ts"; | ||
* import * as popup from "https://deno.land/x/denops_std@$MODULE_VERSION/popup/mod.ts"; | ||
* | ||
* export async function main(denops: Denops): Promise<void> { | ||
* // Open a popup window | ||
* const popupWindow = await popup.open(denops, { | ||
* relative: "editor", | ||
* width: 20, | ||
* height: 20, | ||
* row: 1, | ||
* col: 1, | ||
* }); | ||
* | ||
* // Do something with the popup window... | ||
* | ||
* // Close the popup window manually | ||
* await popupWindow.close(); | ||
* } | ||
* ``` | ||
* | ||
* Or with `await using` statement: | ||
* | ||
* ```typescript | ||
* import type { Denops } from "https://deno.land/x/denops_std@$MODULE_VERSION/mod.ts"; | ||
* import * as popup from "https://deno.land/x/denops_std@$MODULE_VERSION/popup/mod.ts"; | ||
* | ||
* export async function main(denops: Denops): Promise<void> { | ||
* // Open a popup window with `await using` statement | ||
* await using popupWindow = await popup.open(denops, { | ||
* relative: "editor", | ||
* width: 20, | ||
* height: 20, | ||
* row: 1, | ||
* col: 1, | ||
* }); | ||
* | ||
* // Do something with the popup window... | ||
* | ||
* // The popup window is automatically closed, due to `await using` statement | ||
* } | ||
* ``` | ||
* | ||
* Note that this function does NOT work in `batch.collect()`. | ||
*/ | ||
export async function open( | ||
denops: Denops, | ||
options: OpenOptions, | ||
): Promise<PopupWindow> { | ||
if (options.title && !options.border) { | ||
// Vim allows `title` without `border`, but Neovim does not. | ||
// so we throw an error here to keep consistent behavior. | ||
throw new Error("title requires border to be set"); | ||
} | ||
const open = denops.meta.host === "vim" ? openPopupVim : openPopupNvim; | ||
const close = denops.meta.host === "vim" ? closePopupVim : closePopupNvim; | ||
const bufnr = options.bufnr ?? await fn.bufadd(denops, ""); | ||
const winid = await open(denops, bufnr, options); | ||
if (!options.noRedraw) { | ||
await denops.redraw(); | ||
} | ||
return { | ||
bufnr, | ||
winid, | ||
close: async () => { | ||
if (!options.noRedraw) { | ||
await close(denops, winid); | ||
} | ||
await denops.redraw(); | ||
}, | ||
[Symbol.asyncDispose]: async () => { | ||
await close(denops, winid); | ||
await denops.redraw(); | ||
}, | ||
}; | ||
} | ||
|
||
export type { OpenOptions, PopupWindow } from "./types.ts"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import { assertEquals } from "https://deno.land/std@0.217.0/assert/mod.ts"; | ||
import { test } from "https://deno.land/x/denops_test@v1.6.2/mod.ts"; | ||
import * as fn from "../function/mod.ts"; | ||
import * as popup from "./mod.ts"; | ||
|
||
test({ | ||
mode: "all", | ||
name: "popup", | ||
fn: async (denops, t) => { | ||
// Extend screen size | ||
await denops.cmd("set lines=100 columns=100"); | ||
|
||
await t.step({ | ||
name: `open() opens a popup window and close() closes it`, | ||
fn: async () => { | ||
let popupWindow: popup.PopupWindow; | ||
const inner = async () => { | ||
popupWindow = await popup.open(denops, { | ||
relative: "editor", | ||
width: 50, | ||
height: 50, | ||
row: 1, | ||
col: 1, | ||
}); | ||
const { winid } = popupWindow; | ||
assertEquals(await fn.win_gettype(denops, winid), "popup"); | ||
assertEquals(await fn.winwidth(denops, winid), 50); | ||
assertEquals(await fn.winheight(denops, winid), 50); | ||
}; | ||
await inner(); | ||
|
||
// Still alive | ||
assertEquals(await fn.win_gettype(denops, popupWindow!.winid), "popup"); | ||
|
||
// Explicitly close | ||
await popupWindow!.close(); | ||
assertEquals( | ||
await fn.win_gettype(denops, popupWindow!.winid), | ||
"unknown", | ||
); | ||
}, | ||
}); | ||
|
||
await t.step({ | ||
name: `open() with await using statement`, | ||
fn: async () => { | ||
let winid: number; | ||
const inner = async () => { | ||
await using popupWindow = await popup.open(denops, { | ||
relative: "editor", | ||
width: 50, | ||
height: 50, | ||
row: 1, | ||
col: 1, | ||
}); | ||
winid = popupWindow.winid; | ||
assertEquals(await fn.win_gettype(denops, winid), "popup"); | ||
assertEquals(await fn.winwidth(denops, winid), 50); | ||
assertEquals(await fn.winheight(denops, winid), 50); | ||
}; | ||
await inner(); | ||
|
||
// Automatically disposed | ||
assertEquals(await fn.win_gettype(denops, winid!), "unknown"); | ||
}, | ||
}); | ||
|
||
// With numerous options | ||
const base: popup.OpenOptions = { | ||
relative: "editor", | ||
width: 20, // Max 20 in test | ||
height: 20, // Max 24 in test | ||
row: 50, | ||
col: 50, | ||
}; | ||
const optionsSet: popup.OpenOptions[] = [ | ||
{ ...base }, | ||
{ ...base, relative: "cursor" }, | ||
{ ...base, anchor: "NW" }, | ||
{ ...base, anchor: "NE" }, | ||
{ ...base, anchor: "SW" }, | ||
{ ...base, anchor: "SE" }, | ||
{ ...base, zindex: 100 }, | ||
{ ...base, border: "single" }, | ||
{ ...base, border: "double" }, | ||
{ ...base, border: "rounded" }, | ||
{ ...base, border: ["", "", "", "|", "", "", "", "|"] }, | ||
{ ...base, border: "single", title: "Hello world!" }, | ||
{ ...base, highlight: {} }, | ||
{ ...base, highlight: { normal: "Normal" } }, | ||
{ ...base, highlight: { border: "Border" } }, | ||
{ ...base, highlight: { normal: "Normal", border: "Border" } }, | ||
]; | ||
for (const options of optionsSet) { | ||
await t.step({ | ||
name: `open() with options: ${JSON.stringify(options)}`, | ||
fn: async () => { | ||
await using popupWindow = await popup.open(denops, options); | ||
assertEquals( | ||
await fn.win_gettype(denops, popupWindow.winid), | ||
"popup", | ||
); | ||
assertEquals(await fn.winwidth(denops, popupWindow.winid), 20); | ||
assertEquals(await fn.winheight(denops, popupWindow.winid), 20); | ||
}, | ||
}); | ||
} | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import type { Denops } from "../mod.ts"; | ||
import * as nvimFn from "../function/nvim/mod.ts"; | ||
import { execute } from "../helper/execute.ts"; | ||
import { ulid } from "https://deno.land/std@0.217.0/ulid/mod.ts"; | ||
|
||
import type { Border, OpenOptions } from "./types.ts"; | ||
|
||
const cacheKey = "denops_std/popup/nvim.ts@1"; | ||
|
||
async function ensurePrerequisites(denops: Denops): Promise<string> { | ||
if (typeof denops.context[cacheKey] === "string") { | ||
return denops.context[cacheKey]; | ||
} | ||
const suffix = ulid(); | ||
denops.context[cacheKey] = suffix; | ||
const script = ` | ||
function! DenopsStdPopupNvimOpenPopup_${suffix}(bufnr, config, winhighlight) abort | ||
let winid = nvim_open_win(a:bufnr, v:false, a:config) | ||
if a:winhighlight isnot v:null | ||
call nvim_win_set_option(winid, 'winhighlight', a:winhighlight) | ||
endif | ||
return winid | ||
endfunction | ||
`; | ||
await execute(denops, script); | ||
return suffix; | ||
} | ||
|
||
export async function openPopup( | ||
denops: Denops, | ||
bufnr: number, | ||
options: Omit<OpenOptions, "bufnr" | "noRedraw">, | ||
): Promise<number> { | ||
const suffix = await ensurePrerequisites(denops); | ||
const nvimOpenWinConfig = toNvimOpenWinConfig(options); | ||
const winhighlight = toNvimWinhighlight(options.highlight); | ||
return await denops.call( | ||
`DenopsStdPopupNvimOpenPopup_${suffix}`, | ||
bufnr, | ||
nvimOpenWinConfig, | ||
winhighlight, | ||
) as number; | ||
} | ||
|
||
export async function closePopup(denops: Denops, winid: number): Promise<void> { | ||
await nvimFn.nvim_win_close(denops, winid, true); | ||
} | ||
|
||
function toNvimOpenWinConfig(options: OpenOptions): nvimFn.NvimOpenWinConfig { | ||
const v: nvimFn.NvimOpenWinConfig = { | ||
relative: options.relative, | ||
anchor: options.anchor, | ||
width: options.width, | ||
height: options.height, | ||
col: options.col, | ||
row: options.row, | ||
focusable: false, // To keep consistent with the behavior of Vim's `popup_create()` | ||
zindex: options.zindex, | ||
border: options.border ? toNvimBorder(options.border) : undefined, | ||
title: options.title, | ||
}; | ||
return Object.fromEntries( | ||
Object | ||
.entries(v) | ||
.filter(([, v]) => v != undefined), | ||
) as nvimFn.NvimOpenWinConfig; | ||
} | ||
|
||
function toNvimBorder( | ||
border: Border, | ||
): nvimFn.NvimOpenWinConfig["border"] { | ||
if (typeof border === "string") { | ||
return border; | ||
} | ||
const [lt, t, rt, r, rb, b, lb, l] = border; | ||
return [lt, t, rt, r, rb, b, lb, l]; | ||
} | ||
|
||
function toNvimWinhighlight( | ||
highlight: OpenOptions["highlight"], | ||
): string | null { | ||
if (!highlight) { | ||
return null; | ||
} | ||
const { | ||
normal = "FloatNormal", | ||
border = "FloatBorder", | ||
} = highlight; | ||
if (normal && border) { | ||
return `Normal:${normal},FloatBorder:${border}`; | ||
} else if (normal) { | ||
return `Normal:${normal}`; | ||
} else if (border) { | ||
return `FloatBorder:${border}`; | ||
} | ||
return null; | ||
} |
Oops, something went wrong.