Add Roslyn support for Razor and C# scripts (#24228)

This commit is contained in:
Luke Parker 2026-04-25 10:25:57 +10:00 committed by GitHub
parent 5cd178ba70
commit 1e4b7b5451
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 135 additions and 25 deletions

View file

@ -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",

View file

@ -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"]),

View file

@ -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) |