diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1f623e3..571d369 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,8 +23,8 @@ jobs: node-version-file: .nvmrc cache: "pnpm" - run: pnpm install + - run: pnpm build - uses: reviewdog/action-eslint@v1.32.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} reporter: github-pr-review # Change reporter. - - run: pnpm build diff --git a/packages/integration-tests/cypress/e2e/neovim.cy.ts b/packages/integration-tests/cypress/e2e/neovim.cy.ts index f92a5dd..211621d 100644 --- a/packages/integration-tests/cypress/e2e/neovim.cy.ts +++ b/packages/integration-tests/cypress/e2e/neovim.cy.ts @@ -120,4 +120,55 @@ describe("neovim features", () => { }) }) }) + + it("can run lua code and get its result", () => { + cy.visit("/") + cy.startNeovim().then(() => { + // wait until text on the start screen is visible + cy.contains("If you see this text, Neovim is ready!") + + // can do math + cy.runLuaCode({ luaCode: "return 40 + 2" }).then(result => { + expect(result.value).to.equal(42) + }) + + // can return strings + cy.runLuaCode({ luaCode: "return 'hello from lua'" }).then(result => { + expect(result.value).to.equal("hello from lua") + }) + + // can use nvim apis + cy.runLuaCode({ luaCode: "return vim.api.nvim_get_current_win()" }).then(result => { + expect(result.value).to.equal(1000) + }) + + // does not return anything if the lua code does not return anything. + // But side effects are visible in the neovim instance. + cy.runLuaCode({ luaCode: `vim.api.nvim_command('echo "hello from lua"')` }).then(result => { + expect(result.value).to.equal(null) + }) + cy.contains("hello from lua") + + // can programmatically manipulate the buffer + cy.runLuaCode({ + luaCode: ` + vim.api.nvim_buf_set_lines(0, 0, -1, false, {'hello', 'from', 'lua'}) + `, + }) + cy.contains("If you see this text, Neovim is ready!").should("not.exist") + cy.contains("hello") + cy.contains("from") + cy.contains("lua") + + // can return tables + cy.runLuaCode({ luaCode: "return {1, 2, 3}" }).then(result => { + expect(result.value).to.deep.equal([1, 2, 3]) + }) + + // can return nested tables + cy.runLuaCode({ luaCode: "return {1, {2, 3}, 4}" }).then(result => { + expect(result.value).to.deep.equal([1, [2, 3], 4]) + }) + }) + }) }) diff --git a/packages/integration-tests/cypress/support/tui-sandbox.ts b/packages/integration-tests/cypress/support/tui-sandbox.ts index 3f285bd..a638332 100644 --- a/packages/integration-tests/cypress/support/tui-sandbox.ts +++ b/packages/integration-tests/cypress/support/tui-sandbox.ts @@ -2,9 +2,10 @@ // // This file is autogenerated by tui-sandbox. Do not edit it directly. // -import type { BlockingCommandClientInput } from "@tui-sandbox/library/dist/src/server/server" +import type { BlockingCommandClientInput, LuaCodeClientInput } from "@tui-sandbox/library/dist/src/server/server" import type { BlockingShellCommandOutput, + RunLuaCodeOutput, StartNeovimGenericArguments, } from "@tui-sandbox/library/dist/src/server/types" import type { OverrideProperties } from "type-fest" @@ -19,6 +20,7 @@ declare global { interface Window { startNeovim(startArguments?: MyStartNeovimServerArguments): Promise runBlockingShellCommand(input: BlockingCommandClientInput): Promise + runLuaCode(input: LuaCodeClientInput): Promise } } @@ -49,6 +51,12 @@ Cypress.Commands.add("typeIntoTerminal", (text: string, options?: Partial { + cy.window().then(async win => { + return await win.runLuaCode(input) + }) +}) + before(function () { // disable Cypress's default behavior of logging all XMLHttpRequests and // fetches to the Command Log @@ -68,6 +76,8 @@ declare global { /** Runs a shell command in a blocking manner, waiting for the command to * finish before returning. Requires neovim to be running. */ runBlockingShellCommand(input: BlockingCommandClientInput): Chainable + + runLuaCode(input: LuaCodeClientInput): Chainable } } } diff --git a/packages/library/src/browser/neovim-client.ts b/packages/library/src/browser/neovim-client.ts index 69920a8..97e3278 100644 --- a/packages/library/src/browser/neovim-client.ts +++ b/packages/library/src/browser/neovim-client.ts @@ -1,6 +1,11 @@ import { TerminalClient } from "../client/index.js" -import type { BlockingCommandClientInput } from "../server/server.js" -import type { BlockingShellCommandOutput, StartNeovimGenericArguments, TestDirectory } from "../server/types.js" +import type { BlockingCommandClientInput, LuaCodeClientInput } from "../server/server.js" +import type { + BlockingShellCommandOutput, + RunLuaCodeOutput, + StartNeovimGenericArguments, + TestDirectory, +} from "../server/types.js" const app = document.querySelector("#app") if (!app) { @@ -26,9 +31,14 @@ window.runBlockingShellCommand = async function ( return client.runBlockingShellCommand(input) } +window.runLuaCode = async function (input: LuaCodeClientInput): Promise { + return client.runLuaCode(input) +} + declare global { interface Window { startNeovim(startArguments?: StartNeovimGenericArguments): Promise runBlockingShellCommand(input: BlockingCommandClientInput): Promise + runLuaCode(input: LuaCodeClientInput): Promise } } diff --git a/packages/library/src/client/terminal-client.ts b/packages/library/src/client/terminal-client.ts index aa7eb39..4316563 100644 --- a/packages/library/src/client/terminal-client.ts +++ b/packages/library/src/client/terminal-client.ts @@ -1,8 +1,13 @@ import { createTRPCClient, httpBatchLink, splitLink, unstable_httpSubscriptionLink } from "@trpc/client" import type { Terminal } from "@xterm/xterm" import "@xterm/xterm/css/xterm.css" -import type { AppRouter, BlockingCommandClientInput } from "../server/server.js" -import type { BlockingShellCommandOutput, StartNeovimGenericArguments, TestDirectory } from "../server/types.js" +import type { AppRouter, BlockingCommandClientInput, LuaCodeClientInput } from "../server/server.js" +import type { + BlockingShellCommandOutput, + RunLuaCodeOutput, + StartNeovimGenericArguments, + TestDirectory, +} from "../server/types.js" import "./style.css" import { getTabId, startTerminal } from "./websocket-client.js" @@ -92,4 +97,12 @@ export class TerminalClient { tabId: this.tabId, }) } + + public async runLuaCode(input: LuaCodeClientInput): Promise { + await this.ready + return this.trpc.neovim.runLuaCode.mutate({ + luaCode: input.luaCode, + tabId: this.tabId, + }) + } } diff --git a/packages/library/src/server/cypress-support/contents.test.ts b/packages/library/src/server/cypress-support/contents.test.ts index 366d754..51214ad 100644 --- a/packages/library/src/server/cypress-support/contents.test.ts +++ b/packages/library/src/server/cypress-support/contents.test.ts @@ -7,9 +7,10 @@ it("should return the expected contents", async () => { // // This file is autogenerated by tui-sandbox. Do not edit it directly. // - import type { BlockingCommandClientInput } from "@tui-sandbox/library/dist/src/server/server" + import type { BlockingCommandClientInput, LuaCodeClientInput } from "@tui-sandbox/library/dist/src/server/server" import type { BlockingShellCommandOutput, + RunLuaCodeOutput, StartNeovimGenericArguments, } from "@tui-sandbox/library/dist/src/server/types" import type { OverrideProperties } from "type-fest" @@ -24,6 +25,7 @@ it("should return the expected contents", async () => { interface Window { startNeovim(startArguments?: MyStartNeovimServerArguments): Promise runBlockingShellCommand(input: BlockingCommandClientInput): Promise + runLuaCode(input: LuaCodeClientInput): Promise } } @@ -54,6 +56,12 @@ it("should return the expected contents", async () => { cy.get("textarea").focus().type(text, options) }) + Cypress.Commands.add("runLuaCode", (input: LuaCodeClientInput) => { + cy.window().then(async win => { + return await win.runLuaCode(input) + }) + }) + before(function () { // disable Cypress's default behavior of logging all XMLHttpRequests and // fetches to the Command Log @@ -73,6 +81,8 @@ it("should return the expected contents", async () => { /** Runs a shell command in a blocking manner, waiting for the command to * finish before returning. Requires neovim to be running. */ runBlockingShellCommand(input: BlockingCommandClientInput): Chainable + + runLuaCode(input: LuaCodeClientInput): Chainable } } } diff --git a/packages/library/src/server/cypress-support/contents.ts b/packages/library/src/server/cypress-support/contents.ts index 44b094b..00adcb4 100644 --- a/packages/library/src/server/cypress-support/contents.ts +++ b/packages/library/src/server/cypress-support/contents.ts @@ -6,14 +6,15 @@ const __filename = fileURLToPath(import.meta.url) export async function createCypressSupportFileContents(): Promise { // this is the interface of tui-sandbox as far as cypress in the user's // application is concerned - let text = ` + let text = ` /// // // This file is autogenerated by tui-sandbox. Do not edit it directly. // -import type { BlockingCommandClientInput } from "@tui-sandbox/library/dist/src/server/server" +import type { BlockingCommandClientInput, LuaCodeClientInput } from "@tui-sandbox/library/dist/src/server/server" import type { BlockingShellCommandOutput, + RunLuaCodeOutput, StartNeovimGenericArguments, } from "@tui-sandbox/library/dist/src/server/types" import type { OverrideProperties } from "type-fest" @@ -28,6 +29,7 @@ declare global { interface Window { startNeovim(startArguments?: MyStartNeovimServerArguments): Promise runBlockingShellCommand(input: BlockingCommandClientInput): Promise + runLuaCode(input: LuaCodeClientInput): Promise } } @@ -58,6 +60,12 @@ Cypress.Commands.add("typeIntoTerminal", (text: string, options?: Partial { + cy.window().then(async win => { + return await win.runLuaCode(input) + }) +}) + before(function () { // disable Cypress's default behavior of logging all XMLHttpRequests and // fetches to the Command Log @@ -77,6 +85,8 @@ declare global { /** Runs a shell command in a blocking manner, waiting for the command to * finish before returning. Requires neovim to be running. */ runBlockingShellCommand(input: BlockingCommandClientInput): Chainable + + runLuaCode(input: LuaCodeClientInput): Chainable } } } diff --git a/packages/library/src/server/neovim/index.ts b/packages/library/src/server/neovim/index.ts index d984537..ead37f6 100644 --- a/packages/library/src/server/neovim/index.ts +++ b/packages/library/src/server/neovim/index.ts @@ -2,8 +2,13 @@ import assert from "assert" import { exec } from "child_process" import "core-js/proposals/async-explicit-resource-management.js" import util from "util" -import type { BlockingCommandInput } from "../server.js" -import type { BlockingShellCommandOutput, StartNeovimGenericArguments, TestDirectory } from "../types.js" +import type { BlockingCommandInput, LuaCodeInput } from "../server.js" +import type { + BlockingShellCommandOutput, + RunLuaCodeOutput, + StartNeovimGenericArguments, + TestDirectory, +} from "../types.js" import type { TestServerConfig } from "../updateTestdirectorySchemaFile.js" import { convertEventEmitterToAsyncGenerator } from "../utilities/generator.js" import type { TabId } from "../utilities/tabId.js" @@ -110,3 +115,29 @@ export async function runBlockingShellCommand( throw new Error(`Error running shell blockingCommand (${input.command})`, { cause: e }) } } + +export async function runLuaCode(options: LuaCodeInput): Promise { + const neovim = neovims.get(options.tabId.tabId) + assert( + neovim !== undefined, + `Neovim instance for clientId not found - cannot run Lua code. Maybe neovim's not started yet?` + ) + assert( + neovim.application, + `Neovim application not found for client id ${options.tabId.tabId}. Maybe it's not started yet?` + ) + + const api = await neovim.state?.client.get() + if (!api) { + throw new Error(`Neovim API not available for client id ${options.tabId.tabId}. Maybe it's not started yet?`) + } + + console.log(`Neovim ${neovim.application.processId()} running Lua code: ${options.luaCode}`) + try { + const value = await api.lua(options.luaCode) + return { value } + } catch (e) { + console.warn(`Error running Lua code: ${options.luaCode}`, e) + throw new Error(`Error running Lua code: ${options.luaCode}`, { cause: e }) + } +} diff --git a/packages/library/src/server/server.ts b/packages/library/src/server/server.ts index 6fc4411..3e5b16d 100644 --- a/packages/library/src/server/server.ts +++ b/packages/library/src/server/server.ts @@ -25,6 +25,10 @@ const blockingCommandInputSchema = z.object({ export type BlockingCommandClientInput = Except export type BlockingCommandInput = z.infer +const luaCodeInputSchema = z.object({ tabId: tabIdSchema, luaCode: z.string() }) +export type LuaCodeClientInput = Except +export type LuaCodeInput = z.infer + /** @private */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export async function createAppRouter(config: TestServerConfig) { @@ -72,6 +76,10 @@ export async function createAppRouter(config: TestServerConfig) { runBlockingShellCommand: trpc.procedure.input(blockingCommandInputSchema).mutation(async options => { return neovim.runBlockingShellCommand(options.signal, options.input, options.input.allowFailure ?? false) }), + + runLuaCode: trpc.procedure.input(luaCodeInputSchema).mutation(options => { + return neovim.runLuaCode(options.input) + }), }), }) diff --git a/packages/library/src/server/types.ts b/packages/library/src/server/types.ts index dd2a641..c098e6a 100644 --- a/packages/library/src/server/types.ts +++ b/packages/library/src/server/types.ts @@ -1,3 +1,5 @@ +import type { VimValue } from "neovim/lib/types/VimValue.js" + /** Describes the contents of the test directory, which is a blueprint for * files and directories. Tests can create a unique, safe environment for * interacting with the contents of such a directory. @@ -42,3 +44,8 @@ export type BlockingShellCommandOutput = // for now we log the error to the server's console output. It will be // visible when running the tests. } + +export type RunLuaCodeOutput = { + value?: VimValue + // to catch errors, use pcall() in the Lua code +}