Move lsp server to this repo (#5619)

This commit is contained in:
Jess Frazelle
2025-03-04 22:21:12 -08:00
committed by GitHub
parent e8af61e11f
commit 37715d9fa8
47 changed files with 5929 additions and 28 deletions

View File

@ -1,4 +1,6 @@
rust/* rust/**/*.ts
!rust/kcl-language-server/client/src/**/*.ts
*.typegen.ts *.typegen.ts
packages/codemirror-lsp-client/dist/* packages/codemirror-lsp-client/dist/*
e2e/playwright/snapshots/prompt-to-edit/* e2e/playwright/snapshots/prompt-to-edit/*
.vscode-test

View File

@ -0,0 +1,8 @@
FROM node:slim
COPY . /action
WORKDIR /action
RUN npm install --production
ENTRYPOINT ["node", "/action/main.js"]

View File

@ -0,0 +1,21 @@
# github-release
Copy-pasted from
https://github.com/bytecodealliance/wasmtime/tree/8acfdbdd8aa550d1b84e0ce1e6222a6605d14e38/.github/actions/github-release
An action used to publish GitHub releases for `wasmtime`.
As of the time of this writing there's a few actions floating around which
perform github releases but they all tend to have their set of drawbacks.
Additionally nothing handles deleting releases which we need for our rolling
`dev` release.
To handle all this, this action rolls its own implementation using the
actions/toolkit repository and packages published there. These run in a Docker
container and take various inputs to orchestrate the release from the build.
More comments can be found in `main.js`.
Testing this is really hard. If you want to try though run `npm install` and
then `node main.js`. You'll have to configure a bunch of env vars though to get
anything reasonably working.

View File

@ -0,0 +1,15 @@
name: "wasmtime github releases"
description: "wasmtime github releases"
inputs:
token:
description: ""
required: true
name:
description: ""
required: true
files:
description: ""
required: true
runs:
using: "docker"
image: "Dockerfile"

143
.github/actions/github-release/main.js vendored Normal file
View File

@ -0,0 +1,143 @@
const core = require("@actions/core");
const path = require("path");
const fs = require("fs");
const github = require("@actions/github");
const glob = require("glob");
function sleep(milliseconds) {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
async function runOnce() {
// Load all our inputs and env vars. Note that `getInput` reads from `INPUT_*`
const files = core.getInput("files");
const name = core.getInput("name");
const token = core.getInput("token");
const slug = process.env.GITHUB_REPOSITORY;
const owner = slug.split("/")[0];
const repo = slug.split("/")[1];
const sha = process.env.HEAD_SHA;
core.info(`files: ${files}`);
core.info(`name: ${name}`);
const options = {
request: {
timeout: 30000,
},
};
const octokit = github.getOctokit(token, options);
// Delete the previous release since we can't overwrite one. This may happen
// due to retrying an upload or it may happen because we're doing the dev
// release.
const releases = await octokit.paginate("GET /repos/:owner/:repo/releases", { owner, repo });
for (const release of releases) {
if (release.tag_name !== name) {
continue;
}
const release_id = release.id;
core.info(`deleting release ${release_id}`);
await octokit.rest.repos.deleteRelease({ owner, repo, release_id });
}
// We also need to update the `dev` tag while we're at it on the `dev` branch.
if (name == "nightly") {
try {
core.info(`updating nightly tag`);
await octokit.rest.git.updateRef({
owner,
repo,
ref: "tags/nightly",
sha,
force: true,
});
} catch (e) {
core.error(e);
core.info(`creating nightly tag`);
await octokit.rest.git.createTag({
owner,
repo,
tag: "nightly",
message: "nightly release",
object: sha,
type: "commit",
});
}
}
// Creates an official GitHub release for this `tag`, and if this is `dev`
// then we know that from the previous block this should be a fresh release.
core.info(`creating a release`);
const release = await octokit.rest.repos.createRelease({
owner,
repo,
name,
tag_name: name,
target_commitish: sha,
prerelease: name === "nightly",
});
const release_id = release.data.id;
// Upload all the relevant assets for this release as just general blobs.
for (const file of glob.sync(files)) {
const size = fs.statSync(file).size;
const name = path.basename(file);
await runWithRetry(async function () {
// We can't overwrite assets, so remove existing ones from a previous try.
let assets = await octokit.rest.repos.listReleaseAssets({
owner,
repo,
release_id,
});
for (const asset of assets.data) {
if (asset.name === name) {
core.info(`delete asset ${name}`);
const asset_id = asset.id;
await octokit.rest.repos.deleteReleaseAsset({ owner, repo, asset_id });
}
}
core.info(`upload ${file}`);
const headers = { "content-length": size, "content-type": "application/octet-stream" };
const data = fs.createReadStream(file);
await octokit.rest.repos.uploadReleaseAsset({
data,
headers,
name,
url: release.data.upload_url,
});
});
}
}
async function runWithRetry(f) {
const retries = 10;
const maxDelay = 4000;
let delay = 1000;
for (let i = 0; i < retries; i++) {
try {
await f();
break;
} catch (e) {
if (i === retries - 1) throw e;
core.error(e);
const currentDelay = Math.round(Math.random() * delay);
core.info(`sleeping ${currentDelay} ms`);
await sleep(currentDelay);
delay = Math.min(delay * 2, maxDelay);
}
}
}
async function run() {
await runWithRetry(runOnce);
}
run().catch((err) => {
core.error(err);
core.setFailed(err.message);
});

View File

@ -0,0 +1,10 @@
{
"name": "wasmtime-github-release",
"version": "0.0.0",
"main": "main.js",
"dependencies": {
"@actions/core": "^1.6",
"@actions/github": "^5.0",
"glob": "^7.1.5"
}
}

View File

@ -10,6 +10,7 @@ updates:
- '/' - '/'
- '/packages/codemirror-lang-kcl/' - '/packages/codemirror-lang-kcl/'
- '/packages/codemirror-lsp-client/' - '/packages/codemirror-lsp-client/'
- '/rust/kcl-language-server/'
schedule: schedule:
interval: weekly interval: weekly
day: monday day: monday

View File

@ -0,0 +1,401 @@
name: kcl-language-server
on:
push:
branches:
- main
paths:
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- 'rust/kcl-language-server/**'
- '**.rs'
- .github/workflows/kcl-language-server.yml
tags:
- 'kcl-*'
pull_request:
paths:
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- 'rust/kcl-language-server/**'
- '**.rs'
- .github/workflows/kcl-language-server.yml
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
RUSTFLAGS: ""
RUSTUP_MAX_RETRIES: 10
FETCH_DEPTH: 0 # pull in the tags for the version string
MACOSX_DEPLOYMENT_TARGET: 10.15
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_LINKER: arm-linux-gnueabihf-gcc
jobs:
test:
name: vscode tests
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Install dependencies
run: |
yarn install
cd rust/kcl-language-server
yarn install
- name: Run tests
run: |
cd rust/kcl-language-server
yarn build
yarn test-compile
ls -la dist
xvfb-run -a yarn test
if: runner.os == 'Linux'
- name: Run tests
run: |
cd rust/kcl-language-server
yarn test
if: runner.os != 'Linux'
build-release:
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
target: x86_64-pc-windows-msvc
code-target:
win32-x64
#- os: windows-latest
#target: i686-pc-windows-msvc
#code-target:
#win32-ia32
#- os: windows-latest
#target: aarch64-pc-windows-msvc
#code-target: win32-arm64
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
code-target:
linux-x64
#- os: ubuntu-latest
#target: aarch64-unknown-linux-musl
#code-target: linux-arm64
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
code-target: linux-arm64
- os: ubuntu-latest
target: arm-unknown-linux-gnueabihf
code-target: linux-armhf
- os: macos-latest
target: x86_64-apple-darwin
code-target: darwin-x64
- os: macos-latest
target: aarch64-apple-darwin
code-target: darwin-arm64
name: build-release (${{ matrix.target }})
runs-on: ${{ matrix.os }}
container: ${{ matrix.container }}
env:
RA_TARGET: ${{ matrix.target }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: ${{ env.FETCH_DEPTH }}
- name: Use correct Rust toolchain
shell: bash
run: |
rm rust/rust-toolchain.toml
- name: Install rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: rust
components: rust-src
target: ${{ matrix.target }}
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Update apt repositories
if: matrix.target == 'aarch64-unknown-linux-gnu' || matrix.target == 'arm-unknown-linux-gnueabihf' || matrix.os == 'ubuntu-latest'
run: sudo apt-get update
- if: ${{ matrix.os == 'ubuntu-latest' }}
name: Install deps
shell: bash
run: |
sudo apt install -y \
ca-certificates \
clang \
cmake \
curl \
g++ \
gcc \
gcc-mingw-w64-i686 \
gcc-mingw-w64 \
jq \
libmpc-dev \
libmpfr-dev \
libgmp-dev \
libssl-dev \
libxml2-dev \
mingw-w64 \
wget \
zlib1g-dev
cargo install cross
- name: Install AArch64 target toolchain
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: sudo apt-get install gcc-aarch64-linux-gnu
- name: Install ARM target toolchain
if: matrix.target == 'arm-unknown-linux-gnueabihf'
run: sudo apt-get install gcc-arm-linux-gnueabihf
- name: build
run: |
cd rust
cargo kcl-language-server-release build --client-patch-version ${{ github.run_number }}
- name: Install dependencies
run: |
cd rust/kcl-language-server
yarn install
- name: Package Extension (release)
if: startsWith(github.event.ref, 'refs/tags/')
run: |
cd rust/kcl-language-server
npx vsce package -o "../build/kcl-language-server-${{ matrix.code-target }}.vsix" --target ${{ matrix.code-target }}
- name: Package Extension (nightly)
if: startsWith(github.event.ref, 'refs/tags/') == false
run: |
cd rust/kcl-language-server
npx vsce package -o "../build/kcl-language-server-${{ matrix.code-target }}.vsix" --target ${{ matrix.code-target }} --pre-release
- name: remove server
if: matrix.target == 'x86_64-unknown-linux-gnu'
run: |
cd rust/kcl-language-server
rm -rf server
- name: Package Extension (no server, release)
if: matrix.target == 'x86_64-unknown-linux-gnu' && startsWith(github.event.ref, 'refs/tags/')
run: |
cd rust/kcl-language-server
npx vsce package -o ../build/kcl-language-server-no-server.vsix
- name: Package Extension (no server, nightly)
if: matrix.target == 'x86_64-unknown-linux-gnu' && startsWith(github.event.ref, 'refs/tags/') == false
run: |
cd rust/kcl-language-server
npx vsce package -o ../build/kcl-language-server-no-server.vsix --pre-release
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: release-${{ matrix.target }}
path: ./rust/build
build-release-x86_64-unknown-linux-musl:
name: build-release (x86_64-unknown-linux-musl)
runs-on: ubuntu-latest
env:
RA_TARGET: x86_64-unknown-linux-musl
# For some reason `-crt-static` is not working for clang without lld
RUSTFLAGS: "-C link-arg=-fuse-ld=lld -C target-feature=-crt-static"
container:
image: alpine:latest
volumes:
- /usr/local/cargo/registry:/usr/local/cargo/registry
steps:
- name: Install dependencies
run: |
apk add --no-cache \
bash \
curl \
git \
clang \
lld \
musl-dev \
nodejs \
yarn \
npm
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: ${{ env.FETCH_DEPTH }}
- name: Use correct Rust toolchain
shell: bash
run: |
rm rust/rust-toolchain.toml
- name: Install rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: rust
components: rust-src
target: ${{ matrix.target }}
- name: build
run: |
cd rust
cargo kcl-language-server-release build --client-patch-version ${{ github.run_number }}
- name: Install dependencies
run: |
cd rust/kcl-language-server
yarn install
- name: Package Extension (release)
if: startsWith(github.event.ref, 'refs/tags/')
run: |
cd rust/kcl-language-server
npx vsce package -o "../build/kcl-language-server-alpine-x64.vsix" --target alpine-x64
- name: Package Extension (release)
if: startsWith(github.event.ref, 'refs/tags/') == false
run: |
cd rust/kcl-language-server
npx vsce package -o "../build/kcl-language-server-alpine-x64.vsix" --target alpine-x64 --pre-release
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: release-x86_64-unknown-linux-musl
path: ./rust/build
publish:
name: publish
runs-on: ubuntu-latest
needs: ["build-release", "build-release-x86_64-unknown-linux-musl"]
if: startsWith(github.event.ref, 'refs/tags')
permissions:
contents: write
steps:
- run: echo "TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- run: 'echo "TAG: $TAG"'
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: ${{ env.FETCH_DEPTH }}
- name: Install Nodejs
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- run: echo "HEAD_SHA=$(git rev-parse HEAD)" >> $GITHUB_ENV
- run: 'echo "HEAD_SHA: $HEAD_SHA"'
- uses: actions/download-artifact@v4
with:
name: release-aarch64-apple-darwin
path: rust/build
- uses: actions/download-artifact@v4
with:
name: release-x86_64-apple-darwin
path: rust/build
- uses: actions/download-artifact@v4
with:
name: release-x86_64-unknown-linux-gnu
path: rust/build
- uses: actions/download-artifact@v4
with:
name: release-x86_64-unknown-linux-musl
path: rust/build
- uses: actions/download-artifact@v4
with:
name: release-aarch64-unknown-linux-gnu
path: rust/build
- uses: actions/download-artifact@v4
with:
name: release-arm-unknown-linux-gnueabihf
path: rust/build
- uses: actions/download-artifact@v4
with:
name: release-x86_64-pc-windows-msvc
path:
rust/build
#- uses: actions/download-artifact@v4
#with:
#name: release-i686-pc-windows-msvc
#path:
#build
#- uses: actions/download-artifact@v4
#with:
#name: release-aarch64-pc-windows-msvc
#path: rust/build
- run: ls -al ./rust/build
- name: Publish Release
uses: ./.github/actions/github-release
with:
files: "rust/build/*"
name: ${{ env.TAG }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: move files to dir for upload
shell: bash
run: |
cd rust
mkdir -p releases/language-server/${{ env.TAG }}
cp -r build/* releases/language-server/${{ env.TAG }}
- name: "Authenticate to Google Cloud"
uses: "google-github-actions/auth@v2.1.7"
with:
credentials_json: "${{ secrets.GOOGLE_CLOUD_DL_SA }}"
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2.1.2
with:
project_id: kittycadapi
- name: "upload files to gcp"
id: upload-files
uses: google-github-actions/upload-cloud-storage@v2.2.1
with:
path: rust/releases
destination: dl.kittycad.io
- run: rm rust/build/kcl-language-server-no-server.vsix
- name: Publish Extension (Code Marketplace, release)
# token from https://dev.azure.com/kcl-language-server/
run: |
cd rust/kcl-language-server
npx vsce publish --pat ${{ secrets.VSCE_PAT }} --packagePath ../build/kcl-language-server-*.vsix
- name: Publish Extension (OpenVSX, release)
run: |
cd rust/kcl-language-server
npx ovsx publish --pat ${{ secrets.OPENVSX_TOKEN }} --packagePath ../build/kcl-language-server-*.vsix
timeout-minutes: 2

View File

@ -52,6 +52,7 @@ jobs:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'
- run: yarn install - run: yarn install
- run: yarn --cwd ./rust/kcl-language-server --modules-folder node_modules install
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
workspaces: './rust' workspaces: './rust'
@ -72,6 +73,7 @@ jobs:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'
- run: yarn install - run: yarn install
- run: yarn --cwd ./rust/kcl-language-server --modules-folder node_modules install
- run: yarn lint - run: yarn lint
python-codespell: python-codespell:
@ -141,7 +143,7 @@ jobs:
- name: run unit tests - name: run unit tests
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }} if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: yarn test:unit run: xvfb-run -a yarn test:unit
env: env:
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}

12
.gitignore vendored
View File

@ -1,7 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies # dependencies
/node_modules node_modules
/.pnp /.pnp
.pnp.js .pnp.js
@ -9,7 +9,7 @@
/coverage /coverage
# production # production
/build build
# misc # misc
.DS_Store .DS_Store
@ -35,6 +35,10 @@ rust/lcov.info
rust/kcl-wasm-lib/pkg rust/kcl-wasm-lib/pkg
*.snap.new *.snap.new
rust/kcl-lib/fuzz/Cargo.lock rust/kcl-lib/fuzz/Cargo.lock
rust/kcl-language-server-release/Cargo.lock
# kcl language server package
rust/kcl-language-server/dist/
e2e/playwright/playwright-secrets.env e2e/playwright/playwright-secrets.env
e2e/playwright/temp1.png e2e/playwright/temp1.png
@ -71,5 +75,7 @@ out/
# python # python
__pycache__/ __pycache__/
uv.lock uv.lock
rust/kcl-python-bindings/dist dist
venv venv
.vscode-test

View File

@ -7,10 +7,10 @@ coverage
*.rs *.rs
*.hbs *.hbs
target target
rust/
e2e/playwright/export-snapshots e2e/playwright/export-snapshots
e2e/playwright/snapshots/prompt-to-edit e2e/playwright/snapshots/prompt-to-edit
# XState generated files # XState generated files
src/machines/**.typegen.ts src/machines/**.typegen.ts
.vscode-test

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -87,8 +87,8 @@
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &", "simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
"simpleserver:bg": "yarn pretest && http-server ./public --cors -p 3000 &", "simpleserver:bg": "yarn pretest && http-server ./public --cors -p 3000 &",
"simpleserver:stop": "kill-port 3000", "simpleserver:stop": "kill-port 3000",
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages", "fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages ./rust/kcl-language-server",
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages", "fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages ./rust/kcl-language-server",
"fetch:wasm": "./scripts/get-latest-wasm-bundle.sh", "fetch:wasm": "./scripts/get-latest-wasm-bundle.sh",
"fetch:wasm:windows": "./scripts/get-latest-wasm-bundle.ps1", "fetch:wasm:windows": "./scripts/get-latest-wasm-bundle.ps1",
"fetch:samples": "echo \"Fetching latest KCL samples...\" && curl -o public/kcl-samples-manifest-fallback.json https://raw.githubusercontent.com/KittyCAD/kcl-samples/next/manifest.json", "fetch:samples": "echo \"Fetching latest KCL samples...\" && curl -o public/kcl-samples-manifest-fallback.json https://raw.githubusercontent.com/KittyCAD/kcl-samples/next/manifest.json",
@ -98,8 +98,8 @@
"build:wasm:windows": "yarn install:wasm-pack:cargo && yarn build:wasm:nocopy && ./scripts/copy-wasm.ps1 && yarn fmt", "build:wasm:windows": "yarn install:wasm-pack:cargo && yarn build:wasm:nocopy && ./scripts/copy-wasm.ps1 && yarn fmt",
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./rust/kcl-wasm-lib/pkg/kcl_wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./rust/kcl-wasm-lib/pkg/kcl_wasm_lib.js\" || echo \"sed for both mac and linux\"", "remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./rust/kcl-wasm-lib/pkg/kcl_wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./rust/kcl-wasm-lib/pkg/kcl_wasm_lib.js\" || echo \"sed for both mac and linux\"",
"wasm-prep": "rimraf rust/kcl-wasm-lib/pkg && mkdirp rust/kcl-wasm-lib/pkg && rimraf rust/kcl-lib/bindings", "wasm-prep": "rimraf rust/kcl-wasm-lib/pkg && mkdirp rust/kcl-wasm-lib/pkg && rimraf rust/kcl-lib/bindings",
"lint-fix": "eslint --fix --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src", "lint-fix": "eslint --fix --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src rust/kcl-language-server/client/src",
"lint": "eslint --max-warnings 0 --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src", "lint": "eslint --max-warnings 0 --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src rust/kcl-language-server/client/src",
"files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json", "files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
"files:set-notes": "./scripts/set-files-notes.sh", "files:set-notes": "./scripts/set-files-notes.sh",
"files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh", "files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh",

2
rust/.cargo/config.toml Normal file
View File

@ -0,0 +1,2 @@
[alias]
kcl-language-server-release = "run --manifest-path ./kcl-language-server-release/Cargo.toml --"

471
rust/Cargo.lock generated
View File

@ -27,6 +27,17 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.8.11" version = "0.8.11"
@ -361,6 +372,26 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "bzip2"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
dependencies = [
"bzip2-sys",
"libc",
]
[[package]]
name = "bzip2-sys"
version = "0.1.13+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
dependencies = [
"cc",
"pkg-config",
]
[[package]] [[package]]
name = "cast" name = "cast"
version = "0.3.0" version = "0.3.0"
@ -373,6 +404,8 @@ version = "1.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229"
dependencies = [ dependencies = [
"jobserver",
"libc",
"shlex", "shlex",
] ]
@ -442,6 +475,16 @@ dependencies = [
"half", "half",
] ]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.31" version = "4.5.31"
@ -523,6 +566,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.8.0" version = "0.8.0"
@ -557,6 +606,21 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crc"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.4.2" version = "1.4.2"
@ -604,6 +668,15 @@ dependencies = [
"itertools 0.10.5", "itertools 0.10.5",
] ]
[[package]]
name = "crossbeam-channel"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-deque" name = "crossbeam-deque"
version = "0.8.6" version = "0.8.6"
@ -713,6 +786,12 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010"
[[package]]
name = "deflate64"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b"
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.11" version = "0.3.11"
@ -794,6 +873,28 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [ dependencies = [
"block-buffer", "block-buffer",
"crypto-common", "crypto-common",
"subtle",
]
[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if",
"dirs-sys-next",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users",
"winapi",
] ]
[[package]] [[package]]
@ -899,9 +1000,9 @@ dependencies = [
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.35" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"miniz_oxide", "miniz_oxide",
@ -1182,6 +1283,15 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]] [[package]]
name = "home" name = "home"
version = "0.5.11" version = "0.5.11"
@ -1564,6 +1674,15 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a" checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a"
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "insta" name = "insta"
version = "1.42.1" version = "1.42.1"
@ -1643,6 +1762,15 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "jobserver"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.77" version = "0.3.77"
@ -1683,6 +1811,47 @@ dependencies = [
"syn 2.0.96", "syn 2.0.96",
] ]
[[package]]
name = "kcl-language-server"
version = "0.2.45"
dependencies = [
"anyhow",
"clap",
"dashmap 6.1.0",
"kcl-lib",
"kittycad",
"lazy_static",
"log",
"signal-hook",
"slog",
"slog-async",
"slog-json",
"slog-term",
"tokio",
"tower-lsp",
"tracing-subscriber",
]
[[package]]
name = "kcl-language-server-release"
version = "0.1.45"
dependencies = [
"anyhow",
"clap",
"flate2",
"lazy_static",
"log",
"slog",
"slog-async",
"slog-json",
"slog-term",
"time",
"tokio",
"tracing-subscriber",
"xshell",
"zip",
]
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.2.45" version = "0.2.45"
@ -1930,6 +2099,16 @@ version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.8.0",
"libc",
]
[[package]] [[package]]
name = "linked-hash-map" name = "linked-hash-map"
version = "0.5.6" version = "0.5.6"
@ -1959,10 +2138,16 @@ dependencies = [
] ]
[[package]] [[package]]
name = "log" name = "lockfree-object-pool"
version = "0.4.25" version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e"
[[package]]
name = "log"
version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@ -1989,6 +2174,16 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "lzma-rs"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
dependencies = [
"byteorder",
"crc",
]
[[package]] [[package]]
name = "measurements" name = "measurements"
version = "0.11.0" version = "0.11.0"
@ -2068,9 +2263,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.3" version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
dependencies = [ dependencies = [
"adler2", "adler2",
"simd-adler32", "simd-adler32",
@ -2118,6 +2313,16 @@ dependencies = [
"minimal-lexical", "minimal-lexical",
] ]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]] [[package]]
name = "num-bigint" name = "num-bigint"
version = "0.4.6" version = "0.4.6"
@ -2209,6 +2414,12 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]] [[package]]
name = "owo-colors" name = "owo-colors"
version = "4.1.0" version = "4.1.0"
@ -2299,6 +2510,16 @@ dependencies = [
"syn 2.0.96", "syn 2.0.96",
] ]
[[package]]
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest",
"hmac",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@ -2408,6 +2629,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]] [[package]]
name = "plotters" name = "plotters"
version = "0.3.7" version = "0.3.7"
@ -2747,6 +2974,17 @@ dependencies = [
"bitflags 2.8.0", "bitflags 2.8.0",
] ]
[[package]]
name = "redox_users"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
"getrandom 0.2.15",
"libredox",
"thiserror 1.0.69",
]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.11.1" version = "1.11.1"
@ -3197,12 +3435,31 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.2" version = "1.4.2"
@ -3233,6 +3490,49 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "slog"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06"
[[package]]
name = "slog-async"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72c8038f898a2c79507940990f05386455b3a317d8f18d4caea7cbc3d5096b84"
dependencies = [
"crossbeam-channel",
"slog",
"take_mut",
"thread_local",
]
[[package]]
name = "slog-json"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e1e53f61af1e3c8b852eef0a9dee29008f55d6dd63794f3f12cef786cf0f219"
dependencies = [
"serde",
"serde_json",
"slog",
"time",
]
[[package]]
name = "slog-term"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6e022d0b998abfe5c3782c1f03551a596269450ccd677ea51c56f8b214610e8"
dependencies = [
"is-terminal",
"slog",
"term",
"thread_local",
"time",
]
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.13.2" version = "1.13.2"
@ -3410,6 +3710,12 @@ dependencies = [
"syn 2.0.96", "syn 2.0.96",
] ]
[[package]]
name = "take_mut"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60"
[[package]] [[package]]
name = "tap" name = "tap"
version = "1.0.1" version = "1.0.1"
@ -3436,6 +3742,17 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "term"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f"
dependencies = [
"dirs-next",
"rustversion",
"winapi",
]
[[package]] [[package]]
name = "termcolor" name = "termcolor"
version = "1.4.1" version = "1.4.1"
@ -3511,6 +3828,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820"
[[package]]
name = "thread_local"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.37" version = "0.3.37"
@ -3798,6 +4125,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-serde"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
dependencies = [
"serde",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"nu-ansi-term",
"serde",
"serde_json",
"sharded-slab",
"smallvec",
"thread_local",
"tracing-core",
"tracing-log",
"tracing-serde",
] ]
[[package]] [[package]]
@ -4004,6 +4370,12 @@ dependencies = [
"syn 2.0.96", "syn 2.0.96",
] ]
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.5" version = "0.9.5"
@ -4349,6 +4721,21 @@ dependencies = [
"tap", "tap",
] ]
[[package]]
name = "xshell"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e7290c623014758632efe00737145b6867b66292c42167f2ec381eb566a373d"
dependencies = [
"xshell-macros",
]
[[package]]
name = "xshell-macros"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32ac00cd3f8ec9c1d33fb3e7958a82df6989c42d747bd326c822b1d625283547"
[[package]] [[package]]
name = "yaml-rust" name = "yaml-rust"
version = "0.4.5" version = "0.4.5"
@ -4435,6 +4822,20 @@ name = "zeroize"
version = "1.8.1" version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
]
[[package]] [[package]]
name = "zerovec" name = "zerovec"
@ -4464,11 +4865,67 @@ version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45" checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45"
dependencies = [ dependencies = [
"aes",
"arbitrary", "arbitrary",
"bzip2",
"constant_time_eq",
"crc32fast", "crc32fast",
"crossbeam-utils", "crossbeam-utils",
"deflate64",
"displaydoc", "displaydoc",
"flate2",
"hmac",
"indexmap 2.7.1", "indexmap 2.7.1",
"lzma-rs",
"memchr", "memchr",
"pbkdf2",
"rand 0.8.5",
"sha1",
"thiserror 2.0.11", "thiserror 2.0.11",
"time",
"zeroize",
"zopfli",
"zstd",
]
[[package]]
name = "zopfli"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946"
dependencies = [
"bumpalo",
"crc32fast",
"lockfree-object-pool",
"log",
"once_cell",
"simd-adler32",
]
[[package]]
name = "zstd"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "7.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3051792fbdc2e1e143244dc28c60f73d8470e93f3f9cbd0ead44da5ed802722"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.14+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fb060d4926e4ac3a3ad15d864e99ceb5f343c6b34f5bd6d81ae6ed417311be5"
dependencies = [
"cc",
"pkg-config",
] ]

View File

@ -3,6 +3,8 @@ resolver = "2"
members = [ members = [
"kcl-bumper", "kcl-bumper",
"kcl-derive-docs", "kcl-derive-docs",
"kcl-language-server",
"kcl-language-server-release",
"kcl-lib", "kcl-lib",
"kcl-python-bindings", "kcl-python-bindings",
"kcl-test-server", "kcl-test-server",
@ -27,17 +29,26 @@ debug = "line-tables-only"
[workspace.dependencies] [workspace.dependencies]
async-trait = "0.1.85" async-trait = "0.1.85"
anyhow = { version = "1" } anyhow = { version = "1" }
clap = { version = "4.5.31", features = ["derive"] }
dashmap = { version = "6.1.0" }
http = "1" http = "1"
indexmap = "2.7.0" indexmap = "2.7.0"
kittycad = { version = "0.3.28", default-features = false, features = ["js", "requests"] } kittycad = { version = "0.3.28", default-features = false, features = ["js", "requests"] }
kittycad-modeling-cmds = { version = "0.2.100", features = ["ts-rs", "websocket"] } kittycad-modeling-cmds = { version = "0.2.100", features = ["ts-rs", "websocket"] }
lazy_static = "1.5.0"
miette = "7.5.0" miette = "7.5.0"
pyo3 = { version = "0.22.6" } pyo3 = { version = "0.22.6" }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = { version = "1" } serde_json = { version = "1" }
slog = "2.7.0"
slog-async = "2.8.0"
slog-json = "2.6.1"
slog-term = "2.9.1"
tokio = { version = "1" } tokio = { version = "1" }
tower-lsp = { version = "0.20.0", default-features = false } tower-lsp = { version = "0.20.0", default-features = false }
tracing-subscriber = { version = "0.3.19", features = ["registry", "std", "fmt", "smallvec", "ansi", "tracing-log", "json"] }
uuid = { version = "1", features = ["v4", "serde"] } uuid = { version = "1", features = ["v4", "serde"] }
zip = { version = "2.2.2", default-features = false }
[workspace.lints.clippy] [workspace.lints.clippy]
assertions_on_result_states = "warn" assertions_on_result_states = "warn"

View File

@ -20,4 +20,5 @@
- This will publish the relevant crates and push a new tag with the prefix - This will publish the relevant crates and push a new tag with the prefix
`kcl-`. DO NOT SET THE PREFIX TO `kcl-` when you run the command. The `just` `kcl-`. DO NOT SET THE PREFIX TO `kcl-` when you run the command. The `just`
command will do that for you. command will do that for you.
- The tag will then trigger the release of kcl-python-bindings. - The tag will then trigger the release of `kcl-python-bindings` and
`kcl-language-server`.

View File

@ -11,7 +11,7 @@ publish = false
[dependencies] [dependencies]
anyhow = { workspace = true } anyhow = { workspace = true }
clap = { version = "4.5.31", features = ["derive"] } clap = { workspace = true, features = ["derive"] }
semver = "1.0.25" semver = "1.0.25"
serde = { workspace = true } serde = { workspace = true }
toml_edit = "0.22.16" toml_edit = "0.22.16"

View File

@ -0,0 +1,26 @@
[package]
name = "kcl-language-server-release"
version = "0.1.45"
edition = "2021"
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = { workspace = true }
clap = { workspace = true, features = ["cargo", "derive", "env", "unicode"] }
flate2 = "1.1.0"
lazy_static = { workspace = true }
log = { version = "0.4.26", features = ["serde"] }
slog = { workspace = true }
slog-async = { workspace = true }
slog-json = { workspace = true }
slog-term = { workspace = true }
time = "0.3.37"
tokio = { workspace = true, features = ["full"] }
tracing-subscriber = { workspace = true }
xshell = "0.2.6"
zip = { workspace = true, features = ["default"] }
[lints]
workspace = true

View File

@ -0,0 +1,247 @@
use std::{
env,
fs::File,
io::{self, BufWriter},
path::{Path, PathBuf},
};
use anyhow::Result;
use clap::Parser;
use flate2::{write::GzEncoder, Compression};
use time::OffsetDateTime;
use xshell::{cmd, Shell};
use zip::ZipWriter;
/// A subcommand for building and packaging a release.
#[derive(Parser, Clone, Debug)]
pub struct Build {
/// An optional client patch version to use.
#[clap(long = "client-patch-version", default_value = "None")]
pub client_patch_version: Option<String>,
}
impl Build {
pub(crate) fn run(&self, sh: &Shell) -> Result<()> {
let stable = sh
.var("GITHUB_REF")
.unwrap_or_default()
.as_str()
.contains("refs/tags/v");
let project_root = crate::project_root();
let target = Target::get(&project_root);
let build = project_root.join("build");
sh.remove_path(&build)?;
sh.create_dir(&build)?;
// Read the version from our root Cargo.toml.
let version = sh.read_file("kcl-language-server/Cargo.toml")?;
let mut version = version
.lines()
.find(|line| line.starts_with("version = "))
.unwrap_or_default()
.replace("version = ", "")
.replace(['\"', '\''], "")
.trim()
.to_string();
if !stable {
version = format!("{}-nightly", version);
}
let release_tag = if stable {
// We already checked above if the env var contains "refs/tags/v".
// So this is safe to unwrap.
sh.var("GITHUB_REF")
.unwrap_or_default()
.replace("refs/tags/", "")
.to_string()
} else {
"nightly".to_string()
};
if stable && !release_tag.contains(&version) {
// bail early if the tag doesn't match the version
anyhow::bail!(
"Tag {} doesn't match version {}. Did you forget to update Cargo.toml?",
release_tag,
version
);
}
build_server(sh, &version, &target)?;
build_client(sh, &version, &release_tag, &target)?;
Ok(())
}
}
fn build_client(sh: &Shell, version: &str, release_tag: &str, target: &Target) -> anyhow::Result<()> {
let bundle_path = Path::new("server");
sh.create_dir(bundle_path)?;
sh.copy_file(&target.server_path, bundle_path)?;
if let Some(symbols_path) = &target.symbols_path {
sh.copy_file(symbols_path, bundle_path)?;
}
let mut patch = Patch::new(sh, "./kcl-language-server/package.json")?;
patch
.replace(r#""version": "0.0.0""#, &format!(r#""version": "{version}""#))
.replace(r#""releaseTag": null"#, &format!(r#""releaseTag": "{release_tag}""#))
.replace(r#""enabledApiProposals": [],"#, r#""#);
patch.commit(sh)?;
Ok(())
}
fn build_server(sh: &Shell, release: &str, target: &Target) -> anyhow::Result<()> {
let _e = sh.push_env("CFG_RELEASE", release);
let _e = sh.push_env("CARGO_PROFILE_RELEASE_LTO", "thin");
// Uncomment to enable debug info for releases. Note that:
// * debug info is split on windows and macs, so it does nothing for those platforms,
// * on Linux, this blows up the binary size from 8MB to 43MB, which is unreasonable.
// let _e = sh.push_env("CARGO_PROFILE_RELEASE_DEBUG", "1");
if target.name.contains("-linux-") {
env::set_var("CC", "clang");
}
let target_name = &target.name;
cmd!(
sh,
"cargo build -p kcl-language-server --target {target_name} --release"
)
.run()?;
let dst = Path::new("build").join(&target.artifact_name);
gzip(&target.server_path, &dst.with_extension("gz"))?;
if target_name.contains("-windows-") {
zip(
&target.server_path,
target.symbols_path.as_ref(),
&dst.with_extension("zip"),
)?;
}
Ok(())
}
fn gzip(src_path: &Path, dest_path: &Path) -> anyhow::Result<()> {
let mut encoder = GzEncoder::new(File::create(dest_path)?, Compression::best());
let mut input = io::BufReader::new(File::open(src_path)?);
io::copy(&mut input, &mut encoder)?;
encoder.finish()?;
Ok(())
}
fn zip(src_path: &Path, symbols_path: Option<&PathBuf>, dest_path: &Path) -> anyhow::Result<()> {
let file = File::create(dest_path)?;
let mut writer = ZipWriter::new(BufWriter::new(file));
let file_options = zip::write::SimpleFileOptions::default()
.last_modified_time(convert_date_time(OffsetDateTime::from(
std::fs::metadata(src_path)?.modified()?,
))?)
.unix_permissions(0o755)
.compression_method(zip::CompressionMethod::Deflated)
.compression_level(Some(9));
writer.start_file(src_path.file_name().unwrap().to_str().unwrap(), file_options)?;
let mut input = io::BufReader::new(File::open(src_path)?);
io::copy(&mut input, &mut writer)?;
if let Some(symbols_path) = symbols_path {
writer.start_file(symbols_path.file_name().unwrap().to_str().unwrap(), file_options)?;
let mut input = io::BufReader::new(File::open(symbols_path)?);
io::copy(&mut input, &mut writer)?;
}
writer.finish()?;
Ok(())
}
struct Target {
name: String,
server_path: PathBuf,
symbols_path: Option<PathBuf>,
artifact_name: String,
}
impl Target {
fn get(project_root: &Path) -> Self {
let name = match env::var("RA_TARGET") {
Ok(target) => target,
_ => {
if cfg!(target_os = "linux") {
"x86_64-unknown-linux-gnu".to_string()
} else if cfg!(target_os = "windows") {
"x86_64-pc-windows-msvc".to_string()
} else if cfg!(target_os = "macos") {
"aarch64-apple-darwin".to_string()
} else {
panic!("Unsupported OS, maybe try setting RA_TARGET")
}
}
};
let out_path = project_root.join("target").join(&name).join("release");
let (exe_suffix, symbols_path) = if name.contains("-windows-") {
(".exe".into(), Some(out_path.join("kcl_language_server.pdb")))
} else {
(String::new(), None)
};
let server_path = out_path.join(format!("kcl-language-server{exe_suffix}"));
let artifact_name = format!("kcl-language-server-{name}{exe_suffix}");
Self {
name,
server_path,
symbols_path,
artifact_name,
}
}
}
struct Patch {
path: PathBuf,
original_contents: String,
contents: String,
}
impl Patch {
fn new(sh: &Shell, path: impl Into<PathBuf>) -> anyhow::Result<Patch> {
let path = path.into();
let contents = sh.read_file(&path)?;
Ok(Patch {
path,
original_contents: contents.clone(),
contents,
})
}
fn replace(&mut self, from: &str, to: &str) -> &mut Patch {
assert!(self.contents.contains(from));
self.contents = self.contents.replace(from, to);
self
}
fn commit(&self, sh: &Shell) -> anyhow::Result<()> {
sh.write_file(&self.path, &self.contents)?;
Ok(())
}
}
impl Drop for Patch {
fn drop(&mut self) {
// FIXME: find a way to bring this back
let _ = &self.original_contents;
// write_file(&self.path, &self.original_contents).unwrap();
}
}
fn convert_date_time(offset_dt: OffsetDateTime) -> anyhow::Result<zip::DateTime> {
// Convert to MS-DOS date time format that the zip crate expects
zip::DateTime::from_date_and_time(
offset_dt.year() as u16,
offset_dt.month() as u8,
offset_dt.day(),
offset_dt.hour(),
offset_dt.minute(),
offset_dt.second(),
)
.map_err(|err| anyhow::anyhow!("Failed to convert date time to MS-DOS format: {}", err))
}

View File

@ -0,0 +1,157 @@
//! A release building tool and packager.
#![deny(missing_docs)]
/// A subcommand for building and packaging a release.
pub mod build;
use std::{
env,
path::{Path, PathBuf},
};
use anyhow::{bail, Result};
use clap::Parser;
use slog::Drain;
use tracing_subscriber::{prelude::*, Layer};
use xshell::Shell;
lazy_static::lazy_static! {
/// Initialize the logger.
// We need a slog::Logger for steno and when we export out the logs from re-exec-ed processes.
pub static ref LOGGER: slog::Logger = {
let decorator = slog_term::TermDecorator::new().build();
let drain = slog_term::FullFormat::new(decorator).build().fuse();
let drain = slog_async::Async::new(drain).build().fuse();
slog::Logger::root(drain, slog::slog_o!())
};
}
/// This doc string acts as a help message when the user runs '--help'
/// as do all doc strings on fields.
#[derive(Parser, Debug, Clone)]
#[clap(version = clap::crate_version!(), author = clap::crate_authors!("\n"))]
pub struct Opts {
/// Print debug info
#[clap(short, long)]
pub debug: bool,
/// Print logs as json
#[clap(short, long)]
pub json: bool,
/// The subcommand to run.
#[clap(subcommand)]
pub subcmd: SubCommand,
}
impl Opts {
/// Setup our logger.
pub fn create_logger(&self) -> slog::Logger {
if self.json {
let drain = slog_json::Json::default(std::io::stderr()).fuse();
self.async_root_logger(drain)
} else {
let decorator = slog_term::TermDecorator::new().build();
let drain = slog_term::FullFormat::new(decorator).build().fuse();
self.async_root_logger(drain)
}
}
fn async_root_logger<T>(&self, drain: T) -> slog::Logger
where
T: slog::Drain + Send + 'static,
<T as slog::Drain>::Err: std::fmt::Debug,
{
let level = if self.debug {
slog::Level::Debug
} else {
slog::Level::Info
};
let level_drain = slog::LevelFilter(drain, level).fuse();
let async_drain = slog_async::Async::new(level_drain).build().fuse();
slog::Logger::root(async_drain, slog::o!())
}
}
/// A subcommand for our cli.
#[derive(Parser, Debug, Clone)]
pub enum SubCommand {
/// Build release packages.
Build(crate::build::Build),
}
#[tokio::main]
async fn main() -> Result<()> {
let opts: Opts = Opts::parse();
let level_filter = if opts.debug {
tracing_subscriber::filter::LevelFilter::DEBUG
} else {
tracing_subscriber::filter::LevelFilter::INFO
};
// Format fields using the provided closure.
// We want to make this very consise otherwise the logs are not able to be read by humans.
let format = tracing_subscriber::fmt::format::debug_fn(|writer, field, value| {
if format!("{}", field) == "message" {
write!(writer, "{}: {:?}", field, value)
} else {
write!(writer, "{}", field)
}
})
// Separate each field with a comma.
// This method is provided by an extension trait in the
// `tracing-subscriber` prelude.
.delimited(", ");
let (json, plain) = if opts.json {
// Cloud run likes json formatted logs if possible.
// See: https://cloud.google.com/run/docs/logging
// We could probably format these specifically for cloud run if we wanted,
// will save that as a TODO: https://cloud.google.com/run/docs/logging#special-fields
(
Some(tracing_subscriber::fmt::layer().json().with_filter(level_filter)),
None,
)
} else {
(
None,
Some(
tracing_subscriber::fmt::layer()
.pretty()
.fmt_fields(format)
.with_filter(level_filter),
),
)
};
// Initialize the tracing.
tracing_subscriber::registry().with(json).with(plain).init();
if let Err(err) = run_cmd(&opts).await {
bail!("running cmd `{:?}` failed: {:?}", &opts.subcmd, err);
}
Ok(())
}
async fn run_cmd(opts: &Opts) -> Result<()> {
let sh = &Shell::new()?;
sh.change_dir(project_root());
match &opts.subcmd {
SubCommand::Build(b) => b.run(sh)?,
}
Ok(())
}
fn project_root() -> PathBuf {
Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| env!("CARGO_MANIFEST_DIR").to_owned()))
.ancestors()
.nth(1)
.unwrap()
.to_path_buf()
}

View File

@ -0,0 +1,53 @@
[package]
name = "kcl-language-server"
description = "A language server for KCL."
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
version = "0.2.45"
edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name = "kcl-language-server"
path = "src/main.rs"
[dependencies]
anyhow = { workspace = true }
clap = { workspace = true, features = ["cargo", "derive", "env", "unicode"] }
dashmap = { workspace = true }
kcl-lib = { path = "../kcl-lib", default-features = false, features = [
"cli",
"engine",
"disable-println",
] }
kittycad = { workspace = true }
lazy_static = { workspace = true }
log = { version = "0.4.26", features = ["serde"] }
slog = { workspace = true }
slog-async = { workspace = true }
slog-json = { workspace = true }
slog-term = { workspace = true }
tracing-subscriber = { workspace = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
signal-hook = "0.3.17"
tokio = { version = "1.43.0", features = ["full"] }
tower-lsp = { version = "0.20.0", features = ["proposed"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
tower-lsp = { version = "0.20.0", default-features = false, features = [
"runtime-agnostic",
] }
[lints]
workspace = true
[profile.dev]
# Disabling debug info speeds up builds a bunch,
# and we don't rely on it for debugging that much.
debug = 0
[profile.release]
incremental = true
# Set this to 1 or 2 to get more useful backtraces in debugger.
debug = 0

View File

@ -0,0 +1,101 @@
# kcl-lsp
The `kcl` [Language Server Protocol](https://microsoft.github.io/language-server-protocol)
implementation and VSCode extension.
This language server is a thin wrapper around the KCL language tooling library.
That is found in the [modeling-app](https://github.com/kittycad/modeling-app) repo, and published as
on crates.io as [kcl-lib](https://crates.io/crates/kcl-lib).
## VSCode
Install our extension: [KittyCAD Language Server](https://marketplace.visualstudio.com/items?itemName=KittyCAD.kcl-language-server)
## Neovim
You can add the following to your `vim` configuration if you are using `lspconfig`.
This is [@jessfraz's
setup](https://github.com/jessfraz/.vim/blob/master/vimrc#L935).
```vim
if executable('kcl-language-server')
lua << EOF
local lspconfig = require 'lspconfig'
local configs = require 'lspconfig.configs'
if not configs.kcl_lsp then
configs.kcl_lsp = {
default_config = {
cmd = {'kcl-language-server', 'server', '--stdio'},
filetypes = {'kcl'},
root_dir = lspconfig.util.root_pattern('.git'),
single_file_support = true,
},
docs = {
description = [=[
https://github.com/KittyCAD/kcl-lsp
https://kittycad.io
The KittyCAD Language Server Protocol implementation for the KCL language.
To better detect kcl files, the following can be added:
vim.cmd [[ autocmd BufRead,BufNewFile *.kcl set filetype=kcl ]]
]=],
default_config = {
root_dir = [[root_pattern(".git")]],
},
}
}
end
lspconfig.kcl_lsp.setup{}
EOF
else
echo "You might want to install kcl-language-server: https://github.com/KittyCAD/kcl-lsp/releases"
end
```
## Helix
Add this to your `languages.toml` file. Remember to change `/Users/adamchalmers` to your path.
Note that we don't currently have Treesitter parsers, so there won't be syntax highlighting.
```toml
[[language]]
name = "kcl"
scope = "source.kcl"
injection-regex = "kcl"
file-types = ["kcl"]
comment-tokens = "//"
indent = { tab-width = 2, unit = " " }
language-servers = [ "kcl-lsp" ]
block-comment-tokens = { start = "/*", end = "*/"}
[language-server.kcl-lsp]
command = "/Users/adamchalmers/kc-repos/kcl-lsp/target/release/kcl-language-server"
args = ["server", "--stdio"]
```
## Development
```bash
$ yarn install
$ cargo build
$ code .
```
Once VSCode opens, go to the "Run and Debug" panel (cmd-shift-D on MacOS), and choose Run Extension (Debug Build).
This opens a new VSCode window with our KCL extension installed. Open a KCL file and check that the LSP is working.
- press <kbd>F5</kbd> or change to the Debug panel and click <kbd>Launch Client</kbd>
> **Note**
>
> If encountered errors like `Cannot find module '/xxx/xxx/dist/extension.js'`
> please try run command `tsc -b` manually

View File

@ -0,0 +1,162 @@
/* eslint suggest-no-throw/suggest-no-throw: 0 */
import * as vscode from 'vscode'
import * as os from 'os'
import type { Config } from './config'
import { log, isValidExecutable } from './util'
import type { PersistentState } from './persistent_state'
import { exec } from 'child_process'
export async function bootstrap(
context: vscode.ExtensionContext,
config: Config,
state: PersistentState
): Promise<string> {
const path = await getServer(context, config, state)
if (!path) {
throw new Error(
'KittyCAD Language Server is not available. ' +
'Please, ensure its [proper installation](https://github.com/kittycad/kcl-lsp).'
)
}
log.info('Using server binary at', path)
if (!isValidExecutable(path)) {
if (config.serverPath) {
throw new Error(`Failed to execute ${path} --version. \`config.server.path\` or \`config.serverPath\` has been set explicitly.\
Consider removing this config or making a valid server binary available at that path.`)
} else {
throw new Error(`Failed to execute ${path} --version`)
}
}
return path
}
async function getServer(
context: vscode.ExtensionContext,
config: Config,
state: PersistentState
): Promise<string | undefined> {
const explicitPath =
process.env['__KCL_LSP_SERVER_DEBUG'] ?? config.serverPath
if (explicitPath) {
if (explicitPath.startsWith('~/')) {
return os.homedir() + explicitPath.slice('~'.length)
}
return explicitPath
}
if (config.package.releaseTag === null) return 'kcl-language-server'
const ext = process.platform === 'win32' ? '.exe' : ''
const bundled = vscode.Uri.joinPath(
context.extensionUri,
'server',
`kcl-language-server${ext}`
)
log.info('Checking if bundled server exists at', bundled)
const bundledExists = await vscode.workspace.fs.stat(bundled).then(
() => true,
() => false
)
log.info('Bundled server exists:', bundledExists)
if (bundledExists) {
let server = bundled
if (await isNixOs()) {
await vscode.workspace.fs.createDirectory(config.globalStorageUri).then()
const dest = vscode.Uri.joinPath(
config.globalStorageUri,
`kcl-language-server${ext}`
)
let exists = await vscode.workspace.fs.stat(dest).then(
() => true,
() => false
)
if (exists && config.package.version !== state.serverVersion) {
log.info(
'Server version changed, removing old server binary',
config.package.version,
state.serverVersion
)
await vscode.workspace.fs.delete(dest)
exists = false
}
if (!exists) {
await vscode.workspace.fs.copy(bundled, dest)
await patchelf(dest)
}
server = dest
}
await state.updateServerVersion(config.package.version)
return server.fsPath
}
await state.updateServerVersion(undefined)
await vscode.window.showErrorMessage(
"Unfortunately we don't ship binaries for your platform yet. " +
'You need to manually clone the kcl-lsp repository and ' +
'run `cargo install` to build the language server from sources. ' +
'If you feel that your platform should be supported, please create an issue ' +
'about that [here](https://github.com/kittycad/kcl-lsp/issues) and we ' +
'will consider it.'
)
return undefined
}
async function isNixOs(): Promise<boolean> {
try {
const contents = (
await vscode.workspace.fs.readFile(vscode.Uri.file('/etc/os-release'))
).toString()
const idString =
contents.split('\n').find((a) => a.startsWith('ID=')) || 'ID=linux'
return idString.indexOf('nixos') !== -1
} catch {
return false
}
}
async function patchelf(dest: vscode.Uri): Promise<void> {
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: 'Patching kcl-language-server for NixOS',
},
async (progress, _) => {
const expression = `
{srcStr, pkgs ? import <nixpkgs> {}}:
pkgs.stdenv.mkDerivation {
name = "kcl-language-server";
src = /. + srcStr;
phases = [ "installPhase" "fixupPhase" ];
installPhase = "cp $src $out";
fixupPhase = ''
chmod 755 $out
patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" $out
'';
}
`
const origFile = vscode.Uri.file(dest.fsPath + '-orig')
await vscode.workspace.fs.rename(dest, origFile, { overwrite: true })
try {
progress.report({ message: 'Patching executable', increment: 20 })
await new Promise((resolve, reject) => {
const handle = exec(
`nix-build -E - --argstr srcStr '${origFile.fsPath}' -o '${dest.fsPath}'`,
(err, stdout, stderr) => {
if (err != null) {
reject(Error(stderr))
} else {
resolve(stdout)
}
}
)
handle.stdin?.write(expression)
handle.stdin?.end()
})
} finally {
await vscode.workspace.fs.delete(origFile)
}
}
)
}

