Skip to content

Commit 1e4b7b5

Browse files
authored
Add Roslyn support for Razor and C# scripts (anomalyco#24228)
1 parent 5cd178b commit 1e4b7b5

3 files changed

Lines changed: 135 additions & 25 deletions

File tree

packages/opencode/src/lsp/language.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
1414
".cc": "cpp",
1515
".c++": "cpp",
1616
".cs": "csharp",
17+
".csx": "csharp",
1718
".css": "css",
1819
".d": "d",
1920
".pas": "pascal",

packages/opencode/src/lsp/server.ts

Lines changed: 132 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -703,31 +703,10 @@ export const Zls: Info = {
703703
export const CSharp: Info = {
704704
id: "csharp",
705705
root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]),
706-
extensions: [".cs"],
706+
extensions: [".cs", ".csx"],
707707
async spawn(root) {
708-
let bin = which("roslyn-language-server")
709-
if (!bin) {
710-
if (!which("dotnet")) {
711-
log.error(".NET SDK is required to install roslyn-language-server")
712-
return
713-
}
714-
715-
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
716-
log.info("installing roslyn-language-server via dotnet tool")
717-
const proc = Process.spawn(["dotnet", "tool", "install", "--global", "roslyn-language-server", "--prerelease"], {
718-
stdout: "pipe",
719-
stderr: "pipe",
720-
stdin: "pipe",
721-
})
722-
const exit = await proc.exited
723-
if (exit !== 0) {
724-
log.error("Failed to install roslyn-language-server")
725-
return
726-
}
727-
728-
bin = path.join(Global.Path.bin, "roslyn-language-server" + (process.platform === "win32" ? ".exe" : ""))
729-
log.info(`installed roslyn-language-server`, { bin })
730-
}
708+
const bin = await getRoslynLanguageServer()
709+
if (!bin) return
731710

732711
return {
733712
process: spawn(bin, ["--stdio", "--autoLoadProjects"], {
@@ -737,6 +716,135 @@ export const CSharp: Info = {
737716
},
738717
}
739718

719+
export const Razor: Info = {
720+
id: "razor",
721+
root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]),
722+
extensions: [".razor", ".cshtml"],
723+
async spawn(root) {
724+
const bin = await getRoslynLanguageServer()
725+
if (!bin) return
726+
727+
const razor = await findVscodeRazorExtension()
728+
if (!razor) {
729+
log.info("VS Code C# extension with Razor support not found, skipping Razor LSP")
730+
return
731+
}
732+
733+
log.info("using VS Code Razor extension for roslyn-language-server", { extension: razor.extension })
734+
return {
735+
process: spawn(
736+
bin,
737+
[
738+
"--stdio",
739+
"--autoLoadProjects",
740+
`--razorSourceGenerator=${razor.compiler}`,
741+
`--razorDesignTimePath=${razor.targets}`,
742+
"--extension",
743+
razor.extension,
744+
],
745+
{
746+
cwd: root,
747+
},
748+
),
749+
}
750+
},
751+
}
752+
753+
let roslynLanguageServerInstall: Promise<string | undefined> | undefined
754+
755+
async function getRoslynLanguageServer() {
756+
const existing = which("roslyn-language-server")
757+
if (existing) return existing
758+
759+
const global = await roslynLanguageServerGlobalPath()
760+
if (global) return global
761+
762+
roslynLanguageServerInstall ||= installRoslynLanguageServer().finally(() => {
763+
roslynLanguageServerInstall = undefined
764+
})
765+
return roslynLanguageServerInstall
766+
}
767+
768+
async function installRoslynLanguageServer() {
769+
if (!which("dotnet")) {
770+
log.error(".NET SDK is required to install roslyn-language-server")
771+
return
772+
}
773+
774+
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
775+
log.info("installing roslyn-language-server via dotnet tool")
776+
const proc = Process.spawn(["dotnet", "tool", "install", "--global", "roslyn-language-server", "--prerelease"], {
777+
stdout: "pipe",
778+
stderr: "pipe",
779+
stdin: "pipe",
780+
})
781+
const exit = await proc.exited
782+
if (exit !== 0) {
783+
log.error("Failed to install roslyn-language-server")
784+
return
785+
}
786+
787+
const resolved = which("roslyn-language-server")
788+
if (resolved) {
789+
log.info(`installed roslyn-language-server`, { bin: resolved })
790+
return resolved
791+
}
792+
793+
const global = await roslynLanguageServerGlobalPath()
794+
if (global) {
795+
log.info(`installed roslyn-language-server`, { bin: global })
796+
return global
797+
}
798+
799+
log.error("Installed roslyn-language-server but could not resolve executable")
800+
}
801+
802+
async function roslynLanguageServerGlobalPath() {
803+
const bin = path.join(
804+
process.env.DOTNET_CLI_HOME ?? os.homedir(),
805+
".dotnet",
806+
"tools",
807+
"roslyn-language-server" + (process.platform === "win32" ? ".cmd" : ""),
808+
)
809+
return (await pathExists(bin)) ? bin : undefined
810+
}
811+
812+
async function findVscodeRazorExtension() {
813+
const roots = [
814+
process.env.VSCODE_EXTENSIONS,
815+
path.join(os.homedir(), ".vscode", "extensions"),
816+
path.join(os.homedir(), ".vscode-insiders", "extensions"),
817+
path.join(os.homedir(), ".vscode-server", "extensions"),
818+
path.join(os.homedir(), ".vscode-server-insiders", "extensions"),
819+
].filter((item) => item !== undefined)
820+
821+
for (const root of [...new Set(roots)]) {
822+
const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => [])
823+
const candidates = await Promise.all(
824+
entries
825+
.filter((entry) => entry.isDirectory() && entry.name.startsWith("ms-dotnettools.csharp-"))
826+
.map(async (entry) => ({
827+
path: path.join(root, entry.name, ".razorExtension"),
828+
modified: (await fs.stat(path.join(root, entry.name)).catch(() => undefined))?.mtimeMs ?? 0,
829+
})),
830+
)
831+
for (const entry of candidates.sort((a, b) => b.modified - a.modified).map((candidate) => candidate.path)) {
832+
const result = {
833+
compiler: path.join(entry, "Microsoft.CodeAnalysis.Razor.Compiler.dll"),
834+
targets: path.join(entry, "Targets", "Microsoft.NET.Sdk.Razor.DesignTime.targets"),
835+
extension: path.join(entry, "Microsoft.VisualStudioCode.RazorExtension.dll"),
836+
}
837+
if (
838+
(await pathExists(result.compiler)) &&
839+
(await pathExists(result.targets)) &&
840+
(await pathExists(result.extension))
841+
) {
842+
return result
843+
}
844+
}
845+
}
846+
}
847+
740848
export const FSharp: Info = {
741849
id: "fsharp",
742850
root: NearestRoot([".slnx", ".sln", ".fsproj", "global.json"]),

packages/web/src/content/docs/lsp.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ OpenCode comes with several built-in LSP servers for popular languages:
1616
| astro | .astro | Auto-installs for Astro projects |
1717
| bash | .sh, .bash, .zsh, .ksh | Auto-installs bash-language-server |
1818
| clangd | .c, .cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++ | Auto-installs for C/C++ projects |
19-
| csharp | .cs | `.NET SDK` installed |
19+
| csharp | .cs, .csx | `.NET SDK` installed |
2020
| clojure-lsp | .clj, .cljs, .cljc, .edn | `clojure-lsp` command available |
2121
| dart | .dart | `dart` command available |
2222
| 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:
3636
| php intelephense | .php | Auto-installs for PHP projects |
3737
| prisma | .prisma | `prisma` command available |
3838
| pyright | .py, .pyi | `pyright` dependency installed |
39+
| razor | .razor, .cshtml | `.NET SDK` and VS Code C# extension installed |
3940
| ruby-lsp (rubocop) | .rb, .rake, .gemspec, .ru | `ruby` and `gem` commands available |
4041
| rust | .rs | `rust-analyzer` command available |
4142
| sourcekit-lsp | .swift, .objc, .objcpp | `swift` installed (`xcode` on macOS) |

0 commit comments

Comments
 (0)