@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/CHANGELOG.md +18 -0
- package/dist/src/server/dirtree/index.js +1 -1
- package/dist/src/server/dirtree/index.test.js +7 -7
- package/dist/src/server/neovim/NeovimApplication.d.ts +1 -1
- package/dist/src/server/neovim/NeovimApplication.js +28 -7
- package/dist/src/server/neovim/NeovimJavascriptApiClient.d.ts +5 -0
- package/dist/src/server/neovim/NeovimJavascriptApiClient.js +21 -0
- package/dist/src/server/neovim/NeovimJavascriptApiClient.test.d.ts +1 -0
- package/dist/src/server/neovim/NeovimJavascriptApiClient.test.js +33 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -3
- package/src/server/dirtree/index.test.ts +7 -7
- package/src/server/dirtree/index.ts +1 -1
- package/src/server/neovim/NeovimApplication.ts +44 -7
- package/src/server/neovim/NeovimJavascriptApiClient.test.ts +42 -0
- package/src/server/neovim/NeovimJavascriptApiClient.ts +27 -0
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tui-sandbox/library",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@catppuccin/palette": "1.
|
|
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.
|
|
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({
|
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
+
}
|