View File

@ -0,0 +1,74 @@
/* eslint suggest-no-throw/suggest-no-throw: 0 */
import * as lc from 'vscode-languageclient/node'
import type * as vscode from 'vscode'
export async function createClient(
traceOutputChannel: vscode.OutputChannel,
outputChannel: vscode.OutputChannel,
initializationOptions: vscode.WorkspaceConfiguration,
serverOptions: lc.ServerOptions
): Promise<lc.LanguageClient> {
const clientOptions: lc.LanguageClientOptions = {
documentSelector: [{ scheme: 'file', language: 'kcl' }],
initializationOptions,
traceOutputChannel,
outputChannel,
middleware: {
workspace: {
// HACK: This is a workaround, when the client has been disposed, VSCode
// continues to emit events to the client and the default one for this event
// attempt to restart the client for no reason
async didChangeWatchedFile(event: any, next: any) {
if (client.isRunning()) {
await next(event)
}
},
async configuration(
params: lc.ConfigurationParams,
token: vscode.CancellationToken,
next: lc.ConfigurationRequest.HandlerSignature
) {
const resp = await next(params, token)
return resp
},
},
},
}
const client = new lc.LanguageClient(
'kcl-language-server',
'KittyCAD Language Server',
serverOptions,
clientOptions
)
client.registerFeature(new ExperimentalFeatures())
return client
}
class ExperimentalFeatures implements lc.StaticFeature {
getState(): lc.FeatureState {
return { kind: 'static' }
}
fillClientCapabilities(capabilities: lc.ClientCapabilities): void {
capabilities.experimental = {
snippetTextEdit: true,
codeActionGroup: true,
hoverActions: true,
serverStatusNotification: true,
colorDiagnosticOutput: true,
openServerLogs: true,
commands: {
commands: ['editor.action.triggerParameterHints'],
},
...capabilities.experimental,
}
}
initialize(
_capabilities: lc.ServerCapabilities,
_documentSelector: lc.DocumentSelector | undefined
): void {}
dispose(): void {}
clear(): void {}
}

