Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
11 changes: 5 additions & 6 deletions marimo/_server/lsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from marimo._server.utils import find_free_port
from marimo._tracer import server_tracer
from marimo._utils.paths import marimo_package_path
from marimo._utils.strings import cmd_quote

LOGGER = _loggers.marimo_logger()

Expand Down Expand Up @@ -200,8 +199,8 @@ def get_command(self) -> list[str]:
copilot_bin = self._lsp_dir() / "copilot" / "language-server.js"
log_file = _loggers.get_log_directory() / "github-copilot-lsp.log"

# Properly quote the copilot binary path to handle spaces and special characters
copilot_command = f"node {cmd_quote(str(copilot_bin))} --stdio"
# Use typed format to avoid quoting issues: copilot:<binary_path>
copilot_command = f"copilot:{copilot_bin}"

return [
"node",
Expand Down Expand Up @@ -292,7 +291,7 @@ def get_command(self) -> list[str]:
"--port",
str(self.port),
"--lsp",
"basedpyright-langserver --stdio",
"basedpyright:basedpyright-langserver",
"--log-file",
str(log_file),
]
Expand Down Expand Up @@ -330,8 +329,8 @@ def get_command(self) -> list[str]:
lsp_bin = marimo_package_path() / "_lsp" / "index.cjs"
log_file = _loggers.get_log_directory() / "ty-lsp.log"

# Properly quote the ty binary path to handle spaces and special characters
ty_command = f"{cmd_quote(find_ty_bin())} server"
# Use typed format to avoid quoting issues: ty:<binary_path>
ty_command = f"ty:{find_ty_bin()}"

return [
"node",
Expand Down
89 changes: 88 additions & 1 deletion packages/lsp/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,71 @@ import * as path from "node:path";

import { describe, expect, it } from "vitest";

describe("LSP Server", () => {
// Import the parseTypedCommand function - we need to export it first
import { parseTypedCommand } from "./index";

describe("parseTypedCommand", () => {
it("should parse copilot commands correctly", () => {
const result = parseTypedCommand(
"copilot:/path/to/copilot/language-server.js",
);
expect(result).toEqual([
"node",
"/path/to/copilot/language-server.js",
"--stdio",
]);
});

it("should parse copilot commands with spaces in path", () => {
const result = parseTypedCommand(
"copilot:/path/with spaces/copilot/language-server.js",
);
expect(result).toEqual([
"node",
"/path/with spaces/copilot/language-server.js",
"--stdio",
]);
});

it("should parse basedpyright commands correctly", () => {
const result = parseTypedCommand("basedpyright:basedpyright-langserver");
expect(result).toEqual(["basedpyright-langserver", "--stdio"]);
});

it("should parse ty commands correctly", () => {
const result = parseTypedCommand("ty:/path/to/ty");
expect(result).toEqual(["/path/to/ty", "server"]);
});

it("should parse ty commands with spaces in path", () => {
const result = parseTypedCommand("ty:/path/with spaces/ty");
expect(result).toEqual(["/path/with spaces/ty", "server"]);
});

it("should fallback to old format for commands without colon", () => {
const result = parseTypedCommand("node /path/to/binary --stdio");
expect(result).toEqual(["node", "/path/to/binary", "--stdio"]);
});

it("should throw error for unknown server types", () => {
expect(() => parseTypedCommand("unknown:/path/to/binary")).toThrow(
"Unknown LSP server type: unknown",
);
});

it("should handle edge cases with multiple colons", () => {
const result = parseTypedCommand(
"copilot:C:/Program Files/Node.js/language-server.js",
);
expect(result).toEqual([
"node",
"C:/Program Files/Node.js/language-server.js",
"--stdio",
]);
});
});

describe("LSP Server Integration", () => {
it("should start and be killable", async () => {
const process = childProcess.spawn("node", [
"./dist/index.cjs",
Expand All @@ -27,4 +91,27 @@ describe("LSP Server", () => {

expect(process.killed).toBe(true);
});

it("should start with typed copilot command", async () => {
const process = childProcess.spawn("node", [
"./dist/index.cjs",
"--lsp",
"copilot:echo copilot-test",
"--log-file",
path.join(os.tmpdir(), "lsp-server-copilot-test.log"),
]);

// Give it a moment to start
await new Promise((resolve) => setTimeout(resolve, 1000));

// Kill the process
process.kill();

// Wait for process to die
await new Promise<void>((resolve) => {
process.on("exit", () => resolve());
});

expect(process.killed).toBe(true);
});
});
25 changes: 24 additions & 1 deletion packages/lsp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,28 @@ class WebSocketAdapter implements IWebSocket {
}
}

export function parseTypedCommand(typedCommand: string): string[] {
if (!typedCommand.includes(":")) {
// Fallback for old format - simple split by spaces
return typedCommand.split(" ");
}

const colonIndex = typedCommand.indexOf(":");
const serverType = typedCommand.substring(0, colonIndex);
const binaryPath = typedCommand.substring(colonIndex + 1);

switch (serverType) {
case "copilot":
return ["node", binaryPath, "--stdio"];
case "basedpyright":
return [binaryPath, "--stdio"];
case "ty":
return [binaryPath, "server"];
default:
throw new Error(`Unknown LSP server type: ${serverType}`);
}
}

function handleWebSocketConnection(
languageServerCommand: string[],
logger: Logger,
Expand Down Expand Up @@ -181,8 +203,9 @@ async function main(): Promise<void> {

const logger = await Logger.create(argv["log-file"]);
const serverPort = Number.parseInt(argv.port) || 3000;
const languageServerCommand = argv.lsp.split(" ");
const languageServerCommand = parseTypedCommand(argv.lsp);

logger.log(`Parsed LSP command: ${languageServerCommand.join(" ")}`);
startWebSocketServer(serverPort, languageServerCommand, logger);
}

Expand Down
Loading