summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorLuke Parker <[email protected]>2026-04-25 10:25:57 +1000
committerGitHub <[email protected]>2026-04-25 10:25:57 +1000
commit1e4b7b5451dc924515444c006f6babbb9f24bc85 (patch)
treea8b9191bdce0d06a7fa32b280da5681b8f76f745 /packages
parent5cd178ba7008969c9fc711c78603d7e3144b4ce8 (diff)
downloadopencode-1e4b7b5451dc924515444c006f6babbb9f24bc85.tar.gz
opencode-1e4b7b5451dc924515444c006f6babbb9f24bc85.zip
Add Roslyn support for Razor and C# scripts (#24228)
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/lsp/language.ts1
-rw-r--r--packages/opencode/src/lsp/server.ts156
-rw-r--r--packages/web/src/content/docs/lsp.mdx3
3 files changed, 135 insertions, 25 deletions
diff --git a/packages/opencode/src/lsp/language.ts b/packages/opencode/src/lsp/language.ts
index 58f4c8488..07a2e9723 100644
--- a/packages/opencode/src/lsp/language.ts
+++ b/packages/opencode/src/lsp/language.ts
@@ -14,6 +14,7 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
".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 a0cb8fe38..7faaeb42f 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<string | undefined> | 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 f242f4c5e..ad6a4644d 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) |