View File

@ -0,0 +1,32 @@
/* eslint suggest-no-throw/suggest-no-throw: 0 */
import * as vscode from 'vscode'
import type { Cmd, CtxInit } from './ctx'
import { spawnSync } from 'child_process'
export function serverVersion(ctx: CtxInit): Cmd {
return async () => {
if (!ctx.serverPath) {
void vscode.window.showWarningMessage(
`kcl-language-server server is not running`
)
return
}
const { stdout } = spawnSync(ctx.serverPath, ['--version'], {
encoding: 'utf8',
})
const versionString = stdout.slice(`kcl-language-server `.length).trim()
void vscode.window.showInformationMessage(
`kcl-language-server version: ${versionString}`
)
}
}
export function openLogs(ctx: CtxInit): Cmd {
return async () => {
if (ctx.client.outputChannel) {
ctx.client.outputChannel.show()
}
}
}

View File

@ -0,0 +1,293 @@
/* eslint suggest-no-throw/suggest-no-throw: 0 */
import * as Is from 'vscode-languageclient/lib/common/utils/is'
import * as os from 'os'
import * as path from 'path'
import * as vscode from 'vscode'
import { log, type Env } from './util'
import { expectNotUndefined, unwrapUndefinable } from './undefinable'
export type RunnableEnvCfgItem = {
mask?: string
env: Record<string, string>
platform?: string | string[]
}
export type RunnableEnvCfg =
| undefined
| Record<string, string>
| RunnableEnvCfgItem[]
export class Config {
readonly extensionId = 'kittycad.kcl-language-server'
configureLang: vscode.Disposable | undefined
readonly rootSection = 'kcl-language-server'
private readonly requiresReloadOpts = ['serverPath', 'server', 'files'].map(
(opt) => `${this.rootSection}.${opt}`
)
readonly package: {
version: string
releaseTag: string | null
enableProposedApi: boolean | undefined
} = vscode.extensions.getExtension(this.extensionId)!.packageJSON
readonly globalStorageUri: vscode.Uri
constructor(ctx: vscode.ExtensionContext) {
this.globalStorageUri = ctx.globalStorageUri
vscode.workspace.onDidChangeConfiguration(
this.onDidChangeConfiguration,
this,
ctx.subscriptions
)
this.refreshLogging()
}
dispose() {
this.configureLang?.dispose()
}
private refreshLogging() {
log.setEnabled(this.traceExtension ?? false)
log.info('Extension version:', this.package.version)
const cfg = Object.entries(this.cfg).filter(
([_, val]) => !(val instanceof Function)
)
log.info('Using configuration', Object.fromEntries(cfg))
}
private async onDidChangeConfiguration(
event: vscode.ConfigurationChangeEvent
) {
this.refreshLogging()
const requiresReloadOpt = this.requiresReloadOpts.find((opt) =>
event.affectsConfiguration(opt)
)
if (!requiresReloadOpt) return
const message = `Changing "${requiresReloadOpt}" requires a server restart`
const userResponse = await vscode.window.showInformationMessage(
message,
'Restart now'
)
if (userResponse) {
const command = 'kcl-language-server.restartServer'
await vscode.commands.executeCommand(command)
}
}
// We don't do runtime config validation here for simplicity. More on stackoverflow:
// https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension
private get cfg(): vscode.WorkspaceConfiguration {
return vscode.workspace.getConfiguration(this.rootSection)
}
/**
* Beware that postfix `!` operator erases both `null` and `undefined`.
* This is why the following doesn't work as expected:
*
* ```ts
* const nullableNum = vscode
* .workspace
* .getConfiguration
* .getConfiguration("kcl-language-server")
* .get<number | null>(path)!;
*
* // What happens is that type of `nullableNum` is `number` but not `null | number`:
* const fullFledgedNum: number = nullableNum;
* ```
* So this getter handles this quirk by not requiring the caller to use postfix `!`
*/
private get<T>(path: string): T | undefined {
return prepareVSCodeConfig(this.cfg.get<T>(path))
}
get serverPath() {
return (
this.get<null | string>('server.path') ??
this.get<null | string>('serverPath')
)
}
get traceExtension() {
return this.get<boolean>('trace.extension')
}
}
// the optional `cb?` parameter is meant to be used to add additional
// key/value pairs to the VS Code configuration. This needed for, e.g.,
// including a `rust-project.json` into the `linkedProjects` key as part
// of the configuration/InitializationParams _without_ causing VS Code
// configuration to be written out to workspace-level settings. This is
// undesirable behavior because rust-project.json files can be tens of
// thousands of lines of JSON, most of which is not meant for humans
// to interact with.
export function prepareVSCodeConfig<T>(
resp: T,
cb?: (key: Extract<keyof T, string>, res: { [key: string]: any }) => void
): T {
if (Is.string(resp)) {
return substituteVSCodeVariableInString(resp) as T
} else if (resp && Is.array<any>(resp)) {
return resp.map((val) => {
return prepareVSCodeConfig(val)
}) as T
} else if (resp && typeof resp === 'object') {
const res: { [key: string]: any } = {}
for (const key in resp) {
const val = resp[key]
res[key] = prepareVSCodeConfig(val)
if (cb) {
cb(key, res)
}
}
return res as T
}
return resp
}
// FIXME: Merge this with `substituteVSCodeVariables` above
export function substituteVariablesInEnv(env: Env): Env {
const missingDeps = new Set<string>()
// vscode uses `env:ENV_NAME` for env vars resolution, and it's easier
// to follow the same convention for our dependency tracking
const definedEnvKeys = new Set(Object.keys(env).map((key) => `env:${key}`))
const envWithDeps = Object.fromEntries(
Object.entries(env).map(([key, value]) => {
const deps = new Set<string>()
const depRe = new RegExp(/\${(?<depName>.+?)}/g)
let match = undefined
while ((match = depRe.exec(value))) {
const depName = unwrapUndefinable(match.groups?.['depName'])
deps.add(depName)
// `depName` at this point can have a form of `expression` or
// `prefix:expression`
if (!definedEnvKeys.has(depName)) {
missingDeps.add(depName)
}
}
return [`env:${key}`, { deps: [...deps], value }]
})
)
const resolved = new Set<string>()
for (const dep of missingDeps) {
const match = /(?<prefix>.*?):(?<body>.+)/.exec(dep)
if (match) {
const { prefix, body } = match.groups!
if (prefix === 'env') {
const envName = unwrapUndefinable(body)
envWithDeps[dep] = {
value: process.env[envName] ?? '',
deps: [],
}
resolved.add(dep)
} else {
// we can't handle other prefixes at the moment
// leave values as is, but still mark them as resolved
envWithDeps[dep] = {
value: '${' + dep + '}',
deps: [],
}
resolved.add(dep)
}
} else {
envWithDeps[dep] = {
value: computeVscodeVar(dep) || '${' + dep + '}',
deps: [],
}
}
}
const toResolve = new Set(Object.keys(envWithDeps))
let leftToResolveSize
do {
leftToResolveSize = toResolve.size
for (const key of toResolve) {
const item = unwrapUndefinable(envWithDeps[key])
if (item.deps.every((dep) => resolved.has(dep))) {
item.value = item.value.replace(
/\${(?<depName>.+?)}/g,
(_wholeMatch, depName) => {
const item = unwrapUndefinable(envWithDeps[depName])
return item.value
}
)
resolved.add(key)
toResolve.delete(key)
}
}
} while (toResolve.size > 0 && toResolve.size < leftToResolveSize)
const resolvedEnv: Env = {}
for (const key of Object.keys(env)) {
const item = unwrapUndefinable(envWithDeps[`env:${key}`])
resolvedEnv[key] = item.value
}
return resolvedEnv
}
const VarRegex = new RegExp(/\$\{(.+?)\}/g)
function substituteVSCodeVariableInString(val: string): string {
return val.replace(VarRegex, (substring: string, varName) => {
if (Is.string(varName)) {
return computeVscodeVar(varName) || substring
} else {
return substring
}
})
}
function computeVscodeVar(varName: string): string | null {
const workspaceFolder = () => {
const folders = vscode.workspace.workspaceFolders ?? []
const folder = folders[0]
// TODO: support for remote workspaces?
const fsPath: string =
folder === undefined
? // no workspace opened
''
: // could use currently opened document to detect the correct
// workspace. However, that would be determined by the document
// user has opened on Editor startup. Could lead to
// unpredictable workspace selection in practice.
// It's better to pick the first one
folder.uri.fsPath
return fsPath
}
// https://code.visualstudio.com/docs/editor/variables-reference
const supportedVariables: { [k: string]: () => string } = {
workspaceFolder,
workspaceFolderBasename: () => {
return path.basename(workspaceFolder())
},
cwd: () => process.cwd(),
userHome: () => os.homedir(),
// see
// https://github.com/microsoft/vscode/blob/08ac1bb67ca2459496b272d8f4a908757f24f56f/src/vs/workbench/api/common/extHostVariableResolverService.ts#L81
// or
// https://github.com/microsoft/vscode/blob/29eb316bb9f154b7870eb5204ec7f2e7cf649bec/src/vs/server/node/remoteTerminalChannel.ts#L56
execPath: () => process.env['VSCODE_EXEC_PATH'] ?? process.execPath,
pathSeparator: () => path.sep,
}
if (varName in supportedVariables) {
const fn = expectNotUndefined(
supportedVariables[varName],
`${varName} should not be undefined here`
)
return fn()
} else {
// return "${" + varName + "}";
return null
}
}

