diff --git a/packages/opencode/src/lsp/language.ts b/packages/opencode/src/lsp/language.ts index 58f4c8488ba4..07a2e97231e8 100644 --- a/packages/opencode/src/lsp/language.ts +++ b/packages/opencode/src/lsp/language.ts @@ -14,6 +14,7 @@ export const LANGUAGE_EXTENSIONS: Record = { ".cc": "cpp", ".c++": "cpp", ".cs": "csharp", + ".csx": "csharp", ".css": "css", ".d": "d", ".pas": "pascal", diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index a0cb8fe3881f..7faaeb42fc0b 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -703,31 +703,10 @@ export const Zls: Info = { export const CSharp: Info = { id: "csharp", root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]), - extensions: [".cs"], + extensions: [".cs", ".csx"], async spawn(root) { - let bin = which("roslyn-language-server") - if (!bin) { - if (!which("dotnet")) { - log.error(".NET SDK is required to install roslyn-language-server") - return - } - - if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - log.info("installing roslyn-language-server via dotnet tool") - const proc = Process.spawn(["dotnet", "tool", "install", "--global", "roslyn-language-server", "--prerelease"], { - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - }) - const exit = await proc.exited - if (exit !== 0) { - log.error("Failed to install roslyn-language-server") - return - } - - bin = path.join(Global.Path.bin, "roslyn-language-server" + (process.platform === "win32" ? ".exe" : "")) - log.info(`installed roslyn-language-server`, { bin }) - } + const bin = await getRoslynLanguageServer() + if (!bin) return return { process: spawn(bin, ["--stdio", "--autoLoadProjects"], { @@ -737,6 +716,135 @@ export const CSharp: Info = { }, } +export const Razor: Info = { + id: "razor", + root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]), + extensions: [".razor", ".cshtml"], + async spawn(root) { + const bin = await getRoslynLanguageServer() + if (!bin) return + + const razor = await findVscodeRazorExtension() + if (!razor) { + log.info("VS Code C# extension with Razor support not found, skipping Razor LSP") + return + } + + log.info("using VS Code Razor extension for roslyn-language-server", { extension: razor.extension }) + return { + process: spawn( + bin, + [ + "--stdio", + "--autoLoadProjects", + `--razorSourceGenerator=${razor.compiler}`, + `--razorDesignTimePath=${razor.targets}`, + "--extension", + razor.extension, + ], + { + cwd: root, + }, + ), + } + }, +} + +let roslynLanguageServerInstall: Promise | undefined + +async function getRoslynLanguageServer() { + const existing = which("roslyn-language-server") + if (existing) return existing + + const global = await roslynLanguageServerGlobalPath() + if (global) return global + + roslynLanguageServerInstall ||= installRoslynLanguageServer().finally(() => { + roslynLanguageServerInstall = undefined + }) + return roslynLanguageServerInstall +} + +async function installRoslynLanguageServer() { + if (!which("dotnet")) { + log.error(".NET SDK is required to install roslyn-language-server") + return + } + + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + log.info("installing roslyn-language-server via dotnet tool") + const proc = Process.spawn(["dotnet", "tool", "install", "--global", "roslyn-language-server", "--prerelease"], { + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + }) + const exit = await proc.exited + if (exit !== 0) { + log.error("Failed to install roslyn-language-server") + return + } + + const resolved = which("roslyn-language-server") + if (resolved) { + log.info(`installed roslyn-language-server`, { bin: resolved }) + return resolved + } + + const global = await roslynLanguageServerGlobalPath() + if (global) { + log.info(`installed roslyn-language-server`, { bin: global }) + return global + } + + log.error("Installed roslyn-language-server but could not resolve executable") +} + +async function roslynLanguageServerGlobalPath() { + const bin = path.join( + process.env.DOTNET_CLI_HOME ?? os.homedir(), + ".dotnet", + "tools", + "roslyn-language-server" + (process.platform === "win32" ? ".cmd" : ""), + ) + return (await pathExists(bin)) ? bin : undefined +} + +async function findVscodeRazorExtension() { + const roots = [ + process.env.VSCODE_EXTENSIONS, + path.join(os.homedir(), ".vscode", "extensions"), + path.join(os.homedir(), ".vscode-insiders", "extensions"), + path.join(os.homedir(), ".vscode-server", "extensions"), + path.join(os.homedir(), ".vscode-server-insiders", "extensions"), + ].filter((item) => item !== undefined) + + for (const root of [...new Set(roots)]) { + const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []) + const candidates = await Promise.all( + entries + .filter((entry) => entry.isDirectory() && entry.name.startsWith("ms-dotnettools.csharp-")) + .map(async (entry) => ({ + path: path.join(root, entry.name, ".razorExtension"), + modified: (await fs.stat(path.join(root, entry.name)).catch(() => undefined))?.mtimeMs ?? 0, + })), + ) + for (const entry of candidates.sort((a, b) => b.modified - a.modified).map((candidate) => candidate.path)) { + const result = { + compiler: path.join(entry, "Microsoft.CodeAnalysis.Razor.Compiler.dll"), + targets: path.join(entry, "Targets", "Microsoft.NET.Sdk.Razor.DesignTime.targets"), + extension: path.join(entry, "Microsoft.VisualStudioCode.RazorExtension.dll"), + } + if ( + (await pathExists(result.compiler)) && + (await pathExists(result.targets)) && + (await pathExists(result.extension)) + ) { + return result + } + } + } +} + export const FSharp: Info = { id: "fsharp", root: NearestRoot([".slnx", ".sln", ".fsproj", "global.json"]), diff --git a/packages/web/src/content/docs/lsp.mdx b/packages/web/src/content/docs/lsp.mdx index f242f4c5e4d3..ad6a4644df7a 100644 --- a/packages/web/src/content/docs/lsp.mdx +++ b/packages/web/src/content/docs/lsp.mdx @@ -16,7 +16,7 @@ OpenCode comes with several built-in LSP servers for popular languages: | astro | .astro | Auto-installs for Astro projects | | bash | .sh, .bash, .zsh, .ksh | Auto-installs bash-language-server | | clangd | .c, .cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++ | Auto-installs for C/C++ projects | -| csharp | .cs | `.NET SDK` installed | +| csharp | .cs, .csx | `.NET SDK` installed | | clojure-lsp | .clj, .cljs, .cljc, .edn | `clojure-lsp` command available | | dart | .dart | `dart` command available | | deno | .ts, .tsx, .js, .jsx, .mjs | `deno` command available (auto-detects deno.json/deno.jsonc) | @@ -36,6 +36,7 @@ OpenCode comes with several built-in LSP servers for popular languages: | php intelephense | .php | Auto-installs for PHP projects | | prisma | .prisma | `prisma` command available | | pyright | .py, .pyi | `pyright` dependency installed | +| razor | .razor, .cshtml | `.NET SDK` and VS Code C# extension installed | | ruby-lsp (rubocop) | .rb, .rake, .gemspec, .ru | `ruby` and `gem` commands available | | rust | .rs | `rust-analyzer` command available | | sourcekit-lsp | .swift, .objc, .objcpp | `swift` installed (`xcode` on macOS) |