diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..54aaf9fe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,75 @@ +name: Bug report +description: Report a defect in SharpLsp (language server, sidecars, or an editor extension). +title: "[Bug]: " +labels: ["bug", "triage"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to file a bug. Please do **not** report + security vulnerabilities here — use private reporting (see SECURITY.md). + - type: textarea + id: what-happened + attributes: + label: What happened? + description: A clear description of the bug and what you expected instead. + placeholder: When I open a workspace with two solutions, hover stops working… + validations: + required: true + - type: textarea + id: repro + attributes: + label: Steps to reproduce + description: Minimal steps. A link to a sample repo or a minimal project is ideal. + placeholder: | + 1. Open '...' + 2. Trigger '...' + 3. Observe '...' + validations: + required: true + - type: dropdown + id: component + attributes: + label: Component + description: Which part of SharpLsp is affected? + options: + - "Not sure" + - "Rust LSP host" + - "C# sidecar (Roslyn)" + - "F# sidecar (FCS)" + - "VS Code extension" + - "Other editor (Zed/Rider/Neovim/…)" + validations: + required: true + - type: input + id: language + attributes: + label: Language + description: C#, F#, or both? + placeholder: C# + validations: + required: false + - type: input + id: version + attributes: + label: SharpLsp version + description: Extension version or `sharplsp --version`. + placeholder: 0.1.0 + validations: + required: true + - type: input + id: env + attributes: + label: Editor & OS + description: Editor + version, operating system, and .NET SDK version. + placeholder: VS Code 1.99.0, macOS 15.5 (arm64), .NET 10.0.203 + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant logs + description: Output from the SharpLsp output channel (set `sharplsp.logging.level` to `debug`). Redact anything sensitive. + render: shell + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..2acac445 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Security vulnerability + url: https://github.com/Nimblesite/SharpLsp/security/advisories/new + about: Please report security issues privately, never as a public issue. See SECURITY.md. + - name: Question or discussion + url: https://github.com/Nimblesite/SharpLsp/discussions + about: Ask usage questions or propose ideas in Discussions. + - name: Documentation + url: https://sharplsp.dev/docs + about: Read the SharpLsp documentation. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..8ba25016 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,39 @@ +name: Feature request +description: Suggest a capability or improvement for SharpLsp. +title: "[Feature]: " +labels: ["enhancement", "triage"] +body: + - type: textarea + id: problem + attributes: + label: Problem / motivation + description: What are you trying to do, and what's missing or painful today? + placeholder: There's no way to … which makes … hard. + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + description: What would you like SharpLsp to do? + validations: + required: true + - type: dropdown + id: language + attributes: + label: Language relevance + description: SharpLsp treats F# and C# as equal first-class citizens — which does this affect? + options: + - "Both C# and F#" + - "C# only" + - "F# only" + - "Editor/tooling (language-agnostic)" + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Other approaches, or how other tools (Visual Studio, Rider, C# Dev Kit) handle this. + validations: + required: false diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 00000000..2b343ea1 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,52 @@ +# Security Policy + +## Supported versions + +SharpLsp is pre-1.0 and ships from a single active line. Security fixes are +released against the latest published version on the +[VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=nimblesite.sharplsp) +and the latest GitHub Release. Please reproduce any report against the most +recent release before filing. + +| Version | Supported | +| ------- | --------- | +| Latest release | ✅ | +| Older releases | ❌ | + +## Reporting a vulnerability + +**Please do not open public issues, pull requests, or discussions for security +vulnerabilities.** Public disclosure before a fix is available puts users at +risk. + +Instead, report privately through GitHub's coordinated disclosure flow: + +1. Go to the repository's **Security** tab → **Report a vulnerability** + (GitHub Private Vulnerability Reporting), or use this direct link: + . +2. Include: affected component (Rust host, C#/F# sidecar, or VS Code + extension), version, a clear description, reproduction steps, and the + impact you observed. + +If you are unable to use the GitHub flow, you may instead email the maintainer +at **cftools@nimblesite.co** with the same details. + +## What to expect + +- **Acknowledgement:** within 3 business days. +- **Triage & initial assessment:** within 7 business days. +- **Fix & disclosure:** we aim to ship a fix and publish an advisory as soon as + practical, and will keep you updated on progress. With your agreement we will + credit you in the advisory. + +## Scope notes + +SharpLsp executes language-tooling on the code you open, similar to any IDE +language server. Reports we are especially interested in: + +- Code execution triggered merely by **opening** a workspace (e.g. a workspace + trust bypass that runs an attacker-controlled binary). +- Path traversal or arbitrary file read/write outside the workspace. +- Memory-safety or denial-of-service issues reachable from untrusted document + or project content. +- Secret/credential exposure in builds, logs, or the published extension. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..1f554228 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,71 @@ +# Dependabot keeps the Rust, .NET, npm, and GitHub Actions dependency trees +# patched. Minor/patch bumps are grouped per-ecosystem to keep PR noise low; +# major bumps still arrive as individual PRs so they get deliberate review. +version: 2 +updates: + # ── Rust: host crate ──────────────────────────────────────────────── + - package-ecosystem: cargo + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 + groups: + rust-host: + patterns: ["*"] + update-types: ["minor", "patch"] + + # ── Rust: Zed extension crate ─────────────────────────────────────── + - package-ecosystem: cargo + directory: "/editors/zed" + schedule: + interval: weekly + open-pull-requests-limit: 5 + groups: + rust-zed: + patterns: ["*"] + update-types: ["minor", "patch"] + + # ── .NET: C#/F# sidecars solution ─────────────────────────────────── + - package-ecosystem: nuget + directory: "/sidecars" + schedule: + interval: weekly + open-pull-requests-limit: 10 + groups: + dotnet-sidecars: + patterns: ["*"] + update-types: ["minor", "patch"] + + # ── npm: VS Code extension ────────────────────────────────────────── + - package-ecosystem: npm + directory: "/editors/vscode" + schedule: + interval: weekly + open-pull-requests-limit: 10 + groups: + vscode-extension: + patterns: ["*"] + update-types: ["minor", "patch"] + + # ── npm: website ──────────────────────────────────────────────────── + - package-ecosystem: npm + directory: "/website" + schedule: + interval: weekly + open-pull-requests-limit: 10 + groups: + website: + patterns: ["*"] + update-types: ["minor", "patch"] + + # ── GitHub Actions: all workflows ─────────────────────────────────── + # Keeps the SHA-pinned actions current (Dependabot bumps the pin and the + # trailing version comment together). + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 + groups: + github-actions: + patterns: ["*"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e51b3c00..6f5a0ee1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,8 +18,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '24' - name: Validate shipwright.json @@ -36,22 +36,23 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable with: + toolchain: stable components: clippy, rustfmt - - uses: Swatinem/rust-cache@v2 - - uses: actions/setup-dotnet@v4 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 with: dotnet-version: | 9.0.x 10.0.203 - - uses: actions/cache@v4 + - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: "~/.nuget/packages" key: "${{ runner.os }}-nuget-${{ hashFiles('sidecars/**/*.csproj', 'sidecars/Directory.Build.props') }}" restore-keys: "${{ runner.os }}-nuget-" - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '20' cache: npm @@ -86,19 +87,24 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable with: + toolchain: stable components: llvm-tools-preview - - uses: Swatinem/rust-cache@v2 - - uses: taiki-e/install-action@cargo-llvm-cov - - uses: taiki-e/install-action@nextest - - uses: actions/setup-dotnet@v4 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + - uses: taiki-e/install-action@0631aa6515c7d545823c67cfae7ef4fc7f490154 # v2 + with: + tool: cargo-llvm-cov + - uses: taiki-e/install-action@0631aa6515c7d545823c67cfae7ef4fc7f490154 # v2 + with: + tool: nextest + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 with: dotnet-version: | 9.0.x 10.0.203 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '20' - name: Install .NET diagnostic tools @@ -118,7 +124,7 @@ jobs: ./target/release/sharplsp --version | grep -Fx "sharplsp ${VERSION}" ./target/release/sharplsp --version --json | VERSION="${VERSION}" node -e 'const fs=require("node:fs"); const actual=JSON.parse(fs.readFileSync(0,"utf8")); const expected={name:"sharplsp",version:process.env.VERSION,kind:"lsp",language:"rust",product:"sharplsp"}; for (const [key,value] of Object.entries(expected)) { if (actual[key] !== value) throw new Error(`${key}: expected ${value}, got ${actual[key]}`); }' - name: Upload coverage - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: coverage-rust path: | @@ -131,8 +137,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 with: dotnet-version: | 9.0.x @@ -148,7 +154,7 @@ jobs: target/sidecar-csharp/SharpLsp.Sidecar.CSharp --version | grep -Fx "sharplsp-sidecar-csharp ${CS_VERSION}" target/sidecar-fsharp/SharpLsp.Sidecar.FSharp --version | grep -Fx "sharplsp-sidecar-fsharp ${FS_VERSION}" - name: Upload coverage - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: coverage-dotnet path: coverage-thresholds.json @@ -163,15 +169,17 @@ jobs: # real solutions). 10 minutes is too tight for the end-to-end run. timeout-minutes: 20 steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - uses: actions/setup-dotnet@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + with: + toolchain: stable + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 with: dotnet-version: | 9.0.x 10.0.203 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '20' cache: npm @@ -197,7 +205,7 @@ jobs: platform="$(node -e 'process.stdout.write(`${process.platform}-${process.arch}`)')" unzip -l sharplsp.vsix | grep -F "bin/${platform}/sharplsp" - name: Upload coverage - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: coverage-vsix path: coverage-thresholds.json diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..de38c6df --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,52 @@ +name: CodeQL + +# Static analysis for the TypeScript VS Code extension and the GitHub Actions +# workflows themselves. Both languages analyze without a build step, so this +# workflow stays fast and reliable. (.NET/Rust semantic engines are covered by +# their own strict lint/clippy gates in ci.yml.) +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + # Weekly, so newly-published CodeQL queries re-scan even without a push. + - cron: "27 4 * * 1" + +permissions: + contents: read + +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + timeout-minutes: 20 + # TEMPORARY: code scanning (SARIF upload) requires GitHub Advanced Security + # on PRIVATE repositories. SharpLsp is private until the go-public flip, so + # the upload fails with "Code Security must be enabled for this repository". + # Gate on public visibility so this self-enables the moment the repo is made + # public — no follow-up edit needed. [TODO-GO-PUBLIC: confirm green once public] + if: github.event.repository.visibility == 'public' + permissions: + security-events: write + actions: read + contents: read + strategy: + fail-fast: false + matrix: + language: [javascript-typescript, actions] + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Initialize CodeQL + uses: github/codeql-action/init@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3 + with: + language: ${{ matrix.language }} + queries: security-extended + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index e15e2dff..5a9fdb90 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -22,10 +22,10 @@ jobs: run: working-directory: website steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: submodules: true - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '20' cache: npm @@ -34,7 +34,7 @@ jobs: - name: Build site run: npm run build - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 with: path: website/_site deploy: @@ -48,4 +48,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e8ee408c..49281281 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,8 +27,8 @@ jobs: outputs: version: "${{ steps.extract.outputs.version }}" steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '24' - name: Extract version from tag @@ -82,25 +82,26 @@ jobs: rust_target: aarch64-pc-windows-msvc can_execute: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 # Each build job stamps the version into the runner working tree from the # tag — deterministic, no artifact hand-off, nothing committed. # [CI-RELEASE] [SWR-VERSION-BUILD-STAMPING] - name: Stamp release version shell: bash run: make _stamp-version VERSION="${{ needs.version.outputs.version }}" - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable with: + toolchain: stable targets: "${{ matrix.rust_target }}" - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: key: "${{ matrix.rust_target }}" - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 with: dotnet-version: | 9.0.x 10.0.203 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '20' cache: npm @@ -147,7 +148,7 @@ jobs: echo "::error::foreign-platform binary found in dist/sharplsp-${{ matrix.platform }}.vsix" exit 1 fi - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: vsix-${{ matrix.platform }} path: dist/*.vsix @@ -163,7 +164,7 @@ jobs: permissions: contents: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: path: artifacts pattern: vsix-* @@ -227,10 +228,10 @@ jobs: contents: read id-token: write steps: - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '20' - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: path: artifacts pattern: vsix-* @@ -239,7 +240,7 @@ jobs: # secret). The publisher identity has no Azure subscription role — its only # authorization is Marketplace publisher membership — hence # allow-no-subscriptions. - - uses: azure/login@v2 + - uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2 with: client-id: "${{ secrets.AZURE_CLIENT_ID }}" tenant-id: "${{ secrets.AZURE_TENANT_ID }}" @@ -294,10 +295,10 @@ jobs: permissions: contents: read steps: - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '20' - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: path: artifacts pattern: vsix-* diff --git a/.gitignore b/.gitignore index 1cb17a71..0b51a605 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,11 @@ mutants.out/ *.key !*.pub.key +# Per-developer machine-local agent config (never publish — leaks local paths/usernames) +.claude/settings.local.json +**/settings.local.json +.claude/**/*.local.json + # Temporary tmp/ temp/ diff --git a/CLAUDE.md b/CLAUDE.md index 255059ee..7686e542 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,10 @@ # CLAUDE.md -⚠️ Never kill VS Code processes — not desktop, not browser. They belong to the user. -⚠️ Don't ask the user questions — use your judgment. +⚠️ Never kill VS Code processes — not desktop, not browser. They belong to the user. ⚠️ + +⚠️ Don't ask the user questions — use your judgment. ⚠️ + +⚠️ Don't use git. Especially critical: don't stamp yourself as coauthor on commits ⚠️ SharpLsp is an open-source, editor-agnostic .NET LSP (C# + F#) built in Rust. One LSP server = complete .NET development experience across every editor. diff --git a/Makefile b/Makefile index a8c281ff..39bb37f1 100644 --- a/Makefile +++ b/Makefile @@ -114,8 +114,8 @@ _build-dotnet: exit 1; \ } @echo "==> Building sidecars ($(DOTNET_CFG))..." - dotnet publish $(SIDECAR_CS)/SharpLsp.Sidecar.CSharp.csproj --configuration $(DOTNET_CFG) --no-self-contained $(if $(VERSION),-p:Version=$(VERSION) -p:PackageVersion=$(VERSION),) --output $(SIDECAR_CS_OUT) - dotnet publish $(SIDECAR_FS)/SharpLsp.Sidecar.FSharp.fsproj --configuration $(DOTNET_CFG) --no-self-contained $(if $(VERSION),-p:Version=$(VERSION) -p:PackageVersion=$(VERSION),) --output $(SIDECAR_FS_OUT) + dotnet publish $(SIDECAR_CS)/SharpLsp.Sidecar.CSharp.csproj --configuration $(DOTNET_CFG) --no-self-contained -p:DebugType=none -p:DebugSymbols=false $(if $(VERSION),-p:Version=$(VERSION) -p:PackageVersion=$(VERSION),) --output $(SIDECAR_CS_OUT) + dotnet publish $(SIDECAR_FS)/SharpLsp.Sidecar.FSharp.fsproj --configuration $(DOTNET_CFG) --no-self-contained -p:DebugType=none -p:DebugSymbols=false $(if $(VERSION),-p:Version=$(VERSION) -p:PackageVersion=$(VERSION),) --output $(SIDECAR_FS_OUT) _build-vsix: _stage-vsix-binary @echo "==> Packaging VS Code extension (host: $(HOST_PLATFORM))..." diff --git a/README.md b/README.md index b613c74c..efa95905 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

Open-source, editor-agnostic .NET language tooling for C# and F#.
- Available today in VS Code, Zed, and Rider. Neovim, Helix, and Emacs support coming soon. + Available today in VS Code. Rider, Zed, Neovim, Helix, and Emacs support is on the way.

@@ -50,9 +50,9 @@ That split keeps the editor protocol fast and portable while letting the .NET co Install the SharpLsp extension from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=nimblesite.sharplsp). The extension ships with the SharpLsp engine and both sidecars — no Rust toolchain or separate install required. -### Neovim & Zed +### Other editors -Neovim and Zed support are coming soon. +Rider, Zed, Neovim, Helix, and Emacs support is coming soon. ## Documentation diff --git a/coverage-thresholds.json b/coverage-thresholds.json index 57a4a96f..dc2abdd7 100644 --- a/coverage-thresholds.json +++ b/coverage-thresholds.json @@ -14,6 +14,6 @@ "line_percent": 80.48 }, "sharplsp-sidecar-common": { - "line_percent": 80.09 + "line_percent": 80.34 } } diff --git a/editors/vscode/package.json b/editors/vscode/package.json index db9c74c4..02d3612f 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -42,6 +42,19 @@ "workspaceContains:**/*.fsproj" ], "main": "./dist/extension.js", + "capabilities": { + "untrustedWorkspaces": { + "supported": "limited", + "description": "In an untrusted workspace, SharpLsp ignores workspace-provided settings that select which executable to run (server/sidecar/debug-adapter paths and extra server arguments) and runs only the bundled, vetted binaries. Grant workspace trust to use custom executable paths.", + "restrictedConfigurations": [ + "sharplsp.lspPath", + "sharplsp.csharpSidecarPath", + "sharplsp.fsharpSidecarPath", + "sharplsp.server.extraArgs", + "sharplsp.debug.netcoredbgPath" + ] + } + }, "contributes": { "languages": [ { @@ -603,6 +616,12 @@ "title": "%cmd.nuget.update%", "category": "%category%" }, + { + "command": "sharplsp.newSolution", + "title": "%cmd.newSolution%", + "category": "%category%", + "icon": "$(files)" + }, { "command": "sharplsp.newProject", "title": "%cmd.newProject%", @@ -711,6 +730,16 @@ } ], "view/title": [ + { + "command": "sharplsp.newSolution", + "when": "view == sharplsp.solutionExplorer", + "group": "navigation@1" + }, + { + "command": "sharplsp.newProject", + "when": "view == sharplsp.solutionExplorer", + "group": "navigation@2" + }, { "command": "sharplsp.selectSolution", "when": "view == sharplsp.solutionExplorer", @@ -840,6 +869,21 @@ "when": "view == sharplsp.solutionExplorer && viewItem == project", "group": "2_build" }, + { + "command": "sharplsp.build", + "when": "view == sharplsp.solutionExplorer && viewItem == solution", + "group": "2_build" + }, + { + "command": "sharplsp.rebuild", + "when": "view == sharplsp.solutionExplorer && viewItem == solution", + "group": "2_build" + }, + { + "command": "sharplsp.clean", + "when": "view == sharplsp.solutionExplorer && viewItem == solution", + "group": "2_build" + }, { "command": "sharplsp.addProjectReference", "when": "view == sharplsp.solutionExplorer && viewItem == dependencyFolder", diff --git a/editors/vscode/package.nls.ja.json b/editors/vscode/package.nls.ja.json index 446fdcea..299f99f0 100644 --- a/editors/vscode/package.nls.ja.json +++ b/editors/vscode/package.nls.ja.json @@ -37,7 +37,7 @@ "viewsContainer.title": "SharpLsp", "view.solutionExplorer.name": "ソリューションエクスプローラー", "view.profiler.name": "プロファイラー", - "viewWelcome.solutionExplorer.contents": "このワークスペースには .sln または .slnx ファイルが見つかりません。", + "viewWelcome.solutionExplorer.contents": "このワークスペースにソリューションが見つかりません。\n[新しいソリューション](command:sharplsp.newSolution)\n[新しいプロジェクト](command:sharplsp.newProject)\nまたは[既存のソリューションを開く](command:sharplsp.selectSolution)。", "cmd.restartServer": "Language Server を再起動", "cmd.retryDotnetAcquisition": ".NET ランタイム取得を再試行", @@ -83,6 +83,7 @@ "cmd.clean": "クリーン", "cmd.nuget.add": "NuGet パッケージを追加", "cmd.nuget.update": "NuGet パッケージを更新", + "cmd.newSolution": "新しいソリューション", "cmd.newProject": "新しいプロジェクト", "cmd.newFile": "新しいファイル(Class / Interface / Enum / Struct / Record)", "cmd.addProjectToSolution": "ソリューションにプロジェクトを追加", diff --git a/editors/vscode/package.nls.json b/editors/vscode/package.nls.json index 9c4a31ab..6db967dd 100644 --- a/editors/vscode/package.nls.json +++ b/editors/vscode/package.nls.json @@ -37,7 +37,7 @@ "viewsContainer.title": "SharpLsp", "view.solutionExplorer.name": "Solution Explorer", "view.profiler.name": "Profiler", - "viewWelcome.solutionExplorer.contents": "No .sln or .slnx files found in this workspace.", + "viewWelcome.solutionExplorer.contents": "No solution found in this workspace.\n[New Solution](command:sharplsp.newSolution)\n[New Project](command:sharplsp.newProject)\nOr [open an existing solution](command:sharplsp.selectSolution).", "cmd.restartServer": "Restart Language Server", "cmd.retryDotnetAcquisition": "Retry .NET Runtime Acquisition", @@ -83,6 +83,7 @@ "cmd.clean": "Clean", "cmd.nuget.add": "Add NuGet Package", "cmd.nuget.update": "Update NuGet Package", + "cmd.newSolution": "New Solution", "cmd.newProject": "New Project", "cmd.newFile": "New File (Class/Interface/Enum/Struct/Record)", "cmd.addProjectToSolution": "Add Project to Solution", diff --git a/editors/vscode/package.nls.zh-cn.json b/editors/vscode/package.nls.zh-cn.json index 6d03d2a0..144086d8 100644 --- a/editors/vscode/package.nls.zh-cn.json +++ b/editors/vscode/package.nls.zh-cn.json @@ -37,7 +37,7 @@ "viewsContainer.title": "SharpLsp", "view.solutionExplorer.name": "解决方案资源管理器", "view.profiler.name": "性能分析器", - "viewWelcome.solutionExplorer.contents": "在此工作区中未找到 .sln 或 .slnx 文件。", + "viewWelcome.solutionExplorer.contents": "在此工作区中未找到解决方案。\n[新建解决方案](command:sharplsp.newSolution)\n[新建项目](command:sharplsp.newProject)\n或[打开现有解决方案](command:sharplsp.selectSolution)。", "cmd.restartServer": "重启 Language Server", "cmd.retryDotnetAcquisition": "重试 .NET 运行时获取", @@ -83,6 +83,7 @@ "cmd.clean": "清理", "cmd.nuget.add": "添加 NuGet 包", "cmd.nuget.update": "更新 NuGet 包", + "cmd.newSolution": "新建解决方案", "cmd.newProject": "新建项目", "cmd.newFile": "新建文件(Class/Interface/Enum/Struct/Record)", "cmd.addProjectToSolution": "将项目添加到解决方案", diff --git a/editors/vscode/src/build.ts b/editors/vscode/src/build.ts index fb1a4194..c6bff931 100644 --- a/editors/vscode/src/build.ts +++ b/editors/vscode/src/build.ts @@ -29,10 +29,7 @@ export class SharpLspBuildTaskProvider implements vscode.TaskProvider { } function createBuildTask(command: string, label: string): vscode.Task { - const dotnetCommand = command === 'rebuild' ? 'build' : command; - const args = command === 'rebuild' ? ['--no-incremental'] : []; - - const execution = new vscode.ShellExecution('dotnet', [dotnetCommand, ...args]); + const execution = new vscode.ShellExecution('dotnet', dotnetArgs(command)); const task = new vscode.Task( { type: SharpLspBuildTaskProvider.Type, command }, vscode.TaskScope.Workspace, @@ -85,17 +82,12 @@ function parseBuildDiagnostics(output: string): void { } } -/** Run dotnet build and capture diagnostics. */ -export async function buildWithDiagnostics( - command: string, - extraArgs: string[] = [], -): Promise { +/** Run dotnet build/clean for an optional target file and capture diagnostics. */ +export async function buildWithDiagnostics(command: string, target?: string): Promise { const folder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; if (folder === undefined) return; - const dotnetCommand = command === 'rebuild' ? 'build' : command; - const args = [dotnetCommand, ...extraArgs]; - if (command === 'rebuild') args.push('--no-incremental'); + const args = dotnetArgs(command, target); try { const output = await new Promise((resolve, reject) => { @@ -114,6 +106,11 @@ export async function buildWithDiagnostics( } } +/** A solution/project tree node that can supply an MSBuild target file path. */ +interface BuildTarget { + readonly projectFilePath?: string; +} + /** * Register build commands and task provider. */ @@ -128,39 +125,61 @@ export function registerBuildCommands(context: vscode.ExtensionContext): void { ); context.subscriptions.push( - vscode.commands.registerCommand(CMD_BUILD, async () => { - info('Running dotnet build'); - runDotnetCommand('build'); - await buildWithDiagnostics('build'); + vscode.commands.registerCommand(CMD_BUILD, async (node?: BuildTarget) => { + await runDotnetTask('build', node); }), - ); - - context.subscriptions.push( - vscode.commands.registerCommand(CMD_REBUILD, async () => { - info('Running dotnet rebuild'); - runDotnetCommand('build', ['--no-incremental']); - await buildWithDiagnostics('rebuild'); + vscode.commands.registerCommand(CMD_REBUILD, async (node?: BuildTarget) => { + await runDotnetTask('rebuild', node); }), - ); - - context.subscriptions.push( - vscode.commands.registerCommand(CMD_CLEAN, () => { - info('Running dotnet clean'); - runDotnetCommand('clean'); - diagnosticCollection.clear(); + vscode.commands.registerCommand(CMD_CLEAN, async (node?: BuildTarget) => { + await runDotnetTask('clean', node); }), ); } -function runDotnetCommand(command: string, extraArgs: string[] = []): void { +/** Run build/rebuild/clean for the workspace, or for a right-clicked solution/project node. */ +async function runDotnetTask(command: string, node?: BuildTarget): Promise { + const target = targetFromNode(node); + const scope = target !== undefined ? ` for ${target}` : ''; + info(`Running dotnet ${command}${scope}`); + runDotnetCommand(command, target); + if (command === 'clean') { + diagnosticCollection.clear(); + return; + } + await buildWithDiagnostics(command, target); +} + +/** Resolve the .sln/.csproj/.fsproj a node represents, if any. */ +function targetFromNode(node?: BuildTarget): string | undefined { + const target = node?.projectFilePath; + return target !== undefined && target.length > 0 ? target : undefined; +} + +/** Build the dotnet CLI argument list for a command targeting an optional file. */ +function dotnetArgs(command: string, target?: string): string[] { + const dotnetCommand = command === 'rebuild' ? 'build' : command; + const args = [dotnetCommand]; + if (target !== undefined) args.push(target); + if (command === 'rebuild') args.push('--no-incremental'); + return args; +} + +function runDotnetCommand(command: string, target?: string): void { try { const terminal = vscode.window.terminals.find((t) => t.name === 'SharpLsp Build') ?? vscode.window.createTerminal('SharpLsp Build'); terminal.show(true); - terminal.sendText(`dotnet ${command} ${extraArgs.join(' ')}`.trim()); + const args = dotnetArgs(command, target).map(quoteArg); + terminal.sendText(`dotnet ${args.join(' ')}`); } catch (err) { const message = err instanceof Error ? err.message : String(err); void vscode.window.showErrorMessage(`Build failed: ${message}`); } } + +/** Wrap an argument in double quotes when it contains whitespace. */ +function quoteArg(value: string): string { + return value.includes(' ') ? `"${value}"` : value; +} diff --git a/editors/vscode/src/config.ts b/editors/vscode/src/config.ts index 554ba58a..24d000b7 100644 --- a/editors/vscode/src/config.ts +++ b/editors/vscode/src/config.ts @@ -10,13 +10,30 @@ function section(): ReturnType { return workspace.getConfiguration(CONFIG_SECTION); } -/** User-configured path to the sharplsp binary, or empty string. */ +/** + * User-configured path to the sharplsp binary, or empty string. + * + * Implements [DIST-WORKSPACE-TRUST]: this value selects which executable is + * spawned as the language server, so an untrusted workspace must never be able + * to supply it. `package.json` already lists `sharplsp.lspPath` under + * `capabilities.untrustedWorkspaces.restrictedConfigurations`, which makes + * VS Code ignore workspace-scoped values until trust is granted; this guard is + * defence-in-depth so the dangerous path is never honoured in an untrusted + * window even if the declarative restriction is ever lifted. + */ export function serverPath(): string { + if (!workspace.isTrusted) return ''; return section().get(CONFIG_SERVER_PATH) ?? ''; } -/** Extra CLI arguments to pass to the server process. */ +/** + * Extra CLI arguments to pass to the server process. + * + * Implements [DIST-WORKSPACE-TRUST]: extra args are applied to whatever server + * binary runs, so an untrusted workspace must not be able to inject them. + */ export function serverExtraArgs(): readonly string[] { + if (!workspace.isTrusted) return []; return section().get(CONFIG_SERVER_EXTRA_ARGS) ?? []; } diff --git a/editors/vscode/src/constants.ts b/editors/vscode/src/constants.ts index 0184b172..9e403eb0 100644 --- a/editors/vscode/src/constants.ts +++ b/editors/vscode/src/constants.ts @@ -67,6 +67,12 @@ export const CMD_NUGET_ADD_FROM_EXPLORER = 'sharplsp.nuget.addFromExplorer'; export const CMD_OPEN_PROJECT_FILE = 'sharplsp.openProjectFile'; export const CMD_ADD_PROJECT_REFERENCE = 'sharplsp.addProjectReference'; +// Scaffolding (dotnet new) +export const CMD_NEW_SOLUTION = 'sharplsp.newSolution'; +export const CMD_NEW_PROJECT = 'sharplsp.newProject'; +export const CMD_NEW_FILE = 'sharplsp.newFile'; +export const CMD_ADD_PROJECT_TO_SOLUTION = 'sharplsp.addProjectToSolution'; + // F# Interactive export const CMD_FSI_SEND_SELECTION = 'sharplsp.fsi.sendSelection'; export const CMD_FSI_SEND_FILE = 'sharplsp.fsi.sendFile'; diff --git a/editors/vscode/src/extension.ts b/editors/vscode/src/extension.ts index b761420f..f5966ab0 100644 --- a/editors/vscode/src/extension.ts +++ b/editors/vscode/src/extension.ts @@ -194,6 +194,21 @@ async function activateInner(context: ExtensionContext): Promise { + log.info( + 'Workspace trust granted — restarting language server to apply trusted configuration.', + ); + void lspClient?.restart().catch((err: unknown) => { + log.error(`Restart after trust grant failed: ${getErrorMessage(err)}`); + }); + }), + ); + log.info('step 13: activate complete'); return { explorerProvider, diff --git a/editors/vscode/src/scaffolding.ts b/editors/vscode/src/scaffolding.ts index 10acc49c..d15acd01 100644 --- a/editors/vscode/src/scaffolding.ts +++ b/editors/vscode/src/scaffolding.ts @@ -5,6 +5,14 @@ import * as path from 'path'; import { info } from './log'; import { findSolutions } from './solution.js'; import * as state from './state.js'; +import { + CMD_NEW_SOLUTION, + CMD_NEW_PROJECT, + CMD_NEW_FILE, + CMD_ADD_PROJECT_TO_SOLUTION, + CMD_OPEN_SOLUTION, + CMD_REFRESH_EXPLORER, +} from './constants.js'; const PROJECT_TEMPLATES = [ { label: 'Console App', template: 'console' }, @@ -27,9 +35,90 @@ const FILE_TEMPLATES = [ { label: 'Record', snippet: 'record' }, ]; -/** Create a new .NET project via `dotnet new`. */ -async function newProject(): Promise { - const pick = await vscode.window.showQuickPick( +// ── .NET CLI core (no UI — pure + testable) ───────────────────── + +/** Build the `dotnet new sln` argument vector. */ +export function newSolutionArgs(name: string, folder: string): string[] { + return ['new', 'sln', '--name', name, '--output', folder]; +} + +/** Build the `dotnet new