View File

@ -0,0 +1,387 @@
/* eslint suggest-no-throw/suggest-no-throw: 0 */
import * as vscode from 'vscode'
import type * as lc from 'vscode-languageclient/node'
import { Config, prepareVSCodeConfig } from './config'
import { createClient } from './client'
import {
isKclDocument,
isKclEditor,
LazyOutputChannel,
log,
type KclEditor,
} from './util'
import type { ServerStatusParams } from './lsp_ext'
import { PersistentState } from './persistent_state'
import { bootstrap } from './bootstrap'
import { TransportKind } from 'vscode-languageclient/node'
// We only support local folders, not eg. Live Share (`vlsl:` scheme), so don't activate if
// only those are in use. We use "Empty" to represent these scenarios
// (r-a still somewhat works with Live Share, because commands are tunneled to the host)
export type Workspace =
| { kind: 'Empty' }
| {
kind: 'Workspace Folder'
}
| {
kind: 'Detached Files'
files: vscode.TextDocument[]
}
export function fetchWorkspace(): Workspace {
const folders = (vscode.workspace.workspaceFolders || []).filter(
(folder) => folder.uri.scheme === 'file'
)
const kclDocuments = vscode.workspace.textDocuments.filter((document) =>
isKclDocument(document)
)
return folders.length === 0
? kclDocuments.length === 0
? { kind: 'Empty' }
: {
kind: 'Detached Files',
files: kclDocuments,
}
: { kind: 'Workspace Folder' }
}
export type CommandFactory = {
enabled: (ctx: CtxInit) => Cmd
disabled?: (ctx: Ctx) => Cmd
}
export type CtxInit = Ctx & {
readonly client: lc.LanguageClient
}
export class Ctx {
readonly statusBar: vscode.StatusBarItem
config: Config
readonly workspace: Workspace
private _client: lc.LanguageClient | undefined
private _serverPath: string | undefined
private traceOutputChannel: vscode.OutputChannel | undefined
private outputChannel: vscode.OutputChannel | undefined
private clientSubscriptions: Disposable[]
private state: PersistentState
private commandFactories: Record<string, CommandFactory>
private commandDisposables: Disposable[]
private lastStatus: ServerStatusParams | { health: 'stopped' } = {
health: 'stopped',
}
get client() {
return this._client
}
constructor(
readonly extCtx: vscode.ExtensionContext,
commandFactories: Record<string, CommandFactory>,
workspace: Workspace
) {
extCtx.subscriptions.push(this)
this.statusBar = vscode.window.createStatusBarItem(
vscode.StatusBarAlignment.Left
)
this.workspace = workspace
this.clientSubscriptions = []
this.commandDisposables = []
this.commandFactories = commandFactories
this.state = new PersistentState(extCtx.globalState)
this.config = new Config(extCtx)
this.updateCommands('disable')
this.setServerStatus({
health: 'stopped',
})
}
dispose() {
this.config.dispose()
this.statusBar.dispose()
void this.disposeClient()
this.commandDisposables.forEach((disposable) => disposable.dispose())
}
async onWorkspaceFolderChanges() {
const workspace = fetchWorkspace()
if (
workspace.kind === 'Detached Files' &&
this.workspace.kind === 'Detached Files'
) {
if (workspace.files !== this.workspace.files) {
if (this.client?.isRunning()) {
// Ideally we wouldn't need to tear down the server here, but currently detached files
// are only specified at server start
await this.stopAndDispose()
await this.start()
}
return
}
}
if (
workspace.kind === 'Workspace Folder' &&
this.workspace.kind === 'Workspace Folder'
) {
return
}
if (workspace.kind === 'Empty') {
await this.stopAndDispose()
return
}
if (this.client?.isRunning()) {
await this.restart()
}
}
private async getOrCreateClient() {
if (this.workspace.kind === 'Empty') {
return
}
if (!this.traceOutputChannel) {
this.traceOutputChannel = new LazyOutputChannel(
'KittyCAD Language Server Trace'
)
this.pushExtCleanup(this.traceOutputChannel)
}
if (!this.outputChannel) {
this.outputChannel = vscode.window.createOutputChannel(
'KittyCAD Language Server'
)
this.pushExtCleanup(this.outputChannel)
}
if (!this._client) {
this._serverPath = await bootstrap(
this.extCtx,
this.config,
this.state
).catch((err) => {
let message = 'bootstrap error. '
message +=
'See the logs in "OUTPUT > KittyCAD Language Client" (should open automatically). '
message +=
'To enable verbose logs use { "kcl-language-server.trace.extension": true }'
log.error('Bootstrap error', err)
throw new Error(message)
})
const run: lc.Executable = {
command: this._serverPath,
args: ['--json', 'server'],
transport: TransportKind.stdio,
options: { env: { ...process.env } },
}
const serverOptions = {
run,
debug: run,
}
let rawInitializationOptions = vscode.workspace.getConfiguration(
'kcl-language-server'
)
if (this.workspace.kind === 'Detached Files') {
rawInitializationOptions = {
detachedFiles: this.workspace.files.map((file) => file.uri.fsPath),
...rawInitializationOptions,
}
}
const initializationOptions = prepareVSCodeConfig(
rawInitializationOptions
)
this._client = await createClient(
this.traceOutputChannel,
this.outputChannel,
initializationOptions,
serverOptions
)
}
return this._client
}
async start() {
log.info('Starting language client')
const client = await this.getOrCreateClient()
if (!client) {
return
}
await client.start()
this.setServerStatus({ health: 'ok', quiescent: true })
this.updateCommands()
}
async restart() {
// FIXME: We should reuse the client, that is ctx.deactivate() if none of the configs have changed
await this.stopAndDispose()
await this.start()
}
async stop() {
if (!this._client) {
return
}
log.info('Stopping language client')
this.updateCommands('disable')
await this._client.stop()
}
async stopAndDispose() {
if (!this._client) {
return
}
log.info('Disposing language client')
this.updateCommands('disable')
await this.disposeClient()
}
private async disposeClient() {
this.clientSubscriptions?.forEach((disposable) => disposable.dispose())
this.clientSubscriptions = []
try {
await this._client?.dispose(2000)
} catch (e) {
// DO nothing.
}
this._serverPath = undefined
this._client = undefined
}
get activeKclEditor(): KclEditor | undefined {
const editor = vscode.window.activeTextEditor
return editor && isKclEditor(editor) ? editor : undefined
}
get extensionPath(): string {
return this.extCtx.extensionPath
}
get subscriptions(): Disposable[] {
return this.extCtx.subscriptions
}
get serverPath(): string | undefined {
return this._serverPath
}
private updateCommands(forceDisable?: 'disable') {
this.commandDisposables.forEach((disposable) => disposable.dispose())
this.commandDisposables = []
const clientRunning = (!forceDisable && this._client?.isRunning()) ?? false
const isClientRunning = function (_ctx: Ctx): _ctx is CtxInit {
return clientRunning
}
for (const [name, factory] of Object.entries(this.commandFactories)) {
const fullName = `kcl-language-server.${name}`
let callback
if (isClientRunning(this)) {
// we asserted that `client` is defined
callback = factory.enabled(this)
} else if (factory.disabled) {
callback = factory.disabled(this)
} else {
callback = () =>
vscode.window.showErrorMessage(
`command ${fullName} failed: kcl-language-server server is not running`
)
}
this.commandDisposables.push(
vscode.commands.registerCommand(fullName, callback)
)
}
}
setServerStatus(status: ServerStatusParams | { health: 'stopped' }) {
this.lastStatus = status
this.updateStatusBarItem()
}
refreshServerStatus() {
this.updateStatusBarItem()
}
private updateStatusBarItem() {
let icon = ''
const status = this.lastStatus
const statusBar = this.statusBar
statusBar.show()
statusBar.tooltip = new vscode.MarkdownString('', true)
statusBar.tooltip.isTrusted = true
switch (status.health) {
case 'ok':
statusBar.tooltip.appendText(status.message ?? 'Ready')
statusBar.color = undefined
statusBar.backgroundColor = undefined
statusBar.command = 'kcl-language-server.openLogs'
break
case 'warning':
if (status.message) {
statusBar.tooltip.appendText(status.message)
}
statusBar.color = new vscode.ThemeColor(
'statusBarItem.warningForeground'
)
statusBar.backgroundColor = new vscode.ThemeColor(
'statusBarItem.warningBackground'
)
statusBar.command = 'kcl-language-server.openLogs'
icon = '$(warning) '
break
case 'error':
if (status.message) {
statusBar.tooltip.appendText(status.message)
}
statusBar.color = new vscode.ThemeColor('statusBarItem.errorForeground')
statusBar.backgroundColor = new vscode.ThemeColor(
'statusBarItem.errorBackground'
)
statusBar.command = 'kcl-language-server.openLogs'
icon = '$(error) '
break
case 'stopped':
statusBar.tooltip.appendText('Server is stopped')
statusBar.tooltip.appendMarkdown(
'\n\n[Start server](command:kcl-language-server.startServer)'
)
statusBar.color = new vscode.ThemeColor(
'statusBarItem.warningForeground'
)
statusBar.backgroundColor = new vscode.ThemeColor(
'statusBarItem.warningBackground'
)
statusBar.command = 'kcl-language-server.startServer'
statusBar.text = '$(stop-circle) kcl-language-server'
return
}
if (statusBar.tooltip.value) {
statusBar.tooltip.appendMarkdown('\n\n---\n\n')
}
statusBar.tooltip.appendMarkdown(
'\n\n[Restart server](command:kcl-language-server.restartServer)'
)
statusBar.tooltip.appendMarkdown(
'\n\n[Stop server](command:kcl-language-server.stopServer)'
)
if (!status.quiescent) icon = '$(sync~spin) '
statusBar.text = `${icon}kcl-language-server`
}
pushExtCleanup(d: Disposable) {
this.extCtx.subscriptions.push(d)
}
}
export interface Disposable {
dispose(): void
}
export type Cmd = (...args: any[]) => unknown

