@tui-sandbox/library 2.2.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@tui-sandbox/library",
3
- "version": "2.2.0",
3
+ "version": "3.0.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "dependencies": {
7
- "@catppuccin/palette": "1.5.0",
7
+ "@catppuccin/palette": "1.7.1",
8
8
  "@trpc/client": "11.0.0-rc.608",
9
9
  "@trpc/server": "11.0.0-rc.608",
10
10
  "@xterm/addon-attach": "0.11.0",
@@ -14,6 +14,7 @@
14
14
  "core-js": "3.39.0",
15
15
  "cors": "2.8.5",
16
16
  "dree": "5.1.5",
17
+ "neovim": "5.3.0",
17
18
  "node-pty": "1.0.0",
18
19
  "prettier": "3.3.3",
19
20
  "type-fest": "4.26.1",
@@ -24,7 +25,7 @@
24
25
  "@runtyping/zod": "2.1.1",
25
26
  "@types/command-exists": "1.2.3",
26
27
  "@types/cors": "2.8.17",
27
- "@types/node": "22.8.6",
28
+ "@types/node": "22.9.0",
28
29
  "nodemon": "3.1.7",
29
30
  "vitest": "2.1.4"
30
31
  },
@@ -36,11 +36,11 @@ describe("dirtree", () => {
36
36
  import { z } from "zod"
37
37
 
38
38
  export const MyDirectoryTreeSchema = z.object({
39
- name: z.literal("test-environment"),
39
+ name: z.literal("test-environment/"),
40
40
  type: z.literal("directory"),
41
41
  contents: z.object({
42
42
  "config-modifications": z.object({
43
- name: z.literal("config-modifications"),
43
+ name: z.literal("config-modifications/"),
44
44
  type: z.literal("directory"),
45
45
  contents: z.object({
46
46
  "add_command_to_count_open_buffers.lua": z.object({
@@ -52,7 +52,7 @@ describe("dirtree", () => {
52
52
  }),
53
53
  }),
54
54
  "dir with spaces": z.object({
55
- name: z.literal("dir with spaces"),
55
+ name: z.literal("dir with spaces/"),
56
56
  type: z.literal("directory"),
57
57
  contents: z.object({
58
58
  "file1.txt": z.object({
@@ -82,7 +82,7 @@ describe("dirtree", () => {
82
82
  stem: z.literal("initial-file."),
83
83
  }),
84
84
  "other-subdirectory": z.object({
85
- name: z.literal("other-subdirectory"),
85
+ name: z.literal("other-subdirectory/"),
86
86
  type: z.literal("directory"),
87
87
  contents: z.object({
88
88
  "other-sub-file.txt": z.object({
@@ -94,11 +94,11 @@ describe("dirtree", () => {
94
94
  }),
95
95
  }),
96
96
  routes: z.object({
97
- name: z.literal("routes"),
97
+ name: z.literal("routes/"),
98
98
  type: z.literal("directory"),
99
99
  contents: z.object({
100
100
  "posts.$postId": z.object({
101
- name: z.literal("posts.$postId"),
101
+ name: z.literal("posts.$postId/"),
102
102
  type: z.literal("directory"),
103
103
  contents: z.object({
104
104
  "adjacent-file.txt": z.object({
@@ -124,7 +124,7 @@ describe("dirtree", () => {
124
124
  }),
125
125
  }),
126
126
  subdirectory: z.object({
127
- name: z.literal("subdirectory"),
127
+ name: z.literal("subdirectory/"),
128
128
  type: z.literal("directory"),
129
129
  contents: z.object({
130
130
  "subdirectory-file.txt": z.object({
@@ -55,7 +55,7 @@ export function convertDree(root: Dree): TreeNode {
55
55
  }
56
56
 
57
57
  const node: DirectoryNode = {
58
- name: root.name,
58
+ name: `${root.name}/`,
59
59
  type: root.type,
60
60
  contents: {},
61
61
  }
@@ -1,11 +1,15 @@
1
1
  import assert from "assert"
2
2
  import { exec } from "child_process"
3
3
  import EventEmitter from "events"
4
- import { existsSync } from "fs"
4
+ import { access } from "fs/promises"
5
+ import type { NeovimClient as NeovimApiClient } from "neovim"
6
+ import { tmpdir } from "os"
5
7
  import path from "path"
6
8
  import type { TestDirectory } from "../types"
7
9
  import { DisposableSingleApplication } from "../utilities/DisposableSingleApplication"
10
+ import type { Lazy } from "../utilities/Lazy"
8
11
  import { TerminalApplication } from "../utilities/TerminalApplication"
12
+ import { connectNeovimApi } from "./NeovimJavascriptApiClient"
9
13
 
10
14
  /*
11
15
 
@@ -60,8 +64,14 @@ export type StartNeovimGenericArguments = {
60
64
  startupScriptModifications?: string[]
61
65
  }
62
66
 
67
+ type ResettableState = {
68
+ testDirectory: TestDirectory
69
+ socketPath: string
70
+ client: Lazy<Promise<NeovimApiClient>>
71
+ }
72
+
63
73
  export class NeovimApplication {
64
- private testDirectory: TestDirectory | undefined
74
+ private state: ResettableState | undefined
65
75
  public readonly events: EventEmitter
66
76
 
67
77
  public constructor(
@@ -79,15 +89,20 @@ export class NeovimApplication {
79
89
  startArgs: StartNeovimGenericArguments
80
90
  ): Promise<void> {
81
91
  await this[Symbol.asyncDispose]()
82
- this.testDirectory = testDirectory
92
+ assert(
93
+ this.state === undefined,
94
+ "NeovimApplication state should be undefined after disposing so that no previous state is reused."
95
+ )
83
96
 
84
97
  const neovimArguments = ["-u", "test-setup.lua"]
85
98
 
86
99
  if (startArgs.startupScriptModifications) {
87
100
  for (const modification of startArgs.startupScriptModifications) {
88
101
  const file = path.join(testDirectory.rootPathAbsolute, "config-modifications", modification)
89
- if (!existsSync(file)) {
90
- throw new Error(`startupScriptModifications file does not exist: ${file}`)
102
+ try {
103
+ await access(file)
104
+ } catch (e) {
105
+ throw new Error(`startupScriptModifications file does not exist: ${file}. Error: ${String(e)}`)
91
106
  }
92
107
 
93
108
  neovimArguments.push("-c", `lua dofile('${file}')`)
@@ -106,6 +121,11 @@ export class NeovimApplication {
106
121
  neovimArguments.push(filePath)
107
122
  }
108
123
  }
124
+
125
+ const id = Math.random().toString().slice(2, 8)
126
+ const socketPath = `${tmpdir()}/tui-sandbox-nvim-socket-${id}`
127
+ neovimArguments.push("--listen", socketPath)
128
+
109
129
  const stdout = this.events
110
130
 
111
131
  await this.application.startNextAndKillCurrent(async () => {
@@ -126,13 +146,30 @@ export class NeovimApplication {
126
146
 
127
147
  const processId = this.application.processId()
128
148
  assert(processId !== undefined, "Neovim was started without a process ID. This is a bug - please open an issue.")
149
+
150
+ this.state = {
151
+ testDirectory,
152
+ socketPath,
153
+ client: connectNeovimApi(socketPath),
154
+ }
155
+
129
156
  console.log(`🚀 Started Neovim instance ${processId}`)
130
157
  }
131
158
 
132
159
  async [Symbol.asyncDispose](): Promise<void> {
133
160
  await this.application[Symbol.asyncDispose]()
134
- if (this.testDirectory) {
135
- exec(`rm -rf ${this.testDirectory.rootPathAbsolute}`)
161
+
162
+ if (!this.state) return
163
+
164
+ exec(`rm -rf ${this.state.testDirectory.rootPathAbsolute}`)
165
+
166
+ try {
167
+ await access(this.state.socketPath)
168
+ throw new Error(`Socket file ${this.state.socketPath} should have been removed by neovim when it exited.`)
169
+ } catch (e) {
170
+ // all good
136
171
  }
172
+
173
+ this.state = undefined
137
174
  }
138
175
  }
@@ -0,0 +1,42 @@
1
+ import { access } from "fs/promises"
2
+ import { attach } from "neovim"
3
+ import type { PollingInterval } from "./NeovimJavascriptApiClient"
4
+ import { connectNeovimApi } from "./NeovimJavascriptApiClient"
5
+
6
+ vi.mock("neovim")
7
+ vi.mock("fs/promises")
8
+
9
+ const mocked = {
10
+ attach: vi.mocked(attach),
11
+ access: vi.mocked(access),
12
+ log: vi.spyOn(console, "log").mockImplementation(() => {
13
+ //
14
+ }),
15
+ }
16
+ const pollingInterval = 100 satisfies PollingInterval
17
+
18
+ beforeEach(() => {
19
+ vi.useFakeTimers()
20
+ })
21
+
22
+ afterEach(() => {
23
+ vi.useRealTimers()
24
+ })
25
+
26
+ it("is lazy - does not connect right away", async () => {
27
+ mocked.access.mockRejectedValue(new Error("no such file or directory"))
28
+ connectNeovimApi("foosocket")
29
+
30
+ vi.advanceTimersByTime(pollingInterval)
31
+ expect(mocked.attach).not.toHaveBeenCalled()
32
+ })
33
+
34
+ it("connects right away if the socket file is already there", async () => {
35
+ mocked.access.mockResolvedValue(undefined)
36
+ const lazyClient = connectNeovimApi("foosocket")
37
+ await lazyClient.get()
38
+
39
+ vi.advanceTimersByTime(pollingInterval)
40
+ expect(mocked.attach).toHaveBeenCalledWith({ socket: "foosocket" })
41
+ expect(mocked.attach).toHaveBeenCalledTimes(1)
42
+ })
@@ -0,0 +1,27 @@
1
+ import { access } from "fs/promises"
2
+ import type { NeovimClient as NeovimApiClient } from "neovim"
3
+ import { attach } from "neovim"
4
+ import { Lazy } from "../utilities/Lazy"
5
+
6
+ export type NeovimJavascriptApiClient = NeovimApiClient
7
+
8
+ export type PollingInterval = 100
9
+
10
+ export function connectNeovimApi(socketPath: string): Lazy<Promise<NeovimJavascriptApiClient>> {
11
+ // it takes about 100ms for the socket file to be created - best make this
12
+ // Lazy so that we don't wait for it unnecessarily.
13
+ return new Lazy(async () => {
14
+ for (let i = 0; i < 100; i++) {
15
+ try {
16
+ await access(socketPath)
17
+ console.log(`socket file ${socketPath} created after at attempt ${i + 1}`)
18
+ break
19
+ } catch (e) {
20
+ console.log(`polling for socket file ${socketPath} to be created (attempt ${i + 1})`)
21
+ await new Promise(resolve => setTimeout(resolve, 100 satisfies PollingInterval))
22
+ }
23
+ }
24
+
25
+ return attach({ socket: socketPath })
26
+ })
27
+ }