Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/neovim.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Neovim Plugin

# Lua unit tests (plenary) and the editor-independent conformance suite, which
# drives a headless Neovim through the real MCP server against a Go fixture
# debugged by delve.

on:
push:
branches: [ main, 'feat/**' ]
paths: [ 'editors/neovim/**', 'editors/conformance/**', 'editors/vscode/src/**', 'schema/**', '.github/workflows/neovim.yml' ]
pull_request:
paths: [ 'editors/neovim/**', 'editors/conformance/**', 'editors/vscode/src/**', 'schema/**', '.github/workflows/neovim.yml' ]
workflow_dispatch:

permissions:
contents: read

jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, macos-latest ]
runs-on: ${{ matrix.os }}
env:
NVIM_DAP_PATH: ${{ github.workspace }}/.deps/nvim-dap
PLENARY_PATH: ${{ github.workspace }}/.deps/plenary.nvim
steps:
- uses: actions/checkout@v7

- uses: rhysd/action-setup-vim@v1
with:
neovim: true
version: v0.10.4

- uses: actions/setup-node@v7
with:
node-version: 22

- uses: actions/setup-go@v7
with:
go-version: stable
cache: false

- name: Install delve
run: go install github.com/go-delve/delve/cmd/dlv@latest && echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"

- name: Fetch nvim-dap and plenary
run: |
git clone --depth 1 https://github.com/mfussenegger/nvim-dap "$NVIM_DAP_PATH"
git clone --depth 1 https://github.com/nvim-lua/plenary.nvim "$PLENARY_PATH"

- name: Lua unit tests
working-directory: editors/neovim
run: nvim --headless --noplugin -u tests/minimal_init.lua -c "PlenaryBustedDirectory tests/unit { minimal_init = 'tests/minimal_init.lua', sequential = true }"

# The conformance suite talks to the window through the real MCP server.
- name: Build the MCP server
working-directory: editors/vscode
run: npm ci && npm run compile

- name: Conformance (Neovim + delve)
working-directory: editors/conformance
run: npm ci && npm run test:nvim
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Thank you for your interest in contributing to Blackbox! We welcome contribution

## Releasing (maintainers)