View File

@ -0,0 +1,24 @@
/* eslint suggest-no-throw/suggest-no-throw: 0 */
import * as lc from 'vscode-languageclient'
export type CommandLink = {
/**
* A tooltip for the command, when represented in the UI.
*/
tooltip?: string
} & lc.Command
export type CommandLinkGroup = {
title?: string
commands: CommandLink[]
}
// experimental extensions
export const serverStatus = new lc.NotificationType<ServerStatusParams>(
'experimental/serverStatus'
)
export type ServerStatusParams = {
health: 'ok' | 'warning' | 'error'
quiescent: boolean
message?: string
}

View File

@ -0,0 +1,79 @@
/* eslint suggest-no-throw/suggest-no-throw: 0 */
import * as vscode from 'vscode'
import type * as lc from 'vscode-languageclient/node'
import * as commands from './commands'
import { type CommandFactory, Ctx, fetchWorkspace } from './ctx'
import { setContextValue } from './util'
const KCL_PROJECT_CONTEXT_NAME = 'inKclProject'
export interface KclAnalyzerExtensionApi {
readonly client?: lc.LanguageClient
}
export async function deactivate() {
await setContextValue(KCL_PROJECT_CONTEXT_NAME, undefined)
}
export async function activate(
context: vscode.ExtensionContext
): Promise<KclAnalyzerExtensionApi> {
const ctx = new Ctx(context, createCommands(), fetchWorkspace())
// VS Code doesn't show a notification when an extension fails to activate
// so we do it ourselves.
const api = await activateServer(ctx).catch((err) => {
void vscode.window.showErrorMessage(
`Cannot activate kcl-language-server extension: ${err.message}`
)
throw err
})
await setContextValue(KCL_PROJECT_CONTEXT_NAME, true)
return api
}
async function activateServer(ctx: Ctx): Promise<KclAnalyzerExtensionApi> {
await ctx.start()
return ctx
}
function createCommands(): Record<string, CommandFactory> {
return {
restartServer: {
enabled: (ctx) => async () => {
await ctx.restart()
},
disabled: (ctx) => async () => {
await ctx.start()
},
},
startServer: {
enabled: (ctx) => async () => {
await ctx.start()
ctx.setServerStatus({
health: 'ok',
quiescent: true,
})
},
disabled: (ctx) => async () => {
await ctx.start()
ctx.setServerStatus({
health: 'ok',
quiescent: true,
})
},
},
stopServer: {
enabled: (ctx) => async () => {
// FIXME: We should reuse the client, that is ctx.deactivate() if none of the configs have changed
await ctx.stopAndDispose()
ctx.setServerStatus({
health: 'stopped',
})
},
disabled: (_) => async () => {},
},
serverVersion: { enabled: commands.serverVersion },
openLogs: { enabled: commands.openLogs },
}
}

