Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 3 additions & 0 deletions editors/vscode/.vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ tsconfig.json
build.ts
bun.lock
node_modules/**
!node_modules/.bin/mdsmith
!node_modules/.bin/mdsmith.cmd
!node_modules/@mdsmith/**
Comment thread
Claude marked this conversation as resolved.
Outdated
Comment thread
Claude marked this conversation as resolved.
Outdated
**/*.map
**/*.ts
31 changes: 18 additions & 13 deletions editors/vscode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ runs `mdsmith fix` on the whole buffer.

## Prerequisites

- The `mdsmith` binary on `$PATH`, or a path you supply via
the `mdsmith.path` setting. Install with
`go install github.com/jeduden/mdsmith/cmd/mdsmith@latest`
or download from the
[releases page](https://github.com/jeduden/mdsmith/releases).
- VS Code 1.85 or later.
- **VS Code 1.85 or later.**
- **The `mdsmith` binary** — the extension bundles a cross-platform
binary from npm that works on Linux, macOS, and Windows from a
single extension install. No separate binary install is required in
most cases. If the bundled binary fails to install (corporate
proxies, offline environments), you can install `mdsmith` manually:
- `npm install -g @mdsmith/cli`
Comment thread
Claude marked this conversation as resolved.
Outdated
- `go install github.com/jeduden/mdsmith/cmd/mdsmith@latest`
- Download from the
[releases page](https://github.com/jeduden/mdsmith/releases)
- Then configure `mdsmith.path` to point to the binary.

## Install

Expand All @@ -24,13 +29,13 @@ code --install-extension mdsmith-<version>.vsix

## Settings

| Setting | Default | Purpose |
|------------------------|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `mdsmith.path` | `"mdsmith"` | Binary path; resolved against the extension-host PATH (use an absolute path if `which mdsmith` works in your terminal but the extension reports `spawn ENOENT` — `~/.bashrc`/`~/.zshrc` are not sourced) |
| `mdsmith.config` | `""` | Override `-c` config path |
| `mdsmith.run` | `"onSave"` | When to lint: `onType`, `onSave`, or `off` |
| `mdsmith.fixOnSave` | `false` | Wires `source.fixAll.mdsmith` on save |
| `mdsmith.trace.server` | `"off"` | LSP trace verbosity |
| Setting | Default | Purpose |
|------------------------|-------------|------------------------------------------------------------------------------------------------------------------------------|
| `mdsmith.path` | `"mdsmith"` | Binary path; defaults to bundled cross-platform binary. Falls back to PATH resolution. Set absolute path if needed (e.g. `/go/bin/mdsmith`) |
| `mdsmith.config` | `""` | Override `-c` config path |
| `mdsmith.run` | `"onSave"` | When to lint: `onType`, `onSave`, or `off` |
| `mdsmith.fixOnSave` | `false` | Wires `source.fixAll.mdsmith` on save |
| `mdsmith.trace.server` | `"off"` | LSP trace verbosity |

See the
[full guide](https://github.com/jeduden/mdsmith/blob/main/docs/guides/editors/vscode.md)
Expand Down
5 changes: 4 additions & 1 deletion editors/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"mdsmith.path": {
"type": "string",
"default": "mdsmith",
"description": "Path to the mdsmith binary. A bare name is resolved against VS Code's extension-host PATH, which is the container/login-shell environment — interactive-only files like ~/.bashrc and ~/.zshrc are NOT sourced. If `which mdsmith` works in a terminal but the extension reports `spawn ENOENT`, set this to an absolute path (e.g. /go/bin/mdsmith) or symlink the binary into /usr/local/bin."
"description": "Path to the mdsmith binary. Defaults to the bundled cross-platform binary from @mdsmith/cli (works on Linux, macOS, Windows from a single install). If the bundled binary is unavailable, falls back to resolving 'mdsmith' against PATH. Set to an absolute path if you installed mdsmith elsewhere (e.g. /go/bin/mdsmith)."
Comment thread
Claude marked this conversation as resolved.
Outdated
},
"mdsmith.config": {
"type": "string",
Expand Down Expand Up @@ -84,6 +84,9 @@
"dependencies": {
"vscode-languageclient": "^9.0.1"
},
"optionalDependencies": {
"@mdsmith/cli": "0.0.0-dev"
},
Comment thread
Claude marked this conversation as resolved.
Comment thread
Claude marked this conversation as resolved.
Comment thread
Claude marked this conversation as resolved.
Comment thread
Claude marked this conversation as resolved.
Comment thread
Claude marked this conversation as resolved.
"devDependencies": {
"@types/bun": "^1.1.0",
"@types/node": "^20.11.0",
Expand Down
60 changes: 60 additions & 0 deletions editors/vscode/src/binary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Unit tests for binary resolution logic.
// The extension bundles a cross-platform mdsmith binary that works on
// all platforms from a single .vsix install; these tests verify the
// fallback behavior when bundling fails.

import { describe, expect, mock, test } from "bun:test";
import { join } from "node:path";
import { resolveBinary } from "./binary";

describe("resolveBinary", () => {
test("returns custom path unchanged when user specifies non-default", () => {
const fileExists = mock(() => false);
const result = resolveBinary("/custom/path/to/mdsmith", "/ext", "linux", fileExists);
expect(result).toBe("/custom/path/to/mdsmith");
// Should not even attempt to check for bundled binary
expect(fileExists).not.toHaveBeenCalled();
});

test("returns bundled binary when default path and bundled exists (Unix)", () => {
const extensionPath = "/ext";
const bundledPath = join(extensionPath, "node_modules", ".bin", "mdsmith");

// Mock: bundled binary exists
const fileExists = mock((path) => path === bundledPath);
Comment thread
Claude marked this conversation as resolved.

const result = resolveBinary("mdsmith", extensionPath, "linux", fileExists);
expect(result).toBe(bundledPath);
expect(fileExists).toHaveBeenCalledWith(bundledPath);
});

test("returns bundled binary when default path and bundled exists (Windows)", () => {
const extensionPath = "/ext";
const bundledPath = join(extensionPath, "node_modules", ".bin", "mdsmith.cmd");

// Mock: bundled binary exists
const fileExists = mock((path) => path === bundledPath);

Comment thread
Claude marked this conversation as resolved.
const result = resolveBinary("mdsmith", extensionPath, "win32", fileExists);
expect(result).toBe(bundledPath);
expect(fileExists).toHaveBeenCalledWith(bundledPath);
});

test("falls back to default path when bundled binary does not exist", () => {
// Mock: no bundled binary
const fileExists = mock(() => false);

const result = resolveBinary("mdsmith", "/ext", "linux", fileExists);
expect(result).toBe("mdsmith");
// Should have checked for bundled binary
expect(fileExists).toHaveBeenCalled();
});

test("returns custom bare name unchanged", () => {
const fileExists = mock(() => false);
const result = resolveBinary("my-mdsmith-fork", "/ext", "linux", fileExists);
expect(result).toBe("my-mdsmith-fork");
// Should not check for bundled binary when not the default
expect(fileExists).not.toHaveBeenCalled();
});
});
61 changes: 61 additions & 0 deletions editors/vscode/src/binary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Binary resolution logic for the mdsmith extension.
// The extension bundles a cross-platform mdsmith binary from npm that
// works on all supported platforms (Linux, macOS, Windows) via a single
// .vsix install. This module resolves the bundled binary when the user
// leaves the default "mdsmith" path, falling back to PATH if bundling
// failed.
Comment thread
Claude marked this conversation as resolved.
Outdated

import { existsSync } from "node:fs";
import { join } from "node:path";

// resolveBinary returns the path to the mdsmith binary. When the
// configured path is the bare string "mdsmith", it first checks for
// the bundled binary at node_modules/.bin/mdsmith (Unix) or
// node_modules/.bin/mdsmith.cmd (Windows). The @mdsmith/cli npm package
// ships with platform-specific binaries as optional dependencies, so
// a single extension install works on all platforms. If the bundled
// binary exists, return its absolute path. Otherwise return the
// configured path unchanged so the LanguageClient resolves it against
// PATH (fallback for proxy/offline install failures).
//
// The extensionPath should be the vscode.ExtensionContext.extensionPath
// (the directory containing package.json and node_modules/).
//
// The optional platform and fileExists parameters are for testing; in
// production they default to process.platform and fs.existsSync.
export function resolveBinary(
configuredPath: string,
extensionPath: string,
platform: string = process.platform,
fileExists: (path: string) => boolean = existsSync
): string {
// If the user specified a custom path (not the bare "mdsmith"),
// honor it exactly — they know what they want.
if (configuredPath !== "mdsmith") {
return configuredPath;
}

// The user left the default "mdsmith". Check for the bundled binary
// from @mdsmith/cli. The npm package ships with platform-specific
// binaries (linux-x64, darwin-arm64, win32-x64, etc.) as optional
// dependencies; npm installs ALL of them during packaging (regardless
// of build platform), so a single .vsix works on Linux, macOS, and
// Windows. The bin wrapper (mdsmith.js) selects the correct binary
// at runtime. The wrapper lives at node_modules/.bin/mdsmith (Unix)
// or node_modules/.bin/mdsmith.cmd (Windows).
Comment thread
Claude marked this conversation as resolved.
Outdated
const binDir = join(extensionPath, "node_modules", ".bin");
const unixBin = join(binDir, "mdsmith");
const winBin = join(binDir, "mdsmith.cmd");

// Prefer the platform-appropriate wrapper if it exists.
const candidate = platform === "win32" ? winBin : unixBin;
if (fileExists(candidate)) {
return candidate;
}

// The bundled binary does not exist (optional dependency install
// failed, or this is a dev build without npm install). Fall back
// to the bare "mdsmith" string so the LanguageClient resolves it
// against the shell PATH (same as before bundling).
return configuredPath;
}
4 changes: 3 additions & 1 deletion editors/vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
collectFixAllEdits,
startupErrorMessage
} from "./wiring";
import { resolveBinary } from "./binary";

let client: LanguageClient | undefined;
// Track the .mdsmith.yml file watcher across the activate /
Expand Down Expand Up @@ -74,7 +75,8 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
// must remain usable so the user can retry.
async function startServer(context: vscode.ExtensionContext): Promise<void> {
const cfg = vscode.workspace.getConfiguration("mdsmith");
const binary = cfg.get<string>("path", "mdsmith");
const configuredPath = cfg.get<string>("path", "mdsmith");
const binary = resolveBinary(configuredPath, context.extensionPath);
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;

const serverOptions: ServerOptions = buildServerOptions(
Expand Down
Loading