Each VS Code extension version is a tag `vscode-v<version>`, a [GitHub Release](https://github.com/akash-aman/blackbox/releases) and an entry in [`editors/vscode/CHANGELOG.md`](editors/vscode/CHANGELOG.md).
Each VS Code extension version is a tag `vscode-v<version>`, a [GitHub Release](https://github.com/xcode-studio/blackbox/releases) and an entry in [`editors/vscode/CHANGELOG.md`](editors/vscode/CHANGELOG.md).

1. Bump `version` in `editors/vscode/package.json` (`npm version <x.y.z> --no-git-tag-version`).
2. In `editors/vscode/CHANGELOG.md`, rename `## Unreleased` (where changes collect, one bullet per user-visible change) to `## <x.y.z> (Pre-release) — <YYYY-MM-DD>`. Leave out `(Pre-release)` for a stable release. Check it with `node scripts/release-notes.js <x.y.z>`.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ This project contains implementations for the following editors:
|-----|--------|-----------|
| **VS Code** | Active | [`editors/vscode/`](editors/vscode/) |
| **JetBrains** | Planned | [`editors/jetbrains/`](editors/jetbrains/) |
| **Neovim** | Planned | [`editors/neovim/`](editors/neovim/) |
| **Neovim** | Working, not yet published ([install from this repo](editors/neovim/README.md)) | [`editors/neovim/`](editors/neovim/) |


## 📚 Packages in this Monorepo
Expand All @@ -72,7 +72,7 @@ The core debugging engine that powers Blackbox. It provides framework-agnostic M

## 🤝 Contributing

Contributions, issues, and feature requests are welcome\! Feel free to check the [issues page](https://github.com/akash-aman/blackbox/issues).
Contributions, issues, and feature requests are welcome\! Feel free to check the [issues page](https://github.com/xcode-studio/blackbox/issues).

## 📝 License

Expand Down
12 changes: 8 additions & 4 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,14 @@ Layers on the MCP side, each depending only on the ones below:

---

## Neovim Implementation
`editors/neovim/` is the same window side in Lua on top of nvim-dap. It writes the same registry entry (`app: "Neovim"`; `pid` and `appPid` are both the Neovim process, so an AI tool in `:terminal` is matched to it), listens on the same kind of socket, and answers the same requests. The MCP server needs no changes. Pause state, output and waits port `debugEvents.ts` onto `dap.listeners`. nvim-dap has no function or disabled breakpoints, so the plugin keeps those itself.

## Extension Guide: Adding New IDEs
To integrate a new editor into the Blackbox ecosystem, follow these steps:
A new editor only needs the **window side**; the MCP server is shared.

1. **Namespace:** Create a new directory under `editors/<ide>/`.
2. **Compliance:** Implement the functions defined in `schema/tools.json` using the target IDE's native APIs.
3. **Transport:** Expose the implementation via an MCP-compliant transport (typically stdio or a native IDE plugin API).
4. **Automation:** Register a dedicated CI workflow in `.github/workflows/<ide>.yml` to validate implementation against the schema.
2. **Window protocol:** Per window, listen on `<ipcDir>/<id>.sock` (a randomly named pipe on Windows) in the private per-user folder (`/tmp/blackbox-<uid>/`, mode 0700, checked for ownership), and write `<ipcDir>/<id>.json` with the `RegistryEntry` fields from `editors/vscode/src/ipc/protocol.ts` (`app`, `appPid`, `protocol: 2`, …). Answer newline-delimited JSON requests `{id, tool, args}` with `{id, result}` or `{id, error}`.
3. **Compliance:** Implement every tool in `schema/tools.json` except `ide_*` (answered by the MCP server), plus the internal `window_status`.
4. **Conformance:** Add a harness to `editors/conformance/harness/` that starts the editor, and run `editors/conformance/test` against it. It drives the editor through the real MCP server.
5. **Automation:** Register a CI workflow in `.github/workflows/<ide>.yml`.
27 changes: 27 additions & 0 deletions editors/conformance/fixtures/go/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
// Used by the Blackbox conformance suite.
"version": "0.2.0",
"configurations": [
{
"name": "Fixture",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}"
},
{
// The adapter refuses it, to check that the error is reported.
"name": "Fixture (broken)",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}/does-not-exist"
},
{
// Its adapter never answers, to check the stalled-start explanation.
"name": "Fixture (stalled)",
"type": "blackbox-stall",
"request": "launch"
}
]
}
18 changes: 18 additions & 0 deletions editors/conformance/fixtures/go/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Fixture for the Blackbox conformance suite (editors/conformance/test).
// The tests refer to these line numbers; keep them stable.
package main

import "fmt"

func add(a, b int) int {
return a + b
}

func main() {
total := 0
for i := 1; i <= 2; i++ {
total = add(total, i) // line 14: breakpoint
fmt.Println("total", total) // line 15: logpoint
}
fmt.Println("done", total)
}
3 changes: 3 additions & 0 deletions editors/conformance/fixtures/go/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module blackbox.test/fixture

go 1.21
65 changes: 65 additions & 0 deletions editors/conformance/harness/nvim.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Starts a headless Neovim with blackbox.nvim and nvim-dap, the way a user's
// Neovim would run it, for the conformance suite.
//
// NVIM_DAP_PATH where nvim-dap is checked out (default: lazy.nvim's folder)

const { spawn } = require('child_process');
const fs = require('fs');
const os = require('os');
const path = require('path');

const PLUGIN = path.resolve(__dirname, '../../neovim');
const NVIM_DAP = process.env.NVIM_DAP_PATH || path.join(os.homedir(), '.local/share/nvim/lazy/nvim-dap');

function initScript(workspace) {
return `
vim.opt.rtp:prepend(${JSON.stringify(PLUGIN)})
vim.opt.rtp:prepend(${JSON.stringify(NVIM_DAP)})
vim.cmd.cd(${JSON.stringify(workspace)})
local dap = require('dap')
-- Delve, as nvim-dap-go would configure it.
dap.adapters.go = {
type = 'server', port = '\${port}',
executable = { command = 'dlv', args = { 'dap', '-l', '127.0.0.1:\${port}' } },
}
-- An adapter that starts but never answers, for the stalled-start check.
dap.adapters['blackbox-stall'] = {
type = 'executable', command = ${JSON.stringify(process.execPath)}, args = { '-e', 'setInterval(function () {}, 1000)' },
}
require('blackbox').setup()
`;
}

async function startNvim({ ipcDir, workspace }) {
const init = path.join(ipcDir, 'init.lua');
fs.writeFileSync(init, initScript(workspace));
const nvim = spawn('nvim', ['--headless', '-u', init], {
cwd: workspace,
env: { ...process.env, BLACKBOX_IPC_DIR: ipcDir },
stdio: ['ignore', 'ignore', 'pipe'],
});
let stderr = '';
nvim.stderr.on('data', chunk => { stderr += chunk; });

// Ready once its registry entry exists.
const registry = path.join(ipcDir, `${nvim.pid}.json`);
for (let i = 0; i < 100 && !fs.existsSync(registry); i++) {
await new Promise(r => setTimeout(r, 100));
}
if (!fs.existsSync(registry)) {
nvim.kill();
throw new Error(`Neovim did not register within 10s.\n${stderr}`);
}
return {
name: 'Neovim',
// Resolves at once if Neovim already exited (e.g. crashed during a test).
stop: () => new Promise(resolve => {
if (nvim.exitCode !== null || nvim.signalCode !== null) { return resolve(); }
nvim.once('exit', resolve);
nvim.kill('SIGTERM');
}),
stderr: () => stderr,
};
}

module.exports = { startNvim };
Loading
Loading