View File

@ -0,0 +1,21 @@
/* eslint suggest-no-throw/suggest-no-throw: 0 */
import type * as vscode from 'vscode'
import { log } from './util'
export class PersistentState {
constructor(private readonly globalState: vscode.Memento) {
const { serverVersion } = this
log.info('PersistentState:', { serverVersion })
}
/**
* Version of the extension that installed the server.
* Used to check if we need to run patchelf again on NixOS.
*/
get serverVersion(): string | undefined {
return this.globalState.get('serverVersion')
}
async updateServerVersion(value: string | undefined) {
await this.globalState.update('serverVersion', value)
}
}

View File

@ -0,0 +1,25 @@
import * as path from 'path'
import { runTests } from '@vscode/test-electron'
async function main() {
try {
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath = path.resolve(__dirname, '../../')
// The path to the extension test runner script
// Passed to --extensionTestsPath
const extensionTestsPath = path.resolve(__dirname, './suite/index')
// Download VS Code, unzip it and run the integration test
await runTests({ extensionDevelopmentPath, extensionTestsPath })
} catch (err) {
console.error(err)
console.error('Failed to run tests')
process.exit(1)
}
}
/* eslint-disable */
main()

View File

@ -0,0 +1,16 @@
import * as assert from 'assert'
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from 'vscode'
// import * as myExtension from '../../extension';
suite('Extension Test Suite', () => {
/* eslint-disable */
vscode.window.showInformationMessage('Start all tests.')
test('Sample test', () => {
assert.strictEqual([1, 2, 3].indexOf(5), -1)
assert.strictEqual([1, 2, 3].indexOf(0), -1)
})
})

