Przeglądaj źródła

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

Luke Parker 3 miesięcy temu
rodzic
commit
1e4b7b5451

+ 1 - 0
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",

+ 132 - 24
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"]),

+ 2 - 1
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)                         |