View File

@ -0,0 +1,33 @@
import * as path from 'path'
const Mocha = require('mocha')
const { glob } = require('glob')
export function run(): Promise<void> {
// Create the mocha test
const mocha = new Mocha({
ui: 'tdd',
})
const testsRoot = path.resolve(__dirname, '..')
return new Promise((c, e) => {
glob('**/**.test.js', { cwd: testsRoot }).then((files: string[]) => {
// Add files to the test suite
files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f)))
try {
// Run the mocha test
mocha.run((failures: any) => {
if (failures > 0) {
e(new Error(`${failures} tests failed.`))
} else {
c()
}
})
} catch (err) {
console.error(err)
e(err)
}
})
})
}

View File

@ -0,0 +1,23 @@
/* eslint suggest-no-throw/suggest-no-throw: 0 */
export type NotUndefined<T> = T extends undefined ? never : T
export type Undefinable<T> = T | undefined
function isNotUndefined<T>(input: Undefinable<T>): input is NotUndefined<T> {
return input !== undefined
}
export function expectNotUndefined<T>(
input: Undefinable<T>,
msg: string
): NotUndefined<T> {
if (isNotUndefined(input)) {
return input
}
throw new TypeError(msg)
}
export function unwrapUndefinable<T>(input: Undefinable<T>): NotUndefined<T> {
return expectNotUndefined(input, `unwrapping \`undefined\``)
}

View File

@ -0,0 +1,240 @@
/* eslint suggest-no-throw/suggest-no-throw: 0 */
import * as vscode from 'vscode'
import { strict as nativeAssert } from 'assert'
import { exec, type ExecOptions, spawnSync } from 'child_process'
import { inspect } from 'util'
export interface Env {
[name: string]: string
}
export function assert(
condition: boolean,
explanation: string
): asserts condition {
try {
nativeAssert(condition, explanation)
} catch (err) {
log.error(`Assertion failed:`, explanation)
throw err
}
}
class Logger {
private enabled = true
private readonly output = vscode.window.createOutputChannel(
'KittyCAD Language Client'
)
setEnabled(yes: boolean): void {
log.enabled = yes
}
// Hint: the type [T, ...T[]] means a non-empty array
debug(...msg: [unknown, ...unknown[]]): void {
if (!log.enabled) return
log.write('DEBUG', ...msg)
}
info(...msg: [unknown, ...unknown[]]): void {
log.write('INFO', ...msg)
}
warn(...msg: [unknown, ...unknown[]]): void {
debugger
log.write('WARN', ...msg)
}
error(...msg: [unknown, ...unknown[]]): void {
debugger
log.write('ERROR', ...msg)
log.output.show(true)
}
private write(label: string, ...messageParts: unknown[]): void {
const message = messageParts.map(log.stringify).join(' ')
const dateTime = new Date().toLocaleString()
log.output.appendLine(`${label} [${dateTime}]: ${message}`)
}
private stringify(val: unknown): string {
if (typeof val === 'string') return val
return inspect(val, {
colors: false,
depth: 6, // heuristic
})
}
}
export const log = new Logger()
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
export type KclDocument = vscode.TextDocument & { languageId: 'kcl' }
export type KclEditor = vscode.TextEditor & { document: KclDocument }
export function isKclDocument(
document: vscode.TextDocument
): document is KclDocument {
// Prevent corrupted text (particularly via inlay hints) in diff views
// by allowing only `file` schemes
// unfortunately extensions that use diff views not always set this
// to something different than 'file' (see ongoing bug: #4608)
return document.languageId === 'kcl' && document.uri.scheme === 'file'
}
export function isCargoTomlDocument(
document: vscode.TextDocument
): document is KclDocument {
// ideally `document.languageId` should be 'toml' but user maybe not have toml extension installed
return (
document.uri.scheme === 'file' && document.fileName.endsWith('Cargo.toml')
)
}
export function isKclEditor(editor: vscode.TextEditor): editor is KclEditor {
return isKclDocument(editor.document)
}
export function isDocumentInWorkspace(document: KclDocument): boolean {
const workspaceFolders = vscode.workspace.workspaceFolders
if (!workspaceFolders) {
return false
}
for (const folder of workspaceFolders) {
if (document.uri.fsPath.startsWith(folder.uri.fsPath)) {
return true
}
}
return false
}
export function isValidExecutable(path: string): boolean {
log.debug('Checking availability of a binary at', path)
const res = spawnSync(path, ['--version'], {
encoding: 'utf8',
env: { ...process.env },
})
const printOutput = res.error ? log.warn : log.info
printOutput(path, '--version:', res)
return res.status === 0
}
/** Sets ['when'](https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts) clause contexts */
export function setContextValue(key: string, value: any): Thenable<void> {
return vscode.commands.executeCommand('setContext', key, value)
}
/**
* Returns a higher-order function that caches the results of invoking the
* underlying async function.
*/
export function memoizeAsync<Ret, TThis, Param extends string>(
func: (this: TThis, arg: Param) => Promise<Ret>
) {
const cache = new Map<string, Ret>()
return async function (this: TThis, arg: Param) {
const cached = cache.get(arg)
if (cached) return cached
const result = await func.call(this, arg)
cache.set(arg, result)
return result
}
}
/** Awaitable wrapper around `child_process.exec` */
export function execute(
command: string,
options: ExecOptions
): Promise<string> {
log.info(`running command: ${command}`)
return new Promise((resolve, reject) => {
exec(command, options, (err, stdout, stderr) => {
if (err) {
log.error(err)
reject(err)
return
}
if (stderr) {
reject(new Error(stderr))
return
}
resolve(stdout.trimEnd())
})
})
}
export function executeDiscoverProject(
command: string,
options: ExecOptions
): Promise<string> {
options = Object.assign({ maxBuffer: 10 * 1024 * 1024 }, options)
log.info(`running command: ${command}`)
return new Promise((resolve, reject) => {
exec(command, options, (err, stdout, _) => {
if (err) {
log.error(err)
reject(err)
return
}
resolve(stdout.trimEnd())
})
})
}
export class LazyOutputChannel implements vscode.OutputChannel {
constructor(name: string) {
this.name = name
}
name: string
_channel: vscode.OutputChannel | undefined
get channel(): vscode.OutputChannel {
if (!this._channel) {
this._channel = vscode.window.createOutputChannel(this.name)
}
return this._channel
}
append(value: string): void {
this.channel.append(value)
}
appendLine(value: string): void {
this.channel.appendLine(value)
}
replace(value: string): void {
this.channel.replace(value)
}
clear(): void {
if (this._channel) {
this._channel.clear()
}
}
show(preserveFocus?: boolean): void
show(column?: vscode.ViewColumn, preserveFocus?: boolean): void
show(column?: any, preserveFocus?: any): void {
this.channel.show(column, preserveFocus)
}
hide(): void {
if (this._channel) {
this._channel.hide()
}
}
dispose(): void {
if (this._channel) {
this._channel.dispose()
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1,151 @@
{
"name": "kcl-language-server",
"displayName": "KittyCAD Language Server",
"description": "KittyCAD language support for Visual Studio Code",
"private": true,
"icon": "icon.png",
"publisher": "kittycad",
"homepage": "https://kittycad.io",
"license": "MIT",
"version": "0.0.0",
"releaseTag": null,
"keywords": [
"language-server",
"kittycad",
"kcl",
"hardware",
"cad",
"manufacturing"
],
"categories": [
"Programming Languages"
],
"repository": {
"url": "https://github.com/kittycad/modeling-app.git",
"type": "git"
},
"engines": {
"vscode": "^1.97.0"
},
"enabledApiProposals": [],
"activationEvents": [
"onLanguage:kcl"
],
"main": "./dist/main.js",
"contributes": {
"languages": [
{
"id": "kcl",
"extensions": [
".kcl"
]
}
],
"configuration": {
"type": "object",
"title": "kcl-language-server",
"properties": {
"kcl-language-server.server.path": {
"type": [
"null",
"string"
],
"scope": "machine-overridable",
"default": null,
"markdownDescription": "Path to kcl-language-server executable (points to bundled binary by default)."
},
"kcl-language-server.trace.server": {
"type": "string",
"scope": "window",
"enum": [
"off",
"messages",
"verbose"
],
"enumDescriptions": [
"No traces",
"Error only",
"Full log"
],
"default": "off",
"description": "Trace requests to the kcl-language-server (this is usually overly verbose and not recommended for regular users)."
},
"kcl-language-server.trace.extension": {
"description": "Enable logging of VS Code extensions itself.",
"type": "boolean",
"default": false
}
}
},
"configurationDefaults": {
"[kcl]": {
"editor.semanticHighlighting.enabled": true
}
},
"commands": [
{
"command": "kcl-language-server.restartServer",
"title": "Restart server",
"category": "kcl-language-server"
},
{
"command": "kcl-language-server.startServer",
"title": "Start server",
"category": "kcl-language-server"
},
{
"command": "kcl-language-server.stopServer",
"title": "Stop server",
"category": "kcl-language-server"
},
{
"command": "kcl-language-server.serverVersion",
"title": "Show server version",
"category": "kcl-language-server"
}
],
"menus": {
"commandPalette": [
{
"command": "kcl-language-server.restartServer",
"when": "inKclProject"
},
{
"command": "kcl-language-server.serverVersion",
"when": "inKclProject"
}
]
}
},
"scripts": {
"vscode:prepublish": "yarn run build-base -- --minify",
"deploy": "vsce publish --yarn",
"build-base": "esbuild ./client/src/main.ts --bundle --outfile=dist/main.js --external:vscode --format=cjs --platform=node --target=node16",
"test-compile": "tsc -p ./",
"compile": "cross-env NODE_ENV=production tsc -b",
"build": "yarn run build-base -- --sourcemap",
"watch": "yarn run build-base -- --sourcemap --watch",
"pretest": "yarn run build && yarn test-compile",
"test": "node ./dist/client/src/test/runTest.js",
"package": "vsce package -o kcl-language-server.vsix"
},
"devDependencies": {
"@tsconfig/strictest": "^2.0.5",
"@types/glob": "^8.1.0",
"@types/mocha": "^10.0.10",
"@types/node": "^22.13.9",
"@types/vscode": "^1.97.0",
"@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^6.6.0",
"@vscode/test-electron": "^2.4.1",
"@vscode/vsce": "^2.30.0",
"cross-env": "^7.0.3",
"esbuild": "^0.25.0",
"glob": "^10.4.3",
"mocha": "^11.1.0",
"typescript": "^5.8.2"
},
"dependencies": {
"vscode-languageclient": "^9.0.1"
}
}

View File

@ -0,0 +1,180 @@
//! The `kcl` lsp server.
#![deny(missing_docs)]
use anyhow::{bail, Result};
use clap::Parser;
use slog::Drain;
use tower_lsp::{LspService, Server as LspServer};
use tracing_subscriber::{prelude::*, Layer};
lazy_static::lazy_static! {
/// Initialize the logger.
// We need a slog::Logger for steno and when we export out the logs from re-exec-ed processes.
pub static ref LOGGER: slog::Logger = {
let decorator = slog_term::TermDecorator::new().build();
let drain = slog_term::FullFormat::new(decorator).build().fuse();
let drain = slog_async::Async::new(drain).build().fuse();
slog::Logger::root(drain, slog::slog_o!())
};
}
/// This doc string acts as a help message when the user runs '--help'
/// as do all doc strings on fields.
#[derive(Parser, Debug, Clone)]
#[clap(version = clap::crate_version!(), author = clap::crate_authors!("\n"))]
pub struct Opts {
/// Print debug info
#[clap(short, long)]
pub debug: bool,
/// Print logs as json
#[clap(short, long)]
pub json: bool,
/// The subcommand to run.
#[clap(subcommand)]
pub subcmd: SubCommand,
}
impl Opts {
/// Setup our logger.
pub fn create_logger(&self) -> slog::Logger {
if self.json {
let drain = slog_json::Json::default(std::io::stderr()).fuse();
self.async_root_logger(drain)
} else {
let decorator = slog_term::TermDecorator::new().build();
let drain = slog_term::FullFormat::new(decorator).build().fuse();
self.async_root_logger(drain)
}
}
fn async_root_logger<T>(&self, drain: T) -> slog::Logger
where
T: slog::Drain + Send + 'static,
<T as slog::Drain>::Err: std::fmt::Debug,
{
let level = if self.debug {
slog::Level::Debug
} else {
slog::Level::Info
};
let level_drain = slog::LevelFilter(drain, level).fuse();
let async_drain = slog_async::Async::new(level_drain).build().fuse();
slog::Logger::root(async_drain, slog::o!())
}
}
/// A subcommand for our cli.
#[derive(Parser, Debug, Clone)]
pub enum SubCommand {
/// Run the server.
Server(kcl_lib::KclLspServerSubCommand),
}
#[tokio::main]
async fn main() -> Result<()> {
let opts: Opts = Opts::parse();
let level_filter = if opts.debug {
tracing_subscriber::filter::LevelFilter::DEBUG
} else {
tracing_subscriber::filter::LevelFilter::INFO
};
// Format fields using the provided closure.
// We want to make this very consise otherwise the logs are not able to be read by humans.
let format = tracing_subscriber::fmt::format::debug_fn(|writer, field, value| {
if format!("{}", field) == "message" {
write!(writer, "{}: {:?}", field, value)
} else {
write!(writer, "{}", field)
}
})
// Separate each field with a comma.
// This method is provided by an extension trait in the
// `tracing-subscriber` prelude.
.delimited(", ");
let (json, plain) = if opts.json {
// Cloud run likes json formatted logs if possible.
// See: https://cloud.google.com/run/docs/logging
// We could probably format these specifically for cloud run if we wanted,
// will save that as a TODO: https://cloud.google.com/run/docs/logging#special-fields
(
Some(tracing_subscriber::fmt::layer().json().with_filter(level_filter)),
None,
)
} else {
(
None,
Some(
tracing_subscriber::fmt::layer()
.pretty()
.fmt_fields(format)
.with_filter(level_filter),
),
)
};
// Initialize the tracing.
tracing_subscriber::registry().with(json).with(plain).init();
if let Err(err) = run_cmd(&opts).await {
bail!("running cmd `{:?}` failed: {:?}", &opts.subcmd, err);
}
Ok(())
}
async fn run_cmd(opts: &Opts) -> Result<()> {
match &opts.subcmd {
SubCommand::Server(s) => {
let (service, socket) = LspService::new(|client| {
kcl_lib::KclLspBackend::new(client, Default::default(), kittycad::Client::new(""), false).unwrap()
});
// TODO find a way to ctrl+c on windows.
#[cfg(not(target_os = "windows"))]
{
// For Cloud run & ctrl+c, shutdown gracefully.
// "The main process inside the container will receive SIGTERM, and after a grace period,
// SIGKILL."
// Registering SIGKILL here will panic at runtime, so let's avoid that.
use signal_hook::{
consts::{SIGINT, SIGTERM},
iterator::Signals,
};
let mut signals = Signals::new([SIGINT, SIGTERM])?;
tokio::spawn(async move {
if let Some(sig) = signals.forever().next() {
log::info!("received signal: {:?}", sig);
log::info!("triggering cleanup...");
// Exit the process.
log::info!("all clean, exiting!");
std::process::exit(0);
}
});
}
if s.stdio {
// Listen on stdin and stdout.
let stdin = tokio::io::stdin();
let stdout = tokio::io::stdout();
LspServer::new(stdin, stdout, socket).serve(service).await;
} else {
// Listen on a tcp stream.
let listener = tokio::net::TcpListener::bind(&format!("0.0.0.0:{}", s.socket)).await?;
let (stream, _) = listener.accept().await?;
let (read, write) = tokio::io::split(stream);
LspServer::new(read, write, socket).serve(service).await;
}
}
}
Ok(())
}

View File

@ -0,0 +1,20 @@
{
"extends": "@tsconfig/strictest/tsconfig.json",
"compilerOptions": {
"esModuleInterop": false,
"module": "node16",
"moduleResolution": "node16",
"target": "es2021",
"outDir": "dist",
"lib": ["es2021"],
"sourceMap": true,
"rootDir": ".",
"newLine": "LF",
"allowSyntheticDefaultImports": true,
// FIXME: https://github.com/rust-lang/rust-analyzer/issues/15253
"exactOptionalPropertyTypes": false
},
"exclude": ["node_modules", ".vscode-test"],
"include": ["client"]
}

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,7 @@ clap = { version = "4.5.27", default-features = false, optional = true, features
"derive", "derive",
] } ] }
convert_case = "0.8.0" convert_case = "0.8.0"
dashmap = "6.1.0" dashmap = {workspace = true}
dhat = { version = "0.3", optional = true } dhat = { version = "0.3", optional = true }
fnv = "1.0.7" fnv = "1.0.7"
form_urlencoded = "1.2.1" form_urlencoded = "1.2.1"
@ -35,7 +35,7 @@ itertools = "0.13.0"
kcl-derive-docs = { version = "0.1.44", path = "../kcl-derive-docs" } kcl-derive-docs = { version = "0.1.44", path = "../kcl-derive-docs" }
kittycad = { workspace = true } kittycad = { workspace = true }
kittycad-modeling-cmds = { workspace = true } kittycad-modeling-cmds = { workspace = true }
lazy_static = "1.5.0" lazy_static = { workspace = true }
measurements = "0.11.0" measurements = "0.11.0"
miette = { workspace = true } miette = { workspace = true }
mime_guess = "2.0.5" mime_guess = "2.0.5"
@ -74,7 +74,7 @@ uuid = { workspace = true, features = ["v4", "js", "serde"] }
validator = { version = "0.20.0", features = ["derive"] } validator = { version = "0.20.0", features = ["derive"] }
web-time = "1.1" web-time = "1.1"
winnow = "=0.6.24" winnow = "=0.6.24"
zip = { version = "2.2.2", default-features = false } zip = { workspace = true }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.72" } js-sys = { version = "0.3.72" }

View File

@ -23,6 +23,4 @@ We've built a lot of tooling to make contributing to KCL easier. If you are inte
If you bump the version of kcl-lib and push it to crates, be sure to update the repos we own that use it as well. These are: If you bump the version of kcl-lib and push it to crates, be sure to update the repos we own that use it as well. These are:
- [kcl.py](https://github.com/kittycad/kcl.py)
- [kcl-lsp](https://github.com/kittycad/kcl-lsp)
- [cli](https://github.com/kittycad/cli) - [cli](https://github.com/kittycad/cli)

View File

@ -205,8 +205,8 @@ impl EngineConnection {
pub async fn new(ws: reqwest::Upgraded) -> Result<EngineConnection> { pub async fn new(ws: reqwest::Upgraded) -> Result<EngineConnection> {
let wsconfig = tokio_tungstenite::tungstenite::protocol::WebSocketConfig { let wsconfig = tokio_tungstenite::tungstenite::protocol::WebSocketConfig {
// 4294967296 bytes, which is around 4.2 GB. // 4294967296 bytes, which is around 4.2 GB.
max_message_size: Some(0x100000000), max_message_size: Some(usize::MAX),
max_frame_size: Some(0x100000000), max_frame_size: Some(usize::MAX),
..Default::default() ..Default::default()
}; };

View File

@ -24,3 +24,6 @@ serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
uuid = { workspace = true, features = ["v4"] } uuid = { workspace = true, features = ["v4"] }
[lints]
workspace = true

View File

@ -37,6 +37,6 @@
"jsx": "react-jsx" "jsx": "react-jsx"
}, },
"include": ["src", "e2e", "packages", "*.ts", "rust"], "include": ["src", "e2e", "packages", "*.ts", "rust"],
"exclude": ["node_modules", "./*.grammar", "vite.config.ts"], "exclude": ["node_modules", "./*.grammar", "vite.config.ts", ".vscode-test"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]
} }

View File

@ -34,7 +34,7 @@ const config = defineConfig({
coverage: { coverage: {
provider: 'istanbul', // or 'v8' provider: 'istanbul', // or 'v8'
}, },
exclude: [...configDefaults.exclude, '**/e2e/**/*'], exclude: [...configDefaults.exclude, '**/e2e/**/*', 'rust'],
deps: { deps: {
optimizer: { optimizer: {
web: { web: {