├── .prettierrc.yml ├── .npmrc ├── img ├── icon.png └── icon.svg ├── docs ├── antora.yml ├── nav.adoc └── modules │ └── ROOT │ └── pages │ ├── index.adoc │ ├── features.adoc │ ├── installation.adoc │ └── changelog.adoc ├── .gitmodules ├── .gitignore ├── .github ├── dependabot.yml ├── PklProject ├── workflows │ ├── __lockfile__.yml │ ├── test_report.yml │ ├── main.yml │ ├── build.yml │ ├── release-branch.yml │ ├── prb.yml │ └── release.yml ├── PklProject.deps.json └── index.pkl ├── tslint.json ├── MAINTAINERS.adoc ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── licenserc.toml ├── README.md ├── scripts ├── custom-header-style.toml ├── check-grammar.sh ├── license-header.txt └── download-lsp-jar.ts ├── .vscodeignore ├── queries ├── folds.scm └── highlights.scm ├── SECURITY.md ├── src ├── pkl │ ├── index.pkl │ └── pkl.tmLanguage.pkl └── ts │ ├── requests.ts │ ├── clients │ ├── logger.ts │ └── maven.ts │ ├── config.ts │ ├── consts.ts │ ├── notifications.ts │ ├── providers │ ├── PklTextDocumentContentProvider.ts │ └── PklSemanticTokensProvider.ts │ ├── pklLspDistributionUpdater.ts │ ├── Semver.ts │ ├── utils.ts │ ├── pklLspDistribution.ts │ ├── javaDistribution.ts │ └── extension.ts ├── tsconfig.json ├── DEVELOPMENT.md ├── language-configuration.json ├── CONTRIBUTING.adoc ├── CODE_OF_CONDUCT.md ├── package.json └── LICENSE /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | printWidth: 100 -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org -------------------------------------------------------------------------------- /img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/pkl-vscode/HEAD/img/icon.png -------------------------------------------------------------------------------- /docs/antora.yml: -------------------------------------------------------------------------------- 1 | name: vscode 2 | title: VS Code Extension 3 | version: 0.21.0 4 | nav: 5 | - nav.adoc 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "textmate-bundle"] 2 | path = textmate-bundle 3 | url = https://github.com/apple/pkl.tmbundle.git 4 | -------------------------------------------------------------------------------- /docs/nav.adoc: -------------------------------------------------------------------------------- 1 | * xref:ROOT:installation.adoc[Installation] 2 | * xref:ROOT:features.adoc[Features] 3 | * xref:ROOT:changelog.adoc[Changelog] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | /node_modules 4 | /.vscode-test 5 | 6 | /out 7 | /.out 8 | /.dist 9 | /.gradle 10 | 11 | /.idea 12 | /*.iml 13 | /*.ipr 14 | /*.iws 15 | 16 | emsdk-* 17 | 18 | .pkl-lsp 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | ignore: 6 | - dependency-name: '*' 7 | update-types: 8 | - version-update:semver-major 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-string-throw": true, 4 | "no-unused-expression": true, 5 | "no-duplicate-variable": true, 6 | "curly": true, 7 | "class-name": true, 8 | "semicolon": [ 9 | true, 10 | "always" 11 | ], 12 | "triple-equals": true 13 | }, 14 | "defaultSeverity": "warning" 15 | } -------------------------------------------------------------------------------- /docs/modules/ROOT/pages/index.adoc: -------------------------------------------------------------------------------- 1 | = VS Code Extension 2 | 3 | The VS Code extension provides Pkl language support for https://code.visualstudio.com[Visual Studio Code]. 4 | 5 | Follow xref:installation.adoc[installation instructions] and check out the xref:features.adoc[feature overview]. 6 | 7 | Review the xref:changelog.adoc[changelog] for the latest changes. 8 | -------------------------------------------------------------------------------- /MAINTAINERS.adoc: -------------------------------------------------------------------------------- 1 | = MAINTAINERS 2 | 3 | This page lists all active Maintainers of this repository. 4 | 5 | See link:CONTRIBUTING.adoc[] for general contribution guidelines. 6 | 7 | == Maintainers (in alphabetical order) 8 | 9 | * https://github.com/bioball[Daniel Chao] 10 | * https://github.com/stackoverflow[Islon Scherer] 11 | * https://github.com/HT154[Jen Basch] 12 | * https://github.com/holzensp[Philip Hölzenspies] 13 | -------------------------------------------------------------------------------- /docs/modules/ROOT/pages/features.adoc: -------------------------------------------------------------------------------- 1 | = Features 2 | 3 | This plugin integrates with xref:lsp:ROOT:index.adoc[pkl-lsp] to provide code completion, hover-over documentation, project support, and more! 4 | 5 | == Supported features 6 | 7 | * Brace matching 8 | * Syntax highlighting 9 | * Code snippets 10 | * Go-to-definition 11 | * Hover-over documentation 12 | * Static code analysis 13 | 14 | The feature set grows as pkl-lsp improves over time. -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "bin": true 5 | }, 6 | "search.exclude": { 7 | "out": true, 8 | "bin": true 9 | }, 10 | "files.trimTrailingWhitespace": true, 11 | "editor.insertSpaces": true, 12 | "editor.tabSize": 2, 13 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": true, 14 | "editor.formatOnSave": false 15 | } -------------------------------------------------------------------------------- /licenserc.toml: -------------------------------------------------------------------------------- 1 | additionalHeaders = ["scripts/custom-header-style.toml"] 2 | 3 | headerPath = "scripts/license-header.txt" 4 | 5 | includes = [ 6 | "*.pkl", 7 | "*.scm", 8 | "*.sh", 9 | "*.ts", 10 | ] 11 | 12 | excludes = [ 13 | "docs", 14 | "textmate-bundle" 15 | ] 16 | 17 | [git] 18 | attrs = 'auto' 19 | ignore = 'auto' 20 | 21 | [properties] 22 | copyrightOwner = "Apple Inc. and the Pkl project authors" 23 | 24 | [mapping.SCHEME_STYLE] 25 | extensions = ["scm"] 26 | 27 | #[mapping.PKL_STYLE] 28 | #extensions = ["ts"] 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pkl-vscode 2 | 3 | Pkl language support for [Visual Studio Code](https://code.visualstudio.com). 4 | 5 | ## Documentation 6 | 7 | The [user documentation](https://pkl-lang.org/vscode/current/index.html) provides [installation instructions](https://pkl-lang.org/vscode/current/installation.html) and a [feature overview](https://pkl-lang.org/vscode/current/features.html). 8 | 9 | ## Community 10 | 11 | We'd love to hear from you! 12 | 13 | * Report an [Issue](https://github.com/apple/pkl-vscode/issues/new/choose) 14 | * Submit topics on [GitHub Discussions](https://github.com/apple/pkl/discussions) 15 | -------------------------------------------------------------------------------- /scripts/custom-header-style.toml: -------------------------------------------------------------------------------- 1 | [SCHEME_STYLE] 2 | firstLine = '' 3 | endLine = "\n" 4 | beforeEachLine = '; ' 5 | afterEachLine = '' 6 | allowBlankLines = false 7 | multipleLines = false 8 | padLines = false 9 | skipLinePattern = '^;!.*$' 10 | firstLineDetectionPattern = ';.*$' 11 | lastLineDetectionPattern = ';.*$' 12 | 13 | [VIM_STYLE] 14 | firstLine = '' 15 | endLine = "\n" 16 | beforeEachLine = '" ' 17 | afterEachLine = '' 18 | allowBlankLines = false 19 | multipleLines = false 20 | padLines = false 21 | skipLinePattern = '^\"!.*$' 22 | firstLineDetectionPattern = '\".*$' 23 | lastLineDetectionPattern = '\".*$' 24 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | **/* 2 | 3 | !CHANGELOG.md 4 | !README.md 5 | !language-configuration.json 6 | !package.json 7 | !package-lock.json 8 | !out/** 9 | !queries/* 10 | !img/* 11 | !LICENSE 12 | 13 | # Ensure that prod dependencies are included. 14 | # 15 | # This is computed from: 16 | # `cat package-lock.json | jq -r '.packages | map_values(select(.dev != true)) | keys[] | select(. != "")'` 17 | !node_modules/balanced-match 18 | !node_modules/brace-expansion 19 | !node_modules/minimatch 20 | !node_modules/semver 21 | !node_modules/vscode-jsonrpc 22 | !node_modules/vscode-languageclient 23 | !node_modules/vscode-languageserver-protocol 24 | !node_modules/vscode-languageserver-types 25 | !node_modules/web-tree-sitter 26 | -------------------------------------------------------------------------------- /queries/folds.scm: -------------------------------------------------------------------------------- 1 | ; Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 2 | ; 3 | ; Licensed under the Apache License, Version 2.0 (the "License"); 4 | ; you may not use this file except in compliance with the License. 5 | ; You may obtain a copy of the License at 6 | ; 7 | ; https://www.apache.org/licenses/LICENSE-2.0 8 | ; 9 | ; Unless required by applicable law or agreed to in writing, software 10 | ; distributed under the License is distributed on an "AS IS" BASIS, 11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | ; See the License for the specific language governing permissions and 13 | ; limitations under the License. 14 | 15 | [ 16 | (objectBody) 17 | (classBody) 18 | ] @fold 19 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | For the protection of our community, the Pkl team does not disclose, discuss, or confirm security issues until our investigation is complete and any necessary updates are generally available. 4 | 5 | ## Reporting a security vulnerability 6 | 7 | If you have discovered a security vulnerability within the Pkl VSCode project, please report it to us. 8 | We welcome reports from everyone, including security researchers, developers, and users. 9 | 10 | Security vulnerabilities may be reported on the [Report a vulnerability](https://security.apple.com/submit) form. 11 | When submitting a vulnerability, select "Apple Devices and Software" as the affected platform, and "Open Source" as the affected area. 12 | 13 | For more information, see https://pkl-lang.org/security.html. 14 | -------------------------------------------------------------------------------- /scripts/check-grammar.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | if [ -e "textmate-bundle/Syntaxes/pkl.tmLanguage" ]; then 17 | exit 0 18 | else 19 | echo "Could not find grammar file." 20 | exit 1 21 | fi 22 | -------------------------------------------------------------------------------- /scripts/license-header.txt: -------------------------------------------------------------------------------- 1 | Copyright ©{{ " " }}{%- if attrs.git_file_modified_year != attrs.git_file_created_year -%}{{ attrs.git_file_created_year }}-{{ attrs.git_file_modified_year }}{%- else -%}{{ attrs.git_file_created_year }}{%- endif -%}{{ " " }}{{ props["copyrightOwner"] }}. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /src/pkl/index.pkl: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | //===----------------------------------------------------------------------===// 16 | 17 | output { 18 | files { 19 | ["out/pkl.tmLanguage.json"] = import("pkl.tmLanguage.pkl").output 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/pkl/pkl.tmLanguage.pkl: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | //===----------------------------------------------------------------------===// 16 | 17 | amends "../../textmate-bundle/pkl/pkl.tmLanguage.pkl" 18 | 19 | output { 20 | renderer = new JsonRenderer {} 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // A task runner that calls a custom npm script that compiles the extension. 2 | { 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "label": "npm-build-code", 7 | "type": "npm", 8 | "script": "build:code", 9 | "isBackground": false, 10 | "presentation": { 11 | "reveal": "never" 12 | }, 13 | "group": { 14 | "kind": "build", 15 | "isDefault": true 16 | } 17 | }, 18 | { 19 | "label": "build-lsp", 20 | "type": "shell", 21 | "options": { 22 | "cwd": "../pkl-lsp" 23 | }, 24 | "command": "./gradlew", 25 | "args": ["shadowJar"], 26 | }, 27 | { 28 | "label": "build", 29 | "dependsOn": [ 30 | "npm-build-code", 31 | "build-lsp" 32 | ] 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "runtimeExecutable": "${execPath}", 9 | "args": ["--extensionDevelopmentPath=${workspaceFolder}" ], 10 | "sourceMaps": true, 11 | "outFiles": [ "${workspaceFolder}/out/**/*.js" ], 12 | "preLaunchTask": "build", 13 | "trace": true 14 | }, 15 | { 16 | "name": "Launch Tests", 17 | "type": "extensionHost", 18 | "request": "launch", 19 | "runtimeExecutable": "${execPath}", 20 | "args": ["--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/test" ], 21 | "sourceMaps": true, 22 | "outFiles": [ "${workspaceFolder}/out/test/**/*.js" ], 23 | "preLaunchTask": "npm: watch" 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /docs/modules/ROOT/pages/installation.adoc: -------------------------------------------------------------------------------- 1 | = Installation 2 | 3 | The extension is distributed as a Visual Studio Extension (_.vsix_) file. 4 | To install the extension, 5 | 6 | 1. download the latest version of the _.vsix_ file from https://github.com/apple/pkl-vscode/releases/latest/ 7 | 2. follow https://code.visualstudio.com/docs/editor/extension-gallery#_install-from-a-vsix[Install from a VSIX] in the VS Code docs. 8 | 9 | To confirm that the installation succeeded, open a _.pkl_ file and verify that syntax highlighting works. 10 | 11 | == Java requirement 12 | 13 | The extension currently requires Java 22+ in order to run the xref:lsp:ROOT:index.adoc[Pkl Language Server]. 14 | 15 | By default, it will look for the Java either in `PATH`, or in `JAVA_HOME`. 16 | 17 | The path to the Java executable can also be set via the `pkl.lsp.java.path` setting. 18 | 19 | The Java requirement will be removed when the Pkl Language Server is published as a native executable. For more details, see https://github.com/apple/pkl-lsp/issues/60. 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "esnext", 5 | "outDir": "out", 6 | "lib": [ 7 | "esnext" 8 | ], 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "rootDir": "src/ts", 12 | "resolveJsonModule": true, 13 | "esModuleInterop": true, 14 | /* Strict Type-Checking Option */ 15 | "strict": true, /* enable all strict type-checking options */ 16 | /* Additional Checks */ 17 | "noUnusedLocals": true /* Report errors on unused locals. */ 18 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 19 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 20 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 21 | }, 22 | "exclude": [ 23 | "node_modules", 24 | ".vscode-test", 25 | "scripts" 26 | ] 27 | } -------------------------------------------------------------------------------- /src/ts/requests.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { RequestType, TextDocumentIdentifier } from "vscode-languageclient/node"; 18 | 19 | export const pklFileContentRequest = new RequestType( 20 | "pkl/fileContents", 21 | ); 22 | 23 | export const pklDownloadPackageRequest = new RequestType("pkl/downloadPackage"); 24 | 25 | export const pklSyncProjectsRequest = new RequestType("pkl/syncProjects"); 26 | -------------------------------------------------------------------------------- /.github/PklProject: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | //===----------------------------------------------------------------------===// 16 | 17 | amends "pkl:Project" 18 | 19 | dependencies { 20 | ["pkl.impl.ghactions"] { 21 | uri = "package://pkg.pkl-lang.org/pkl-project-commons/pkl.impl.ghactions@1.1.6" 22 | } 23 | ["com.github.actions"] { 24 | uri = "package://pkg.pkl-lang.org/pkl-pantry/com.github.actions@1.3.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ts/clients/logger.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as vscode from "vscode"; 18 | 19 | const channel = vscode.window.createOutputChannel("pkl-vscode"); 20 | 21 | const logger = { 22 | log(message: string) { 23 | channel.appendLine(`[LOG] ${message}`); 24 | }, 25 | warn(message: string) { 26 | channel.appendLine(`[WARN] ${message}`); 27 | }, 28 | error(message: string) { 29 | channel.appendLine(`[ERROR] ${message}`); 30 | }, 31 | }; 32 | 33 | export default logger; 34 | -------------------------------------------------------------------------------- /.github/workflows/__lockfile__.yml: -------------------------------------------------------------------------------- 1 | #file: noinspection MandatoryParamsAbsent,UndefinedAction 2 | # This is a fake workflow that never runs. 3 | # It's used to pin actions to specific git SHAs when generating actual workflows. 4 | # It also gets updated by dependabot (see .github/dependabot.yml). 5 | # Generated from Workflow.pkl. DO NOT EDIT. 6 | name: __lockfile__ 7 | 'on': 8 | push: 9 | branches-ignore: 10 | - '**' 11 | tags-ignore: 12 | - '**' 13 | jobs: 14 | locks: 15 | if: 'false' 16 | runs-on: nothing 17 | steps: 18 | - name: actions/checkout@v6 19 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 20 | - name: actions/create-github-app-token@v2 21 | uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2 22 | - name: actions/download-artifact@v6 23 | uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 24 | - name: actions/setup-node@v6 25 | uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 26 | - name: actions/upload-artifact@v5 27 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 28 | -------------------------------------------------------------------------------- /scripts/download-lsp-jar.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import path from "node:path"; 18 | import { downloadArtifact } from "../src/ts/clients/maven"; 19 | import { BUNDLED_LSP_VERSION } from "../src/ts/consts"; 20 | 21 | /** 22 | * Downloads the pkl lsp, and places it into `out`. 23 | */ 24 | (async () => { 25 | await downloadArtifact( 26 | { 27 | group: "org.pkl-lang", 28 | artifact: "pkl-lsp", 29 | version: BUNDLED_LSP_VERSION 30 | }, 31 | path.join(__dirname, "../out/pkl-lsp.jar") 32 | ); 33 | })(); 34 | -------------------------------------------------------------------------------- /.github/workflows/test_report.yml: -------------------------------------------------------------------------------- 1 | # Do not modify! 2 | # This file was generated from a template using https://github.com/StefMa/pkl-gha 3 | 4 | name: PR Test Reports 5 | 'on': 6 | workflow_run: 7 | types: 8 | - completed 9 | workflows: 10 | - Pull Request 11 | permissions: 12 | contents: read 13 | jobs: 14 | test-results: 15 | name: Test Results 16 | if: github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure' 17 | permissions: 18 | actions: read 19 | checks: write 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Download artifacts 23 | uses: dawidd6/action-download-artifact@v11 24 | with: 25 | path: artifacts 26 | name: test-results-.* 27 | name_is_regex: true 28 | run_id: ${{ github.event.workflow_run.id }} 29 | - name: Publish test results 30 | uses: EnricoMi/publish-unit-test-result-action@v2 31 | with: 32 | commit: ${{ github.event.workflow_run.head_sha }} 33 | comment_mode: 'off' 34 | files: artifacts/**/*.xml 35 | event_file: artifacts/test-results-event-file/event.json 36 | event_name: ${{ github.event.workflow_run.event }} 37 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## Building locally 4 | 5 | ### Emscripten 6 | 7 | At the moment, we require emscripten version 2.0.24 to build the project, because tree-sitter-pkl 8 | is incompatible with `.wasm` modules built using newer versions. 9 | 10 | The easiest way to set up emscripten is through [emsdk](https://github.com/emscripten-core/emsdk). 11 | Clone the project down, and within the repo, run `./emsdk install 2.0.24 && ./emsdk activate 2.0.24`. 12 | 13 | ### Building the extension 14 | 15 | To install all NPM dependencies, run `npm install`. 16 | 17 | To build the extension, run `npm run package`. This will build a `.vsix` bundle and place it into `.dist/vscode/pkl-vscode`. 18 | 19 | To install the extension locally, run `npm run installextension`. 20 | 21 | ## Developing locally 22 | 23 | To develop locally, there is a "Launch Extension" task. In VSCode, navigate to the "Run and Debug" pane, and select "Launch Extension" to launch an extension host instance of VSCode. 24 | 25 | Prior to launching, ensure the project environment is set up by running `npm run build:local`. 26 | 27 | The local development environment requires that the [esbuild Problem Matchers](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) plugin is installed into VSCode. 28 | -------------------------------------------------------------------------------- /.github/PklProject.deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "resolvedDependencies": { 4 | "package://pkg.pkl-lang.org/pkl-pantry/com.github.actions@1": { 5 | "type": "remote", 6 | "uri": "projectpackage://pkg.pkl-lang.org/pkl-pantry/com.github.actions@1.3.0", 7 | "checksums": { 8 | "sha256": "76174cb974310b3d952280b76ed224eb4ee6fd5419bf696ad9a66fba44bd427d" 9 | } 10 | }, 11 | "package://pkg.pkl-lang.org/pkl-project-commons/pkl.impl.ghactions@1": { 12 | "type": "remote", 13 | "uri": "projectpackage://pkg.pkl-lang.org/pkl-project-commons/pkl.impl.ghactions@1.1.6", 14 | "checksums": { 15 | "sha256": "efe36e694f45b0804c5fcc746774727c016c866478b8c1eb0ea86d00c8bd8cf2" 16 | } 17 | }, 18 | "package://pkg.pkl-lang.org/pkl-pantry/pkl.experimental.deepToTyped@1": { 19 | "type": "remote", 20 | "uri": "projectpackage://pkg.pkl-lang.org/pkl-pantry/pkl.experimental.deepToTyped@1.1.1", 21 | "checksums": { 22 | "sha256": "1e6e29b441ffdee2605d317f6543a4a604aab5af472b63f0c47d92a3b4b36f7f" 23 | } 24 | }, 25 | "package://pkg.pkl-lang.org/pkl-pantry/com.github.dependabot@1": { 26 | "type": "remote", 27 | "uri": "projectpackage://pkg.pkl-lang.org/pkl-pantry/com.github.dependabot@1.0.0", 28 | "checksums": { 29 | "sha256": "02ef6f25bfca5b1d095db73ea15de79d2d2c6832ebcab61e6aba90554382abcb" 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "//", 4 | "blockComment": [ "/*", "*/" ] 5 | }, 6 | "brackets": [ 7 | ["{", "}"], 8 | ["[", "]"], 9 | ["(", ")"] 10 | ], 11 | "autoClosingPairs": [ 12 | { "open": "{", "close": "}" }, 13 | { "open": "[", "close": "]" }, 14 | { "open": "(", "close": ")" }, 15 | { "open": "\"", "close": "\"", "notIn": ["string"] }, 16 | { "open": "#\"", "close": "\"#", "notIn": ["string"] }, 17 | { "open": "##\"", "close": "\"##", "notIn": ["string"] }, 18 | { "open": "###\"", "close": "\"###", "notIn": ["string"] }, 19 | { "open": "\"\"\"", "close": "\"\"\"", "notIn": ["string"] }, 20 | { "open": "#\"\"\"", "close": "\"\"\"#", "notIn": ["string"] }, 21 | { "open": "##\"\"\"", "close": "\"\"\"##", "notIn": ["string"] }, 22 | { "open": "###\"\"\"", "close": "\"\"\"###", "notIn": ["string"] }, 23 | { "open": "`", "close": "`", "notIn": ["string", "comment"] } 24 | ], 25 | "surroundingPairs": [ 26 | ["{", "}"], 27 | ["[", "]"], 28 | ["(", ")"], 29 | ["\"", "\""], 30 | ["#\"", "\"#"], 31 | ["##\"", "\"##"], 32 | ["\"\"\"", "\"\"\""], 33 | ["#\"\"\"", "\"\"\"#"], 34 | ["##\"\"\"", "\"\"\"##"], 35 | ["`", "`"] 36 | ], 37 | "folding": { 38 | "offSide": false, 39 | "markers": { 40 | "start": "^\\s*//#region", 41 | "end": "^\\s*//#endregion" 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /CONTRIBUTING.adoc: -------------------------------------------------------------------------------- 1 | :uri-github-issue-pkl-vscode: https://github.com/apple/pkl-vscode/issues/new 2 | :uri-seven-rules: https://cbea.ms/git-commit/#seven-rules 3 | 4 | = Pkl VSCode Contributors Guide 5 | 6 | Welcome to the Pkl community, and thank you for contributing! 7 | This guide explains how to get involved. 8 | 9 | * <> 10 | * <> 11 | * <> 12 | 13 | == Licensing 14 | 15 | Pkl VSCode is released under the Apache 2.0 license. 16 | This is why we require that, by submitting a pull request, you acknowledge that you have the right to license your contribution to Apple and the community, and agree that your contribution is licensed under the Apache 2.0 license. 17 | 18 | == Issue Tracking 19 | 20 | To file a bug or feature request, use {uri-github-issue-pkl-vscode}[GitHub]. 21 | Be sure to include the following information: 22 | 23 | * Context 24 | ** What are/were you trying to achieve? 25 | ** What's the impact of this bug/feature? 26 | 27 | == Pull Requests 28 | 29 | When preparing a pull request, follow this checklist: 30 | 31 | * Imitate the conventions of surrounding code. 32 | * Follow the {uri-seven-rules}[seven rules] of great Git commit messages: 33 | ** Separate subject from body with a blank line. 34 | ** Limit the subject line to 50 characters. 35 | ** Capitalize the subject line. 36 | ** Do not end the subject line with a period. 37 | ** Use the imperative mood in the subject line. 38 | ** Wrap the body at 72 characters. 39 | ** Use the body to explain what and why vs. how. 40 | 41 | == Maintainers 42 | 43 | The project’s maintainers (those with write access to the upstream repository) are listed in link:MAINTAINERS.adoc[]. 44 | -------------------------------------------------------------------------------- /src/ts/config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { workspace } from "vscode"; 18 | import { 19 | CONFIG_JAVA_PATH, 20 | CONFIG_LSP_DEBUG_PORT, 21 | CONFIG_LSP_PATH, 22 | CONFIG_LSP_SOCKET_HOST, 23 | CONFIG_LSP_SOCKET_PORT, 24 | } from "./consts"; 25 | 26 | const getConfig = (configName: string): T | undefined => { 27 | const value = workspace.getConfiguration().get(configName); 28 | if (value === "" || value === null) { 29 | return undefined; 30 | } 31 | return value; 32 | }; 33 | 34 | const config = { 35 | get javaPath() { 36 | return getConfig(CONFIG_JAVA_PATH); 37 | }, 38 | 39 | get lspPath() { 40 | return getConfig(CONFIG_LSP_PATH); 41 | }, 42 | 43 | get lspDebugPort() { 44 | return getConfig(CONFIG_LSP_DEBUG_PORT); 45 | }, 46 | 47 | get lspSocketPort() { 48 | return getConfig(CONFIG_LSP_SOCKET_PORT); 49 | }, 50 | 51 | get lspSocketHost() { 52 | return getConfig(CONFIG_LSP_SOCKET_HOST); 53 | }, 54 | }; 55 | 56 | export default config; 57 | -------------------------------------------------------------------------------- /src/ts/consts.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import path from "node:path"; 18 | import os from "node:os"; 19 | 20 | export const CONFIG_JAVA_PATH = "pkl.lsp.java.path"; 21 | 22 | export const CONFIG_LSP_PATH = "pkl.lsp.path"; 23 | 24 | export const CONFIG_LSP_SOCKET_PORT = "pkl.lsp.socket.port"; 25 | 26 | export const CONFIG_LSP_SOCKET_HOST = "pkl.lsp.socket.host"; 27 | 28 | export const CONFIG_LSP_DEBUG_PORT = "pkl.lsp.debug.port"; 29 | 30 | // only used by the LSP server 31 | export const CONFIG_CLI_PATH = "pkl.cli.path"; 32 | 33 | export const COMMAND_DOWNLOAD_PACKAGE = "pkl.downloadPackage"; 34 | 35 | export const COMMAND_PKL_OPEN_FILE = "pkl.open.file"; 36 | 37 | export const COMMAND_SYNC_PROJECTS = "pkl.syncProjects"; 38 | 39 | export const COMMAND_PKL_CONFIGURE = "pkl.configure"; 40 | 41 | export const COMMAND_OPEN_WORKSPACE_SETTINGS = "workbench.action.openSettings"; 42 | 43 | export const COMMAND_RELOAD_WORKSPACE_WINDOW = "workbench.action.reloadWindow"; 44 | 45 | export const BUNDLED_LSP_VERSION = "0.5.1"; 46 | 47 | /** 48 | * The directory that pkl-lsp distributions get saved to. 49 | * 50 | * Structure: `~/.pkl/editor-support/lsp-distributions//pkl-lsp-.jar` 51 | */ 52 | export const LSP_DISTRIBUTIONS_DIR = path.join( 53 | os.homedir(), 54 | ".pkl/editor-support/lsp-distributions/", 55 | ); 56 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # Generated from Workflow.pkl. DO NOT EDIT. 2 | name: Build (main) 3 | 'on': 4 | push: 5 | branches: 6 | - main 7 | tags-ignore: 8 | - '**' 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: false 12 | permissions: 13 | contents: read 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 19 | with: 20 | persist-credentials: false 21 | submodules: recursive 22 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 23 | with: 24 | node-version: 24.x 25 | - name: Install emsdk 26 | run: |- 27 | git clone https://github.com/emscripten-core/emsdk 28 | cd emsdk 29 | ./emsdk install 2.0.24 30 | ./emsdk activate 2.0.24 31 | echo "${GITHUB_WORKSPACE}/emsdk" >> $GITHUB_PATH 32 | echo "${GITHUB_WORKSPACE}/emsdk/upstream/emscripten" >> $GITHUB_PATH 33 | - name: Set up Pkl 34 | run: |- 35 | mkdir /tmp/pkl 36 | curl -L "https://github.com/apple/pkl/releases/download/0.30.0/pkl-linux-amd64" -o /tmp/pkl/pkl 37 | chmod +x /tmp/pkl/pkl 38 | echo '/tmp/pkl' >> $GITHUB_PATH 39 | - run: npm ci 40 | - run: npm run build 41 | - run: npm test 42 | - run: npm run lint 43 | - run: npm run package-only 44 | - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 45 | with: 46 | name: pkl-vscode-build-artifacts 47 | path: .dist/vscode/*.* 48 | test-format-license-headers: 49 | name: hawkeye-check 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 53 | with: 54 | persist-credentials: false 55 | fetch-depth: 0 56 | - run: hawkeye check --config licenserc.toml --fail-if-unknown 57 | container: 58 | image: ghcr.io/korandoru/hawkeye@sha256:c3ab994c0d81f3d116aabf9afc534e18648e3e90d7525d741c1e99dd8166ec85 59 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # Generated from Workflow.pkl. DO NOT EDIT. 2 | name: Build 3 | 'on': 4 | push: 5 | branches-ignore: 6 | - main 7 | - release/* 8 | tags-ignore: 9 | - '**' 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: false 13 | permissions: 14 | contents: read 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 20 | with: 21 | persist-credentials: false 22 | submodules: recursive 23 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 24 | with: 25 | node-version: 24.x 26 | - name: Install emsdk 27 | run: |- 28 | git clone https://github.com/emscripten-core/emsdk 29 | cd emsdk 30 | ./emsdk install 2.0.24 31 | ./emsdk activate 2.0.24 32 | echo "${GITHUB_WORKSPACE}/emsdk" >> $GITHUB_PATH 33 | echo "${GITHUB_WORKSPACE}/emsdk/upstream/emscripten" >> $GITHUB_PATH 34 | - name: Set up Pkl 35 | run: |- 36 | mkdir /tmp/pkl 37 | curl -L "https://github.com/apple/pkl/releases/download/0.30.0/pkl-linux-amd64" -o /tmp/pkl/pkl 38 | chmod +x /tmp/pkl/pkl 39 | echo '/tmp/pkl' >> $GITHUB_PATH 40 | - run: npm ci 41 | - run: npm run build 42 | - run: npm test 43 | - run: npm run lint 44 | - run: npm run package-only 45 | - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 46 | with: 47 | name: pkl-vscode-build-artifacts 48 | path: .dist/vscode/*.* 49 | test-format-license-headers: 50 | name: hawkeye-check 51 | runs-on: ubuntu-latest 52 | steps: 53 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 54 | with: 55 | persist-credentials: false 56 | fetch-depth: 0 57 | - run: hawkeye check --config licenserc.toml --fail-if-unknown 58 | container: 59 | image: ghcr.io/korandoru/hawkeye@sha256:c3ab994c0d81f3d116aabf9afc534e18648e3e90d7525d741c1e99dd8166ec85 60 | -------------------------------------------------------------------------------- /src/ts/notifications.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as vscode from "vscode"; 18 | import { 19 | NotificationType, 20 | MessageType, 21 | Command, 22 | NotificationHandler, 23 | LanguageClient, 24 | } from "vscode-languageclient/node"; 25 | 26 | export interface ActionableNotification { 27 | type: MessageType; 28 | message: string; 29 | data?: any; 30 | commands: Command[]; 31 | } 32 | 33 | export const actionableNotificationType: NotificationType = 34 | new NotificationType("pkl/actionableNotification"); 35 | 36 | export const actionableNotificationHandler: NotificationHandler = async ( 37 | notification, 38 | ) => { 39 | let response: string | undefined = undefined; 40 | const titles = notification.commands.map((it) => it.title); 41 | switch (notification.type) { 42 | case MessageType.Info: 43 | case MessageType.Log: { 44 | response = await vscode.window.showInformationMessage(notification.message, ...titles); 45 | break; 46 | } 47 | case MessageType.Error: { 48 | response = await vscode.window.showErrorMessage(notification.message, ...titles); 49 | break; 50 | } 51 | case MessageType.Warning: { 52 | response = await vscode.window.showWarningMessage(notification.message, ...titles); 53 | break; 54 | } 55 | } 56 | if (response != null) { 57 | var command = notification.commands.find((it) => it.title == response)!!; 58 | vscode.commands.executeCommand(command.command, ...(command.arguments ?? [])); 59 | } 60 | }; 61 | 62 | export async function registerNotificationHandlers(client: LanguageClient) { 63 | client.onNotification(actionableNotificationType, actionableNotificationHandler); 64 | } 65 | -------------------------------------------------------------------------------- /img/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 20 | 22 | 23 | -------------------------------------------------------------------------------- /src/ts/providers/PklTextDocumentContentProvider.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | //===----------------------------------------------------------------------===// 18 | // Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. 19 | // 20 | // Licensed under the Apache License, Version 2.0 (the "License"); 21 | // you may not use this file except in compliance with the License. 22 | // You may obtain a copy of the License at 23 | // 24 | // https://www.apache.org/licenses/LICENSE-2.0 25 | // 26 | // Unless required by applicable law or agreed to in writing, software 27 | // distributed under the License is distributed on an "AS IS" BASIS, 28 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29 | // See the License for the specific language governing permissions and 30 | // limitations under the License. 31 | //===----------------------------------------------------------------------===// 32 | 33 | import * as vscode from "vscode"; 34 | import { pklFileContentRequest } from "../requests"; 35 | import { LanguageClientRef } from "../extension"; 36 | 37 | export default class PklTextDocumentContentProvider implements vscode.TextDocumentContentProvider { 38 | #languageClientBox: LanguageClientRef; 39 | 40 | #eventEmitter: vscode.EventEmitter; 41 | 42 | onDidChange: vscode.Event; 43 | 44 | constructor(languageClientBox: LanguageClientRef) { 45 | this.#languageClientBox = languageClientBox; 46 | this.#eventEmitter = new vscode.EventEmitter(); 47 | this.onDidChange = this.#eventEmitter.event; 48 | } 49 | 50 | async provideTextDocumentContent( 51 | uri: vscode.Uri, 52 | token: vscode.CancellationToken, 53 | ): Promise { 54 | const content = await this.#languageClientBox.client?.sendRequest( 55 | pklFileContentRequest, 56 | { uri: uri.toString() }, 57 | token, 58 | ); 59 | return content ?? ""; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/ts/clients/maven.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { httpsDownload, httpsGetText } from "../utils"; 18 | import path from "node:path"; 19 | import Semver from "../Semver"; 20 | 21 | export type MavenSolrResponse = { 22 | response: { 23 | numFound: number; 24 | start: number; 25 | docs: MavenSolrResponseDocs[]; 26 | }; 27 | }; 28 | 29 | export type MavenSolrResponseDocs = { 30 | id: string; 31 | g: string; 32 | a: string; 33 | latestVersion: string; 34 | repositoryId: string; 35 | }; 36 | 37 | export const getLatestVersion = async (query: { 38 | group: string; 39 | artifact: string; 40 | }): Promise => { 41 | const groupSearch = query.group.replace(".", "/"); 42 | const artifactSearch = query.artifact.replace(".", "/"); 43 | const xml = await httpsGetText( 44 | `https://repo1.maven.org/maven2/${groupSearch}/${artifactSearch}/maven-metadata.xml`, 45 | ); 46 | const versionString = extractVersion(xml); 47 | const version = versionString ? Semver.parse(versionString) : null; 48 | if (version == null) { 49 | throw new Error(`Got an artifact from Maven that is not valid semver: ${versionString}`); 50 | } 51 | return version; 52 | }; 53 | 54 | export const downloadArtifact = async ( 55 | coordinates: { group: string; artifact: string; version: string }, 56 | destination: string, 57 | ) => { 58 | const jarPath = path.join( 59 | coordinates.group.replace(/\./g, "/"), 60 | coordinates.artifact, 61 | coordinates.version, 62 | `${coordinates.artifact}-${coordinates.version}.jar`, 63 | ); 64 | const checksumPath = `${jarPath}.sha256`; 65 | const checksum = await httpsGetText(`https://repo1.maven.org/maven2/${checksumPath}`); 66 | await httpsDownload(`https://repo1.maven.org/maven2/${jarPath}`, destination, checksum); 67 | }; 68 | 69 | function extractVersion(xml: string): string | null { 70 | const match = xml.match(/(.+)<\/latest>/); 71 | return match ? match[1] : null; 72 | } 73 | -------------------------------------------------------------------------------- /src/ts/pklLspDistributionUpdater.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import path from "node:path"; 18 | import { COMMAND_RELOAD_WORKSPACE_WINDOW, LSP_DISTRIBUTIONS_DIR } from "./consts"; 19 | import { downloadArtifact, getLatestVersion } from "./clients/maven"; 20 | import { isRegularFile } from "./utils"; 21 | import * as vscode from "vscode"; 22 | import logger from "./clients/logger"; 23 | import { bundledDistribution } from "./pklLspDistribution"; 24 | 25 | export const queryForLatestLspDistribution = async () => { 26 | try { 27 | const latestVersion = await getLatestVersion({ group: "org.pkl-lang", artifact: "pkl-lsp" }); 28 | if (!latestVersion.isCompatibleWith(bundledDistribution.version)) { 29 | logger.log(`Latest version is ${latestVersion}, which I am not compatible with.`); 30 | return; 31 | } 32 | const pathOnDisk = path.join( 33 | LSP_DISTRIBUTIONS_DIR, 34 | latestVersion.toString(), 35 | `pkl-lsp-${latestVersion}.jar`, 36 | ); 37 | const isFile = await isRegularFile(pathOnDisk); 38 | if (isFile) { 39 | // is already downloaded 40 | logger.log(`Latest version of pkl-lsp is ${latestVersion}, and it is already downloaded.`); 41 | return; 42 | } 43 | if (bundledDistribution.version.isGreaterThanOrEqualTo(latestVersion)) { 44 | logger.log( 45 | `Latest version of pkl-lsp is ${latestVersion}, which is less than or equal to my built-in version.`, 46 | ); 47 | return; 48 | } 49 | const callToAction = "Download and reload VSCode"; 50 | const response = await vscode.window.showInformationMessage( 51 | `A new version of pkl-lsp (${latestVersion}) is available.`, 52 | callToAction, 53 | "Later", 54 | ); 55 | if (response !== callToAction) { 56 | return; 57 | } 58 | await downloadArtifact( 59 | { group: "org.pkl-lang", artifact: "pkl-lsp", version: latestVersion.toString() }, 60 | pathOnDisk, 61 | ); 62 | vscode.commands.executeCommand(COMMAND_RELOAD_WORKSPACE_WINDOW); 63 | } catch (err) { 64 | logger.error(`Failed to handle query for latest distribution: ${err}`); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /.github/workflows/release-branch.yml: -------------------------------------------------------------------------------- 1 | # Generated from Workflow.pkl. DO NOT EDIT. 2 | name: Build (release branch) 3 | 'on': 4 | push: 5 | branches: 6 | - release/* 7 | tags-ignore: 8 | - '**' 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | permissions: 13 | contents: read 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 19 | with: 20 | persist-credentials: false 21 | submodules: recursive 22 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 23 | with: 24 | node-version: 24.x 25 | - name: Install emsdk 26 | run: |- 27 | git clone https://github.com/emscripten-core/emsdk 28 | cd emsdk 29 | ./emsdk install 2.0.24 30 | ./emsdk activate 2.0.24 31 | echo "${GITHUB_WORKSPACE}/emsdk" >> $GITHUB_PATH 32 | echo "${GITHUB_WORKSPACE}/emsdk/upstream/emscripten" >> $GITHUB_PATH 33 | - name: Set up Pkl 34 | run: |- 35 | mkdir /tmp/pkl 36 | curl -L "https://github.com/apple/pkl/releases/download/0.30.0/pkl-linux-amd64" -o /tmp/pkl/pkl 37 | chmod +x /tmp/pkl/pkl 38 | echo '/tmp/pkl' >> $GITHUB_PATH 39 | - run: npm ci 40 | - run: npm run build 41 | - run: npm test 42 | - run: npm run lint 43 | - run: npm run package-only 44 | - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 45 | with: 46 | name: pkl-vscode-build-artifacts 47 | path: .dist/vscode/*.* 48 | test-format-license-headers: 49 | name: hawkeye-check 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 53 | with: 54 | persist-credentials: false 55 | fetch-depth: 0 56 | - run: hawkeye check --config licenserc.toml --fail-if-unknown 57 | container: 58 | image: ghcr.io/korandoru/hawkeye@sha256:c3ab994c0d81f3d116aabf9afc534e18648e3e90d7525d741c1e99dd8166ec85 59 | trigger-downstream-builds: 60 | if: github.repository_owner == 'apple' 61 | needs: 62 | - build 63 | - test-format-license-headers 64 | runs-on: ubuntu-latest 65 | steps: 66 | - name: Create app token 67 | id: app-token 68 | uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2 69 | with: 70 | app-id: ${{ secrets.PKL_CI_CLIENT_ID }} 71 | private-key: ${{ secrets.PKL_CI }} 72 | owner: ${{ github.repository_owner }} 73 | - name: Trigger pkl-lang.org build 74 | env: 75 | GH_TOKEN: ${{ steps.app-token.outputs.token }} 76 | run: |- 77 | gh workflow run \ 78 | --repo apple/pkl-lang.org \ 79 | --ref main \ 80 | --field source_run="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ 81 | main.yml 82 | -------------------------------------------------------------------------------- /src/ts/Semver.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export default class Semver { 18 | major: number; 19 | minor: number; 20 | patch: number; 21 | preRelease?: string; 22 | build?: string; 23 | 24 | static parse(versionStr: string): Semver | undefined { 25 | const parsed = versionStr.match(SEMVER_REGEXP); 26 | if (parsed === null) { 27 | return; 28 | } 29 | const [, major, minor, patch, preRelease, build] = parsed; 30 | return new Semver(parseInt(major), parseInt(minor), parseInt(patch), preRelease, build); 31 | } 32 | 33 | constructor(major: number, minor: number, patch: number, preRelease?: string, build?: string) { 34 | this.major = major; 35 | this.minor = minor; 36 | this.patch = patch; 37 | this.preRelease = preRelease; 38 | this.build = build; 39 | } 40 | 41 | toString(): string { 42 | let ret = `${this.major}.${this.minor}.${this.patch}`; 43 | if (this.preRelease !== undefined) { 44 | ret += `-${this.preRelease}`; 45 | if (this.build !== undefined) { 46 | ret += `+${this.build}`; 47 | } 48 | } 49 | return ret; 50 | } 51 | 52 | /** 53 | * Returns `1` if this is greater than `other`, `-1` if this is less than `other`, and `0` otherwise. 54 | * 55 | * Doesn't handle prerelease identifiers. 56 | */ 57 | compareTo(other: Semver) { 58 | if (this.major !== other.major) { 59 | return this.major > other.major ? 1 : -1; 60 | } 61 | if (this.minor !== other.minor) { 62 | return this.minor > other.minor ? 1 : -1; 63 | } 64 | if (this.patch !== other.patch) { 65 | return this.patch > other.patch ? 1 : -1; 66 | } 67 | return 0; 68 | } 69 | 70 | isGreaterThan(other: Semver) { 71 | return this.compareTo(other) === 1; 72 | } 73 | 74 | isGreaterThanOrEqualTo(other: Semver) { 75 | return this.compareTo(other) >= 0; 76 | } 77 | 78 | isLessThan(other: Semver) { 79 | return this.compareTo(other) === -1; 80 | } 81 | 82 | isLessThanOrEqualTo(other: Semver) { 83 | return this.compareTo(other) <= 0; 84 | } 85 | 86 | isCompatibleWith(other: Semver) { 87 | return this.major === other.major && this.minor >= other.minor; 88 | } 89 | } 90 | 91 | const SEMVER_REGEXP = 92 | /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; 93 | -------------------------------------------------------------------------------- /.github/workflows/prb.yml: -------------------------------------------------------------------------------- 1 | # Generated from Workflow.pkl. DO NOT EDIT. 2 | name: Pull Request 3 | 'on': 4 | pull_request: {} 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.ref }} 7 | cancel-in-progress: true 8 | permissions: 9 | contents: read 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 15 | with: 16 | persist-credentials: false 17 | submodules: recursive 18 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 19 | with: 20 | node-version: 24.x 21 | - name: Install emsdk 22 | run: |- 23 | git clone https://github.com/emscripten-core/emsdk 24 | cd emsdk 25 | ./emsdk install 2.0.24 26 | ./emsdk activate 2.0.24 27 | echo "${GITHUB_WORKSPACE}/emsdk" >> $GITHUB_PATH 28 | echo "${GITHUB_WORKSPACE}/emsdk/upstream/emscripten" >> $GITHUB_PATH 29 | - name: Set up Pkl 30 | run: |- 31 | mkdir /tmp/pkl 32 | curl -L "https://github.com/apple/pkl/releases/download/0.30.0/pkl-linux-amd64" -o /tmp/pkl/pkl 33 | chmod +x /tmp/pkl/pkl 34 | echo '/tmp/pkl' >> $GITHUB_PATH 35 | - run: npm ci 36 | - run: npm run build 37 | - run: npm test 38 | - run: npm run lint 39 | - run: npm run package-only 40 | - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 41 | with: 42 | name: pkl-vscode-build-artifacts 43 | path: .dist/vscode/*.* 44 | test-format-license-headers: 45 | name: hawkeye-check 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 49 | with: 50 | persist-credentials: false 51 | fetch-depth: 0 52 | - run: hawkeye check --config licenserc.toml --fail-if-unknown 53 | container: 54 | image: ghcr.io/korandoru/hawkeye@sha256:c3ab994c0d81f3d116aabf9afc534e18648e3e90d7525d741c1e99dd8166ec85 55 | upload-event-file: 56 | runs-on: ubuntu-latest 57 | steps: 58 | - name: Upload event file 59 | if: '!cancelled()' 60 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 61 | with: 62 | name: test-results-event-file 63 | path: ${{ github.event_path }} 64 | check-pkl-github-actions: 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 68 | with: 69 | persist-credentials: false 70 | - name: Setup Pkl 71 | id: setup-pkl 72 | env: 73 | PKL_VERSION: 0.30.0 74 | PKL_FILENAME: pkl 75 | PKL_DOWNLOAD_URL: https://github.com/apple/pkl/releases/download/0.30.0/pkl-linux-amd64 76 | shell: bash 77 | run: |- 78 | DIR="$(mktemp -d /tmp/pkl-$PKL_VERSION-XXXXXX)" 79 | PKL_EXEC="$DIR/$PKL_FILENAME" 80 | curl -sfL -o $PKL_EXEC "$PKL_DOWNLOAD_URL" 81 | chmod +x $PKL_EXEC 82 | echo "$DIR" >> "$GITHUB_PATH" 83 | echo "pkl_exec=$PKL_EXEC" >> "$GITHUB_OUTPUT" 84 | - shell: bash 85 | run: pkl eval -m .github/ --project-dir .github/ .github/index.pkl 86 | - name: check git status 87 | shell: bash 88 | run: |- 89 | if [ -n "$(git status --porcelain)" ]; then 90 | echo "Running pkl resulted in a diff! You likely need to run 'pkl eval' and commit the changes." 91 | git diff --name-only 92 | exit 1 93 | fi 94 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | 3 | ### Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our 7 | project and our community a harassment-free experience for everyone, 8 | regardless of age, body size, disability, ethnicity, sex 9 | characteristics, gender identity and expression, level of experience, 10 | education, socio-economic status, nationality, personal appearance, 11 | race, religion, or sexual identity and orientation. 12 | 13 | ### Our Standards 14 | 15 | Examples of behavior that contributes to creating a positive environment 16 | include: 17 | 18 | * Using welcoming and inclusive language 19 | * Being respectful of differing viewpoints and experiences 20 | * Gracefully accepting constructive criticism 21 | * Focusing on what is best for the community 22 | * Showing empathy towards other community members 23 | 24 | Examples of unacceptable behavior by participants include: 25 | 26 | * The use of sexualized language or imagery and unwelcome sexual 27 | attention or advances 28 | * Trolling, insulting/derogatory comments, and personal or political 29 | attacks 30 | * Public or private harassment 31 | * Publishing others’ private information, such as a physical or 32 | electronic address, without explicit permission 33 | * Other conduct which could reasonably be considered inappropriate in a 34 | professional setting 35 | 36 | ### Our Responsibilities 37 | 38 | Project maintainers are responsible for clarifying the standards of 39 | acceptable behavior and are expected to take appropriate and fair 40 | corrective action in response to any instances of unacceptable behavior. 41 | 42 | Project maintainers have the right and responsibility to remove, edit, 43 | or reject comments, commits, code, wiki edits, issues, and other 44 | contributions that are not aligned to this Code of Conduct, or to ban 45 | temporarily or permanently any contributor for other behaviors that they 46 | deem inappropriate, threatening, offensive, or harmful. 47 | 48 | ### Scope 49 | 50 | This Code of Conduct applies within all project spaces, and it also 51 | applies when an individual is representing the project or its community 52 | in public spaces. Examples of representing a project or community 53 | include using an official project e-mail address, posting via an 54 | official social media account, or acting as an appointed representative 55 | at an online or offline event. Representation of a project may be 56 | further defined and clarified by project maintainers. 57 | 58 | ### Enforcement 59 | 60 | Instances of abusive, harassing, or otherwise unacceptable behavior may 61 | be reported by contacting the open source team at 62 | opensource-conduct@group.apple.com. All complaints will be reviewed and 63 | investigated and will result in a response that is deemed necessary and 64 | appropriate to the circumstances. The project team is obligated to 65 | maintain confidentiality with regard to the reporter of an incident. 66 | Further details of specific enforcement policies may be posted 67 | separately. 68 | 69 | Project maintainers who do not follow or enforce the Code of Conduct in 70 | good faith may face temporary or permanent repercussions as determined 71 | by other members of the project’s leadership. 72 | 73 | ### Attribution 74 | 75 | This Code of Conduct is adapted from the 76 | [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, 77 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 78 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Generated from Workflow.pkl. DO NOT EDIT. 2 | name: Release 3 | 'on': 4 | push: 5 | branches-ignore: 6 | - '**' 7 | tags: 8 | - '**' 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: false 12 | permissions: 13 | contents: read 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 19 | with: 20 | persist-credentials: false 21 | submodules: recursive 22 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 23 | with: 24 | node-version: 24.x 25 | - name: Install emsdk 26 | run: |- 27 | git clone https://github.com/emscripten-core/emsdk 28 | cd emsdk 29 | ./emsdk install 2.0.24 30 | ./emsdk activate 2.0.24 31 | echo "${GITHUB_WORKSPACE}/emsdk" >> $GITHUB_PATH 32 | echo "${GITHUB_WORKSPACE}/emsdk/upstream/emscripten" >> $GITHUB_PATH 33 | - name: Set up Pkl 34 | run: |- 35 | mkdir /tmp/pkl 36 | curl -L "https://github.com/apple/pkl/releases/download/0.30.0/pkl-linux-amd64" -o /tmp/pkl/pkl 37 | chmod +x /tmp/pkl/pkl 38 | echo '/tmp/pkl' >> $GITHUB_PATH 39 | - run: npm ci 40 | - run: npm run build 41 | - run: npm test 42 | - run: npm run lint 43 | - run: npm run package-only 44 | - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 45 | with: 46 | name: pkl-vscode-build-artifacts 47 | path: .dist/vscode/*.* 48 | test-format-license-headers: 49 | name: hawkeye-check 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 53 | with: 54 | persist-credentials: false 55 | fetch-depth: 0 56 | - run: hawkeye check --config licenserc.toml --fail-if-unknown 57 | container: 58 | image: ghcr.io/korandoru/hawkeye@sha256:c3ab994c0d81f3d116aabf9afc534e18648e3e90d7525d741c1e99dd8166ec85 59 | deploy-github-release: 60 | needs: 61 | - build 62 | - test-format-license-headers 63 | permissions: 64 | contents: write 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 68 | with: 69 | name: pkl-vscode-build-artifacts 70 | path: .dist/vscode/ 71 | - name: Publish release to GitHub 72 | env: 73 | GH_TOKEN: ${{ github.token }} 74 | GH_REPO: ${{ github.repository }} 75 | run: |- 76 | gh release create "${{ github.ref_name }}" \ 77 | --title "${{ github.ref_name }}" \ 78 | --target "${{ github.sha }}" \ 79 | --verify-tag \ 80 | --notes "Release notes: https://pkl-lang.org/vscode/current/changelog.html#release-${{ github.ref_name }}" \ 81 | .dist/vscode/*.vsix 82 | trigger-downstream-builds: 83 | if: github.repository_owner == 'apple' 84 | needs: 85 | - build 86 | - test-format-license-headers 87 | - deploy-github-release 88 | runs-on: ubuntu-latest 89 | steps: 90 | - name: Create app token 91 | id: app-token 92 | uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2 93 | with: 94 | app-id: ${{ secrets.PKL_CI_CLIENT_ID }} 95 | private-key: ${{ secrets.PKL_CI }} 96 | owner: ${{ github.repository_owner }} 97 | - name: Trigger pkl-lang.org build 98 | env: 99 | GH_TOKEN: ${{ steps.app-token.outputs.token }} 100 | run: |- 101 | gh workflow run \ 102 | --repo apple/pkl-lang.org \ 103 | --ref main \ 104 | --field source_run="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ 105 | main.yml 106 | -------------------------------------------------------------------------------- /queries/highlights.scm: -------------------------------------------------------------------------------- 1 | ; Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 2 | ; 3 | ; Licensed under the Apache License, Version 2.0 (the "License"); 4 | ; you may not use this file except in compliance with the License. 5 | ; You may obtain a copy of the License at 6 | ; 7 | ; https://www.apache.org/licenses/LICENSE-2.0 8 | ; 9 | ; Unless required by applicable law or agreed to in writing, software 10 | ; distributed under the License is distributed on an "AS IS" BASIS, 11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | ; See the License for the specific language governing permissions and 13 | ; limitations under the License. 14 | 15 | (clazz (identifier) @class) 16 | (typeAlias (identifier) @type) 17 | (declaredType (qualifiedIdentifier (identifier) @type)) 18 | (moduleClause (qualifiedIdentifier (identifier) @type)) 19 | (annotation "@" @decorator (qualifiedIdentifier) @decorator) 20 | (typeArgumentList 21 | "<" @bracket 22 | ">" @bracket) 23 | 24 | (typeParameter (identifier) @type) 25 | 26 | (unqualifiedAccessExpr 27 | (identifier) @method (argumentList)) 28 | 29 | (unqualifiedAccessExpr 30 | (identifier) @property) 31 | 32 | (qualifiedAccessExpr 33 | (identifier) @method (argumentList)) 34 | 35 | (qualifiedAccessExpr 36 | (identifier) @property) 37 | 38 | (classMethod (methodHeader (identifier) @method)) 39 | (objectMethod (methodHeader (identifier) @method)) 40 | 41 | (classProperty (identifier) @property) 42 | (objectProperty (identifier) @property) 43 | 44 | (typedIdentifier (identifier) @parameter) 45 | 46 | (forGenerator (typedIdentifier (identifier) @variable)) 47 | (letExpr (typedIdentifier (identifier) @variable)) 48 | 49 | (importClause (identifier) @variable) 50 | (importGlobClause (identifier) @variable) 51 | 52 | (stringConstant) @string 53 | (slStringLiteralPart) @string 54 | (mlStringLiteralPart) @string 55 | 56 | (stringInterpolation 57 | "\\(" @stringEscape 58 | ")" @stringEscape) @none 59 | 60 | (stringInterpolation 61 | "\\#(" @stringEscape 62 | ")" @stringEscape) @none 63 | 64 | (stringInterpolation 65 | "\\##(" @stringEscape 66 | ")" @stringEscape) @none 67 | 68 | "\"" @string 69 | "#\"" @string 70 | "##\"" @string 71 | "###\"" @string 72 | "####\"" @string 73 | "#####\"" @string 74 | "\"#" @string 75 | "\"##" @string 76 | "\"###" @string 77 | "\"####" @string 78 | "\"#####" @string 79 | 80 | (escapeSequence) @stringEscape 81 | 82 | (intLiteralExpr) @number 83 | (floatLiteralExpr) @number 84 | 85 | 86 | "??" @operator 87 | "=" @operator 88 | "<" @operator 89 | ">" @operator 90 | "!" @operator 91 | "==" @operator 92 | "!=" @operator 93 | "<=" @operator 94 | ">=" @operator 95 | "&&" @operator 96 | "||" @operator 97 | "+" @operator 98 | "-" @operator 99 | "**" @operator 100 | "*" @operator 101 | "/" @operator 102 | "~/" @operator 103 | "%" @operator 104 | "|>" @operator 105 | 106 | 107 | "|" @operator 108 | "->" @operator 109 | 110 | "," @punctuation 111 | ":" @punctuation 112 | "." @punctuation 113 | "?." @punctuation 114 | "...?" @punctuation 115 | "..." @punctuation 116 | 117 | "(" @bracket 118 | ")" @bracket 119 | "]" @bracket 120 | "{" @bracket 121 | "}" @bracket 122 | 123 | 124 | "amends" @keyword 125 | "as" @keyword 126 | "extends" @keyword 127 | "class" @keyword 128 | "typealias" @keyword 129 | "function" @keyword 130 | "module" @keyword 131 | (trueLiteralExpr) @constant 132 | (falseLiteralExpr) @constant 133 | (nullLiteralExpr) @constant 134 | "new" @control 135 | "if" @control 136 | "else" @control 137 | "import*" @keyword 138 | "import" @keyword 139 | (importExpr "import" @keyword) 140 | (importExpr "import*" @keyword) 141 | (readExpr "read" @keyword) 142 | (readExpr "read*" @keyword) 143 | (readExpr "read?" @keyword) 144 | (traceExpr "trace" @keyword) 145 | (throwExpr "throw" @keyword) 146 | (moduleExpr "module" @type.defaultLibrary) 147 | (unknownType) @type.defaultLibrary 148 | (nothingType) @type.defaultLibrary 149 | (moduleType) @type.defaultLibrary 150 | (outerExpr) @variable.defaultLibrary 151 | "super" @variable.defaultLibrary 152 | (thisExpr) @variable.builtin 153 | (ERROR) @error 154 | -------------------------------------------------------------------------------- /.github/index.pkl: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | //===----------------------------------------------------------------------===// 16 | 17 | amends "@pkl.impl.ghactions/PklCI.pkl" 18 | 19 | import "@com.github.actions/catalog.pkl" 20 | import "@com.github.actions/context.pkl" 21 | import "@com.github.actions/Workflow.pkl" 22 | import "@pkl.impl.ghactions/jobs/HawkeyeCheck.pkl" 23 | 24 | local emsdkVersion = "2.0.24" 25 | local pklVersion = "0.30.0" 26 | 27 | local buildArtifactsName = "pkl-vscode-build-artifacts" 28 | 29 | triggerDocsBuild = "release" 30 | 31 | local buildAndTest: Workflow = new { 32 | jobs { 33 | ["build"] { 34 | `runs-on` = "ubuntu-latest" 35 | steps { 36 | (catalog.`actions/checkout@v6`) { 37 | with { 38 | submodules = "recursive" 39 | } 40 | } 41 | (catalog.`actions/setup-node@v6`) { 42 | with { 43 | `node-version` = "24.x" 44 | } 45 | } 46 | new { 47 | name = "Install emsdk" 48 | run = 49 | #""" 50 | git clone https://github.com/emscripten-core/emsdk 51 | cd emsdk 52 | ./emsdk install \#(emsdkVersion) 53 | ./emsdk activate \#(emsdkVersion) 54 | echo "${GITHUB_WORKSPACE}/emsdk" >> $GITHUB_PATH 55 | echo "${GITHUB_WORKSPACE}/emsdk/upstream/emscripten" >> $GITHUB_PATH 56 | """# 57 | } 58 | new { 59 | name = "Set up Pkl" 60 | run = 61 | #""" 62 | mkdir /tmp/pkl 63 | curl -L "https://github.com/apple/pkl/releases/download/\#(pklVersion)/pkl-linux-amd64" -o /tmp/pkl/pkl 64 | chmod +x /tmp/pkl/pkl 65 | echo '/tmp/pkl' >> $GITHUB_PATH 66 | """# 67 | } 68 | new { run = "npm ci" } 69 | new { run = "npm run build" } 70 | new { run = "npm test" } 71 | new { run = "npm run lint" } 72 | new { run = "npm run package-only" } 73 | (catalog.`actions/upload-artifact@v5`) { 74 | with { 75 | name = buildArtifactsName 76 | path = ".dist/vscode/*.*" 77 | } 78 | } 79 | } 80 | } 81 | ["test-format-license-headers"] = new HawkeyeCheck {}.job 82 | } 83 | } 84 | 85 | prb = buildAndTest 86 | build = buildAndTest 87 | main = buildAndTest 88 | releaseBranch = buildAndTest 89 | 90 | release = (buildAndTest) { 91 | jobs { 92 | ["deploy-github-release"] { 93 | `runs-on` = "ubuntu-latest" 94 | needs = buildAndTest.jobs.keys.toListing() 95 | permissions { 96 | contents = "write" 97 | } 98 | steps { 99 | (catalog.`actions/download-artifact@v6`) { 100 | with { 101 | name = buildArtifactsName 102 | path = ".dist/vscode/" 103 | } 104 | } 105 | new { 106 | name = "Publish release to GitHub" 107 | env { 108 | ["GH_TOKEN"] = context.github.token 109 | ["GH_REPO"] = context.github.repository 110 | } 111 | run = 112 | #""" 113 | gh release create "\#(context.github.refName)" \ 114 | --title "\#(context.github.refName)" \ 115 | --target "\#(context.github.sha)" \ 116 | --verify-tag \ 117 | --notes "Release notes: https://pkl-lang.org/vscode/current/changelog.html#release-\#(context.github.refName)" \ 118 | .dist/vscode/*.vsix 119 | """# 120 | } 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/ts/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { promisify } from "node:util"; 18 | import { execFile as _execFile } from "node:child_process"; 19 | import fs from "node:fs/promises"; 20 | import { createWriteStream } from "node:fs"; 21 | import https from "node:https"; 22 | import crypto from "node:crypto"; 23 | import os from "node:os"; 24 | import path from "node:path"; 25 | 26 | export const execFile = promisify(_execFile); 27 | 28 | export const debounce = ( 29 | f: (...args: A) => any, 30 | wait: number, 31 | ): ((...args: A) => void) => { 32 | let timeout: NodeJS.Timeout | undefined = undefined; 33 | return (...args: A) => { 34 | if (timeout != null) { 35 | clearTimeout(timeout); 36 | } 37 | timeout = setTimeout(() => f(...args), wait); 38 | }; 39 | }; 40 | 41 | /** 42 | * Tells if the file exists, and is a file (and not a directory). 43 | */ 44 | export const isRegularFile = async (filepath: string) => { 45 | try { 46 | const stats = await fs.stat(filepath); 47 | return stats.isFile(); 48 | } catch (err: any) { 49 | if (err.code !== "ENOENT") { 50 | throw err; 51 | } 52 | return false; 53 | } 54 | }; 55 | 56 | /** 57 | * Make an HTTPS GET request to the provided URL, parsing the response body as UTF-8 encoded text. 58 | */ 59 | export const httpsGetText = (url: string): Promise => { 60 | return new Promise((resolve, reject) => { 61 | https.get(url, { headers: { accept: "*/*", "user-agent": "pkl-vscode" } }, (response) => { 62 | response.setEncoding("utf-8"); 63 | let body = ""; 64 | response.on("data", (chunk) => { 65 | body += chunk; 66 | }); 67 | response.on("end", () => { 68 | if (response.statusCode !== 200) { 69 | reject(new Error(body)); 70 | } else { 71 | resolve(body); 72 | } 73 | }); 74 | response.on("error", (err) => { 75 | reject(err); 76 | }); 77 | }); 78 | }); 79 | }; 80 | 81 | /** 82 | * Make an HTTPS GET request to the provided URL and parse the response as JSON. 83 | */ 84 | export const httpsGetJson = async (url: string): Promise => { 85 | const text = await httpsGetText(url); 86 | return JSON.parse(text) as T; 87 | }; 88 | 89 | const downloadAndComputeChecksum = async (url: string, dest: string): Promise => { 90 | const writeStream = createWriteStream(dest, { mode: 0o755 }); 91 | const hash = crypto.createHash("sha256"); 92 | return new Promise((resolve, reject) => { 93 | https.get(url, (response) => { 94 | response 95 | .on("data", (chunk) => { 96 | hash.update(chunk); 97 | writeStream.write(chunk); 98 | }) 99 | .on("end", () => { 100 | const computedChecksum = hash.digest().toString("hex"); 101 | writeStream.end(); 102 | resolve(computedChecksum); 103 | }) 104 | .on("error", (err) => { 105 | writeStream.destroy(err); 106 | reject(err); 107 | }); 108 | }); 109 | }); 110 | }; 111 | 112 | /** 113 | * Downloads the file at the specified URL, verifying its contents against the provided checksum. 114 | */ 115 | export const httpsDownload = async (url: string, dest: string, checksum: string): Promise => { 116 | const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "pkl-vscode-")); 117 | const tempFile = `${tempDir}/contents.download`; 118 | const computedChecksum = await downloadAndComputeChecksum(url, tempFile); 119 | if (computedChecksum === checksum) { 120 | await fs.mkdir(path.resolve(dest, ".."), { recursive: true }); 121 | await fs.rename(tempFile, dest); 122 | } else { 123 | throw new Error(`Failed to download ${url}: expected ${checksum}, but got ${computedChecksum}`); 124 | } 125 | }; 126 | -------------------------------------------------------------------------------- /docs/modules/ROOT/pages/changelog.adoc: -------------------------------------------------------------------------------- 1 | = Changelog 2 | 3 | [[release-0.21.0]] 4 | == 0.21.0 (2025-11-05) 5 | 6 | === Additions 7 | 8 | * Add Pkl code formatting (https://github.com/apple/pkl-vscode/pull/64[#64], https://github.com/apple/pkl-vscode/pull/65[#65], https://github.com/apple/pkl-vscode/pull/67[#67]). 9 | * Add support for trailing commas (https://github.com/apple/pkl-vscode/pull/61[#61]). 10 | 11 | === Changes 12 | 13 | * Bump bundled version of pkl-lsp to 0.5.1 (https://github.com/apple/pkl-vscode/pull/66[#66], https://github.com/apple/pkl-vscode/pull/67[#67]). 14 | 15 | === Miscellaneous 16 | 17 | * Upgrade dependencies (https://github.com/apple/pkl-vscode/pull/62[#62]). 18 | 19 | === Contributors ❤️ 20 | 21 | Thank you to all of our contributors! 22 | 23 | * https://github.com/bioball[@bioball] 24 | * https://github.com/HT154[@HT154] 25 | 26 | [[release-0.20.0]] 27 | == 0.20.0 (2025-08-06) 28 | 29 | === Changes 30 | 31 | * Bump bundled version of pkl-lsp to 0.4.0 (https://github.com/apple/pkl-vscode/pull/57[#57]). 32 | * Fix auto-discovery of new LSP versions (https://github.com/apple/pkl-vscode/pull/57[#57]). 33 | 34 | === Contributors ❤️ 35 | 36 | Thank you to all of our contributors! 37 | 38 | * https://github.com/stackoverflow[@stackoverflow] 39 | 40 | [[release-0.19.0]] 41 | == 0.19.0 (2025-04-24) 42 | 43 | === Additions 44 | 45 | * Add configuration options to configure the LSP socket port and host (https://github.com/apple/pkl-vscode/pull/49[#49]). 46 | 47 | === Changes 48 | 49 | * Bump bundled version of pkl-lsp to 0.3.0 (https://github.com/apple/pkl-vscode/pull/54[#54]). 50 | * Consider any 0.x version of pkl-lsp as compatible (https://github.com/apple/pkl-vscode/pull/54[#54]). 51 | + 52 | This change means that pkl-vscode will auto-discover newer versions of pkl-lsp that are greater than version 0.3. 53 | * Improve syntax highlighting (https://github.com/apple/pkl-vscode/pull/50[#50]). 54 | 55 | === Miscellaneous 56 | 57 | * Change build to use vanilla `tsc` instead of `esbuild` (https://github.com/apple/pkl-vscode/pull/50[#50], https://github.com/apple/pkl-vscode/pull/52[#52]). 58 | * Use hawkeye to format/check license headers (https://github.com/apple/pkl-vscode/pull/48[#48]). 59 | * Documentation improvements (https://github.com/apple/pkl-vscode/pull/46[#46]). 60 | 61 | === Contributors ❤️ 62 | 63 | Thank you to all of our contributors! 64 | 65 | * https://github.com/KushalP[@KushalP] 66 | 67 | [[release-0.18.2]] 68 | == 0.18.2 (2024-12-20) 69 | 70 | === Fixes 71 | 72 | * Fix compatibility issues when running on Windows (https://github.com/apple/pkl-vscode/pull/41[#41]). 73 | 74 | === Contributors ❤️ 75 | 76 | Thank you to all of our contributors! 77 | 78 | * link:https://github.com/riccardopiola[@riccardopiola] 79 | 80 | [[release-0.18.1]] 81 | == 0.18.1 (2024-10-14) 82 | 83 | === Fixes 84 | 85 | * Fixes an issue where the extension incorrectly tells users about misconfigured settings (https://github.com/apple/pkl-vscode/pull/35[#35]). 86 | * Fixes an issue where the extension swallows errors coming from starting Pkl Language Server (https://github.com/apple/pkl-vscode/pull/38[#38]). 87 | 88 | [[release-0.18.0]] 89 | == 0.18.0 (2024-10-10) 90 | 91 | === Additions 92 | 93 | * Add support for xref:lsp:ROOT:index.adoc[pkl-lsp]. The editor now ships with a version of pkl-lsp, and queries for updates when the extension starts up. The LSP currently requires Java 22 to run. This requirement will go away when pkl-lsp ships native executables. By default, pkl-vscode will look for Java in `$PATH`, and will prompt if it cannot be found, or is lower than 22 (https://github.com/apple/pkl-vscode/pull/19[#19], https://github.com/apple/pkl-vscode/pull/21[#21], https://github.com/apple/pkl-vscode/pull/22[#22], https://github.com/apple/pkl-vscode/pull/23[#23], https://github.com/apple/pkl-vscode/pull/24[#24], https://github.com/apple/pkl-vscode/pull/25[#25], https://github.com/apple/pkl-vscode/pull/27[#27], https://github.com/apple/pkl-vscode/pull/28[#28], https://github.com/apple/pkl-vscode/pull/32[#32]). 94 | + 95 | These are the initial support features, and the list will grow over time as the LSP improves: 96 | 97 | ** Hover-over documentation 98 | ** Go-to-definition 99 | ** Project syncing 100 | ** Autocompletion 101 | ** Viewing stdlib, https, and package sources 102 | ** Resolving imports 103 | 104 | * Add command "Pkl: Sync projects". 105 | * Add new configuration items: 106 | ** `pkl.cli.path` 107 | ** `pkl.lsp.path` 108 | ** `pkl.lsp.java.path` 109 | ** `pkl.lsp.debug.port` 110 | 111 | === Changes 112 | 113 | * Improve syntax highlighting (https://github.com/apple/pkl-vscode/pull/30[#30]). 114 | 115 | === Miscellaneous 116 | 117 | * Change snippets from textmate snippets to pkl-lsp (https://github.com/apple/pkl-vscode/pull/25[#25]). 118 | * Rename some internal files (https://github.com/apple/pkl-vscode/pull/26[#26]). 119 | * Change some files to markdown (https://github.com/apple/pkl-vscode/pull/20[#20]). 120 | 121 | [[release-0.17.0]] 122 | == 0.17.0 (2024-03-12) 123 | 124 | === Miscellaneous 125 | 126 | * Changes Pkl TextMate grammar to link:https://github.com/apple/pkl.tmbundle[pkl.tmBundle] 127 | 128 | [[release-0.16.0]] 129 | == 0.16.0 (2024-02-28) 130 | 131 | === Fixes 132 | 133 | * Fixes folding ranges (link:https://github.com/apple/pkl-vscode/pull/6[#6]) 134 | * Fixes brace token matching (link:https://github.com/apple/pkl-vscode/pull/8[#8]) 135 | 136 | === Miscellaneous 137 | 138 | * Changes Pkl download link to GitHub (CI) (link:https://github.com/apple/pkl-vscode/pull/3[#3]) 139 | 140 | === Contributors ❤️ 141 | 142 | Thank you to all the contributors for this release! 143 | 144 | * link:https://github.com/RedCMD[@RedCMD] 145 | 146 | [[release-0.15.0]] 147 | == 0.15.0 (2024-02-02) 148 | 149 | Initial release 150 | -------------------------------------------------------------------------------- /src/ts/providers/PklSemanticTokensProvider.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | //===----------------------------------------------------------------------===// 18 | // Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. 19 | // 20 | // Licensed under the Apache License, Version 2.0 (the "License"); 21 | // you may not use this file except in compliance with the License. 22 | // You may obtain a copy of the License at 23 | // 24 | // https://www.apache.org/licenses/LICENSE-2.0 25 | // 26 | // Unless required by applicable law or agreed to in writing, software 27 | // distributed under the License is distributed on an "AS IS" BASIS, 28 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29 | // See the License for the specific language governing permissions and 30 | // limitations under the License. 31 | //===----------------------------------------------------------------------===// 32 | 33 | import vscode from "vscode"; 34 | import { Parser, Query, Tree, Language } from "web-tree-sitter"; 35 | import fs from "fs/promises"; 36 | import { readFileSync } from "fs"; 37 | import path from "path"; 38 | 39 | const foldsQueries = readFileSync(path.join(__dirname, "../../queries/folds.scm"), { 40 | encoding: "utf-8", 41 | }); 42 | 43 | const highlightsQueries = readFileSync(path.join(__dirname, "../../queries/highlights.scm"), { 44 | encoding: "utf-8", 45 | }); 46 | 47 | export class PklSemanticTokensProvider 48 | implements vscode.DocumentSemanticTokensProvider, vscode.FoldingRangeProvider 49 | { 50 | #previousTrees: Map = new Map(); 51 | 52 | #parser: Parser; 53 | 54 | #highlightsQuery: Query; 55 | 56 | #foldsQuery: Query; 57 | 58 | legend: vscode.SemanticTokensLegend; 59 | 60 | constructor(parser: Parser) { 61 | this.#parser = parser; 62 | this.#highlightsQuery = new Query(parser.language!!, highlightsQueries); 63 | this.#foldsQuery = new Query(parser.language!!, foldsQueries); 64 | this.legend = this.#buildLegend(); 65 | } 66 | 67 | #buildLegend() { 68 | const tokenTypes: string[] = []; 69 | const tokenModifiers: string[] = []; 70 | for (const capture of this.#highlightsQuery.captureNames) { 71 | const [type, ...modifiers] = capture.split("."); 72 | if (!tokenTypes.includes(type)) { 73 | tokenTypes.push(type); 74 | } 75 | for (const modifier of modifiers) { 76 | if (!tokenModifiers.includes(modifier)) { 77 | tokenModifiers.push(modifier); 78 | } 79 | } 80 | } 81 | return new vscode.SemanticTokensLegend(tokenTypes, tokenModifiers); 82 | } 83 | 84 | #parse(document: vscode.TextDocument): Tree | null { 85 | const previousParse = this.#previousTrees.get(document); 86 | if (previousParse && previousParse.version === document.version) { 87 | return previousParse.tree; 88 | } 89 | const tree = this.#parser.parse(document.getText()); 90 | if (tree == null) { 91 | return null; 92 | } 93 | this.#previousTrees.set(document, { version: document.version, tree }); 94 | return tree; 95 | } 96 | 97 | provideDocumentSemanticTokens( 98 | document: vscode.TextDocument, 99 | _: vscode.CancellationToken, 100 | ): vscode.ProviderResult { 101 | const tree = this.#parse(document); 102 | if (tree == null) { 103 | return null; 104 | } 105 | const captures = this.#highlightsQuery.captures(tree.rootNode); 106 | const builder = new vscode.SemanticTokensBuilder(this.legend); 107 | 108 | for (const capture of captures) { 109 | const [term, ...modifiers] = capture.name.split("."); 110 | const node = capture.node; 111 | // A token can't span multiple lines. 112 | // We'll have to rely on our textmate grammar here. 113 | if (node.endPosition.row > node.startPosition.row) { 114 | continue; 115 | } 116 | const range = new vscode.Range( 117 | new vscode.Position(node.startPosition.row, node.startPosition.column), 118 | new vscode.Position(node.endPosition.row, node.endPosition.column), 119 | ); 120 | builder.push(range, term, modifiers); 121 | } 122 | const tokens = builder.build(); 123 | return tokens; 124 | } 125 | 126 | provideFoldingRanges( 127 | document: vscode.TextDocument, 128 | context: vscode.FoldingContext, 129 | token: vscode.CancellationToken, 130 | ): vscode.ProviderResult { 131 | const tree = this.#parse(document); 132 | if (tree == null) { 133 | return null; 134 | } 135 | const captures = this.#foldsQuery.captures(tree.rootNode); 136 | return captures 137 | .filter((it) => it.node.endPosition.row > it.node.startPosition.row) 138 | .map((it) => { 139 | return new vscode.FoldingRange(it.node.startPosition.row, it.node.endPosition.row); 140 | }); 141 | } 142 | } 143 | 144 | export async function newPklSemanticTokenProvider(): Promise { 145 | await Parser.init(); 146 | const parser = new Parser(); 147 | const wasmBytes = await fs.readFile(path.join(__dirname, "../pkl.wasm")); 148 | const language = await Language.load(wasmBytes); 149 | parser.setLanguage(language); 150 | return new PklSemanticTokensProvider(parser); 151 | } 152 | -------------------------------------------------------------------------------- /src/ts/pklLspDistribution.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import path from "node:path"; 18 | import config from "./config"; 19 | import { getJavaDistribution } from "./javaDistribution"; 20 | import { debounce, execFile, isRegularFile } from "./utils"; 21 | import Semver from "./Semver"; 22 | import * as vscode from "vscode"; 23 | import fs from "fs/promises"; 24 | import { 25 | BUNDLED_LSP_VERSION, 26 | COMMAND_OPEN_WORKSPACE_SETTINGS, 27 | CONFIG_LSP_PATH, 28 | LSP_DISTRIBUTIONS_DIR, 29 | } from "./consts"; 30 | import logger from "./clients/logger"; 31 | 32 | const emitter = new vscode.EventEmitter(); 33 | 34 | export const onDidChangeLspDistribution = emitter.event; 35 | 36 | export let currentLspDistribution: LspDistribution | undefined = undefined; 37 | 38 | export type LspDistribution = { 39 | path: string; 40 | version: Semver; 41 | }; 42 | 43 | export const getLspDistribution = (): Promise => { 44 | return new Promise((resolve) => { 45 | if (currentLspDistribution !== undefined) { 46 | resolve(currentLspDistribution); 47 | return; 48 | } 49 | const disposables: vscode.Disposable[] = []; 50 | onDidChangeLspDistribution( 51 | (distribution) => { 52 | resolve(distribution); 53 | disposables.every((it) => it.dispose()); 54 | }, 55 | null, 56 | disposables, 57 | ); 58 | }); 59 | }; 60 | 61 | export const bundledDistribution: LspDistribution = { 62 | path: path.join(__dirname, "pkl-lsp.jar"), 63 | version: Semver.parse(BUNDLED_LSP_VERSION)!!, 64 | }; 65 | 66 | const getLspVersion = async (jarPath: string): Promise => { 67 | const javaDistribution = await getJavaDistribution(); 68 | const { stdout } = await execFile(javaDistribution.path, ["-jar", jarPath, "--version"]); 69 | const stdoutParts = stdout.replace(/\r?\n$/, "").split(" version "); 70 | const versionStr = stdoutParts[stdoutParts.length - 1]; 71 | if (versionStr === undefined) { 72 | logger.log( 73 | `Got malformed version output from jar file at ${jarPath}: ${stdout}. Expected "pkl-lsp version "`, 74 | ); 75 | return; 76 | } 77 | const semver = Semver.parse(versionStr); 78 | if (semver === undefined) { 79 | logger.log(`Got malformed semver string from jar file at ${jarPath}: ${versionStr}`); 80 | return; 81 | } 82 | return semver; 83 | }; 84 | 85 | const CTA_CONFIGURE_LSP_PATH = "Configure path to pkl-lsp"; 86 | 87 | const tellInvalidConfiguredLspPath = async () => { 88 | const response = await vscode.window.showWarningMessage( 89 | `Configured path ${config.lspPath} is not a valid lsp jar.`, 90 | CTA_CONFIGURE_LSP_PATH, 91 | ); 92 | if (response === CTA_CONFIGURE_LSP_PATH) { 93 | vscode.commands.executeCommand(COMMAND_OPEN_WORKSPACE_SETTINGS, CONFIG_LSP_PATH); 94 | } 95 | }; 96 | 97 | const handleConfiguredLspDistribution = async (lspPath: string) => { 98 | try { 99 | const version = await getLspVersion(lspPath); 100 | if (version === undefined) { 101 | tellInvalidConfiguredLspPath(); 102 | return; 103 | } 104 | // permit a higher version if it exists, but warn users about it. 105 | if (!version.isCompatibleWith(bundledDistribution.version)) { 106 | vscode.window.showWarningMessage( 107 | `This version of pkl-vscode is not compatible with pkl-lsp version ${version}. Features are not guaranteed to work.`, 108 | ); 109 | } else if (version.isLessThan(bundledDistribution.version)) { 110 | vscode.window.showWarningMessage( 111 | `The configured pkl-lsp distribution version (${version}) is lower than the bundled version (${BUNDLED_LSP_VERSION}). Features are not guaranteed to work.`, 112 | ); 113 | } 114 | const distro = { path: lspPath, version }; 115 | logger.log(`Using pkl-lsp.jar from configured ${CONFIG_LSP_PATH}`); 116 | currentLspDistribution = distro; 117 | emitter.fire(distro); 118 | } catch (err) { 119 | tellInvalidConfiguredLspPath(); 120 | } 121 | }; 122 | 123 | /** 124 | * Get the highest supported pkl-lsp distribution. 125 | */ 126 | const getDownloadedDistribution = async (): Promise => { 127 | try { 128 | const distroFolders = await fs.readdir(LSP_DISTRIBUTIONS_DIR); 129 | const versions = distroFolders 130 | .map(Semver.parse) 131 | .filter( 132 | (it): it is Semver => it !== undefined && it.isCompatibleWith(bundledDistribution.version), 133 | ) 134 | .sort((a, b) => -a.compareTo(b)); 135 | for (const version of versions) { 136 | const lspJar = path.join(LSP_DISTRIBUTIONS_DIR, version.toString(), `pkl-lsp-${version}.jar`); 137 | if (await isRegularFile(lspJar)) { 138 | return { path: lspJar, version }; 139 | } 140 | } 141 | } catch (err) { 142 | return; 143 | } 144 | }; 145 | 146 | vscode.workspace.onDidChangeConfiguration( 147 | // debounce because vscode fires configuration changes _as_ users are typing. 148 | debounce(async (event: vscode.ConfigurationChangeEvent) => { 149 | if (!event.affectsConfiguration(CONFIG_LSP_PATH) || config.lspPath === undefined) { 150 | return; 151 | } 152 | handleConfiguredLspDistribution(config.lspPath); 153 | }, 5000), 154 | ); 155 | 156 | (async () => { 157 | if (config.lspPath !== undefined) { 158 | await handleConfiguredLspDistribution(config.lspPath); 159 | // currentLspDistribution only gets set if it was a valid distribution. 160 | if (currentLspDistribution !== undefined) { 161 | return; 162 | } 163 | } 164 | let distro = await getDownloadedDistribution(); 165 | if (distro !== undefined && distro.version.isGreaterThan(bundledDistribution.version)) { 166 | logger.log(`Using downloaded pkl-lsp.jar at ${distro.path}`); 167 | } else { 168 | distro = bundledDistribution; 169 | logger.log(`Using built-in pkl-lsp.jar`); 170 | } 171 | currentLspDistribution = distro; 172 | emitter.fire(distro); 173 | })(); 174 | -------------------------------------------------------------------------------- /src/ts/javaDistribution.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import fs from "node:fs/promises"; 18 | import path from "node:path"; 19 | import * as vscode from "vscode"; 20 | import { debounce, execFile } from "./utils"; 21 | import { COMMAND_OPEN_WORKSPACE_SETTINGS, CONFIG_JAVA_PATH } from "./consts"; 22 | import config from "./config"; 23 | import logger from "./clients/logger"; 24 | 25 | const emitter = new vscode.EventEmitter(); 26 | 27 | const MINIMUM_JAVA_VERSION = 22; 28 | 29 | /** 30 | * The currently configured Java distribution, if any. 31 | */ 32 | export let currentJavaDistribution: JavaDistribution | undefined = undefined; 33 | 34 | /** 35 | * Fires when the java distribution changes due to users changing it in workspace/user settings. 36 | * 37 | * Also fires on startup if there is a suitable Java distribution. 38 | * 39 | * Excludes Java versions that are incompatible. 40 | */ 41 | export const onDidChangeJavaDistribution = emitter.event; 42 | 43 | export const getJavaDistribution = (): Promise => { 44 | return new Promise((resolve) => { 45 | if (currentJavaDistribution !== undefined) { 46 | resolve(currentJavaDistribution); 47 | return; 48 | } 49 | const disposables: vscode.Disposable[] = []; 50 | onDidChangeJavaDistribution( 51 | (distribution) => { 52 | resolve(distribution); 53 | disposables.every((it) => it.dispose()); 54 | }, 55 | null, 56 | disposables, 57 | ); 58 | }); 59 | }; 60 | 61 | export type JavaDistribution = { 62 | path: string; 63 | version: number; 64 | }; 65 | 66 | const resolveJava = async (path: string): Promise => { 67 | try { 68 | const stats = await fs.stat(path); 69 | if (!stats.isFile()) { 70 | return null; 71 | } 72 | const result = await execFile(path, ["-version"]); 73 | const { stderr } = result; 74 | const versionStr = stderr.split('"')[1]; 75 | if (versionStr == null) { 76 | logger.warn(`Unexpected version string: ${stderr}`); 77 | return null; 78 | } 79 | const majorVersion = versionStr.split(".")[0]; 80 | if (majorVersion == null) { 81 | logger.warn(`Malformed version: ${versionStr}`); 82 | return null; 83 | } 84 | var version = parseInt(majorVersion); 85 | return { path, version }; 86 | } catch (err: any) { 87 | if (err.code !== "ENOENT") { 88 | logger.warn(`Received unexpected error when spawning java: ${err}`); 89 | } 90 | return null; 91 | } 92 | }; 93 | 94 | const extEnvVar = process.env.PATHEXT ?? ""; 95 | const possibleJavaFilenames = extEnvVar.split(path.delimiter).map((it) => `java${it}`); 96 | 97 | const findJavaInDir = async (dir: string) => { 98 | for (const filename of possibleJavaFilenames) { 99 | const candidate = path.join(dir, filename); 100 | const java = await resolveJava(candidate); 101 | if (java != null) { 102 | return java; 103 | } 104 | } 105 | return null; 106 | }; 107 | 108 | const CTA_CONFIGURE_JAVA_PATH = "Configure path to Java"; 109 | 110 | /** 111 | * Find Java from either `$JAVA_HOME`, or `$PATH`. 112 | */ 113 | const findJavaFromSystem = async () => { 114 | const javaHome = process.env.JAVA_HOME; 115 | if (javaHome != null) { 116 | var distro = await findJavaInDir(path.join(javaHome, "bin")); 117 | if (distro != null && distro.version >= MINIMUM_JAVA_VERSION) { 118 | emitter.fire(distro); 119 | return; 120 | } 121 | } 122 | const pathEnvVar = process.env.PATH; 123 | if (pathEnvVar != null) { 124 | for (const pathStr of pathEnvVar.split(path.delimiter)) { 125 | const distro = await findJavaInDir(pathStr); 126 | if (distro != null && distro.version >= MINIMUM_JAVA_VERSION) { 127 | emitter.fire(distro); 128 | return; 129 | } 130 | } 131 | } 132 | const response = await vscode.window.showWarningMessage( 133 | `Cannot find suitable Java in $PATH or $JAVA_HOME. pkl-vscode requires Java ${MINIMUM_JAVA_VERSION} or higher.`, 134 | CTA_CONFIGURE_JAVA_PATH, 135 | ); 136 | if (response === CTA_CONFIGURE_JAVA_PATH) { 137 | vscode.commands.executeCommand(COMMAND_OPEN_WORKSPACE_SETTINGS, CONFIG_JAVA_PATH); 138 | } 139 | }; 140 | 141 | const handleConfiguredJavaPath = async (path: string) => { 142 | const distribution = await resolveJava(path); 143 | if (distribution === null) { 144 | vscode.window 145 | .showWarningMessage( 146 | `Could not resolve Java version information from ${config.javaPath}. Ensure it is the path to the Java executable.`, 147 | CTA_CONFIGURE_JAVA_PATH, 148 | ) 149 | .then((response) => { 150 | if (response === CTA_CONFIGURE_JAVA_PATH) { 151 | vscode.commands.executeCommand(COMMAND_OPEN_WORKSPACE_SETTINGS, CONFIG_JAVA_PATH); 152 | } 153 | }); 154 | return; 155 | } 156 | if (distribution.version < MINIMUM_JAVA_VERSION) { 157 | vscode.window 158 | .showWarningMessage( 159 | `pkl-vscode requires Java ${MINIMUM_JAVA_VERSION} or higher, but was configured to use version ${distribution.version} in ${CONFIG_JAVA_PATH}`, 160 | CTA_CONFIGURE_JAVA_PATH, 161 | ) 162 | .then((response) => { 163 | if (response === CTA_CONFIGURE_JAVA_PATH) { 164 | vscode.commands.executeCommand(COMMAND_OPEN_WORKSPACE_SETTINGS, CONFIG_JAVA_PATH); 165 | } 166 | }); 167 | return; 168 | } 169 | currentJavaDistribution = distribution; 170 | emitter.fire(distribution); 171 | }; 172 | 173 | if (config.javaPath === undefined) { 174 | findJavaFromSystem(); 175 | } else { 176 | handleConfiguredJavaPath(config.javaPath); 177 | } 178 | 179 | vscode.workspace.onDidChangeConfiguration( 180 | // debounce because vscode fires configuration changes _as_ users are typing. 181 | debounce(async (event: vscode.ConfigurationChangeEvent) => { 182 | if (!event.affectsConfiguration(CONFIG_JAVA_PATH) || config.javaPath === undefined) { 183 | return; 184 | } 185 | handleConfiguredJavaPath(config.javaPath); 186 | }, 5000), 187 | ); 188 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pkl-vscode", 3 | "displayName": "Pkl", 4 | "description": "Syntax highlighting, bracket matching, and code folding for Pkl files.", 5 | "icon": "img/icon.png", 6 | "version": "0.21.0", 7 | "publisher": "Pkl", 8 | "license": "Apache-2.0", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/apple/pkl-vscode" 12 | }, 13 | "homepage": "https://github.com/apple/pkl-vscode", 14 | "bugs": { 15 | "url": "https://github.com/apple/pkl-vscode/issues" 16 | }, 17 | "//": "don't publish to npm", 18 | "private": true, 19 | "engines": { 20 | "vscode": "^1.59.0" 21 | }, 22 | "devEngines": { 23 | "packageManager": { 24 | "name": "npm" 25 | } 26 | }, 27 | "categories": [ 28 | "Programming Languages" 29 | ], 30 | "main": "./out/extension.js", 31 | "activationEvents": [ 32 | "onLanguage:pkl" 33 | ], 34 | "contributes": { 35 | "languages": [ 36 | { 37 | "id": "pkl", 38 | "extensions": [ 39 | ".pkl", 40 | ".pcf" 41 | ], 42 | "filenames": [ 43 | "PklProject" 44 | ], 45 | "aliases": [ 46 | "Pkl" 47 | ], 48 | "firstLine": "^#!/.*\\bpkl\\b", 49 | "configuration": "./language-configuration.json", 50 | "icon": { 51 | "light": "./img/icon.svg", 52 | "dark": "./img/icon.svg" 53 | } 54 | } 55 | ], 56 | "grammars": [ 57 | { 58 | "language": "pkl", 59 | "scopeName": "source.pkl", 60 | "path": "out/pkl.tmLanguage.json" 61 | } 62 | ], 63 | "semanticTokenTypes": [ 64 | { 65 | "id": "punctuation", 66 | "description": "Punctuation symbols" 67 | }, 68 | { 69 | "id": "string-escape", 70 | "description": "String escape characters" 71 | }, 72 | { 73 | "id": "constant", 74 | "description": "Constants built into the language, such as booleans or nulls" 75 | }, 76 | { 77 | "id": "error", 78 | "description": "Invalid parsing sequences" 79 | } 80 | ], 81 | "semanticTokenScopes": [ 82 | { 83 | "scopes": { 84 | "keyword": [ 85 | "keyword.pkl" 86 | ], 87 | "punctuation": [ 88 | "punctuation.pkl" 89 | ], 90 | "punctuationBracket": [ 91 | "punctuation.bracket.pkl" 92 | ], 93 | "control": [ 94 | "keyword.control.pkl" 95 | ], 96 | "error": [ 97 | "invalid.illegal.pkl" 98 | ], 99 | "stringEscape": [ 100 | "constant.character.escape.pkl" 101 | ], 102 | "constant": [ 103 | "constant.character.language.pkl" 104 | ] 105 | } 106 | } 107 | ], 108 | "configuration": { 109 | "title": "Pkl", 110 | "properties": { 111 | "pkl.lsp.path": { 112 | "type": "string", 113 | "default": null, 114 | "description": "Path to pkl-lsp jar file. This is an internal developer option." 115 | }, 116 | "pkl.lsp.debug.port": { 117 | "type": "integer", 118 | "default": 5005, 119 | "description": "Debug port to launch pkl-lsp. This is an internal developer option." 120 | }, 121 | "pkl.lsp.socket.port": { 122 | "type": "number", 123 | "description": "Port of the LSP socket to talk to. Setting this takes precedence over `pkl.lsp.path`. This is an internal developer option." 124 | }, 125 | "pkl.lsp.socket.host": { 126 | "type": "string", 127 | "description": "Host of the LSP socket to talk to. If unset or empty, defaults to localhost. This is an internal developer option." 128 | }, 129 | "pkl.cli.path": { 130 | "type": "string", 131 | "default": null, 132 | "description": "Path to pkl executable", 133 | "scope": "Pkl" 134 | }, 135 | "pkl.lsp.java.path": { 136 | "type": "string", 137 | "default": null, 138 | "description": "Path to java executable used to start pkl-lsp." 139 | }, 140 | "pkl.formatter.grammarVersion": { 141 | "type": "string", 142 | "default": "2", 143 | "description": "Grammar version to use when formatting Pkl code", 144 | "enum": ["1", "2"], 145 | "enumDescriptions": [ 146 | "Pkl 0.25 - 0.29", 147 | "Pkl 0.30+" 148 | ] 149 | } 150 | } 151 | }, 152 | "commands": [ 153 | { 154 | "category": "Pkl", 155 | "title": "Sync Projects", 156 | "command": "pkl.syncProjects" 157 | } 158 | ] 159 | }, 160 | "scripts": { 161 | "clean": "shx rm -rf out/", 162 | "build": "npm run clean && npm run build:pkl && npm run build:code && npm run build:tree-sitter && npm run build:download-lsp", 163 | "build:local": "npm run clean && npm run build:pkl && npm run build:code && npm run build:tree-sitter:local && npm run build:download-lsp", 164 | "build:pkl": "pkl eval -m . src/pkl/index.pkl", 165 | "build:download-lsp": "ts-node scripts/download-lsp-jar.ts", 166 | "build:tree-sitter": "shx mkdir -p out/grammar/ && cd node_modules/@apple/tree-sitter-pkl && tree-sitter build --wasm && shx mv tree-sitter-pkl.wasm ../../../out/pkl.wasm", 167 | "build:tree-sitter:local": "shx mkdir -p out/grammar/ && cd node_modules/@apple/tree-sitter-pkl && tree-sitter build --wasm && cd - && mv node_modules/@apple/tree-sitter-pkl/tree-sitter-pkl.wasm out/pkl.wasm", 168 | "build:code": "shx mkdir -p out/ && shx cp node_modules/web-tree-sitter/tree-sitter.wasm out/ && tsc", 169 | "lint:fix": "prettier -w src/", 170 | "lint": "prettier -c src/", 171 | "watch": "tsc --watch", 172 | "test": "npm run test:grammar", 173 | "test:grammar": "sh scripts/check-grammar.sh", 174 | "prepackage": "npm run build", 175 | "package": "shx mkdir -p .dist/vscode && vsce package --out .dist/vscode/pkl-vscode-$npm_package_version.vsix", 176 | "package-only": "shx mkdir -p .dist/vscode && vsce package --out .dist/vscode/pkl-vscode-$npm_package_version.vsix", 177 | "preinstallextension": "npm run package", 178 | "installextension": "code --install-extension .dist/vscode/pkl-vscode-$npm_package_version.vsix", 179 | "clone-grammar": "git submodule update --init --recursive" 180 | }, 181 | "dependencies": { 182 | "vscode-languageclient": "^9.0.1", 183 | "web-tree-sitter": "^0.25.10" 184 | }, 185 | "devDependencies": { 186 | "@apple/tree-sitter-pkl": "^0.20.0", 187 | "@types/node": "^24.10.0", 188 | "@types/vscode": "^1.59.0", 189 | "prettier": "^3.6.2", 190 | "shx": "^0.4.0", 191 | "tree-sitter-cli": "^0.25.10", 192 | "ts-node": "^10.9.2", 193 | "tslint": "^6.1.3", 194 | "typescript": "^5.9.3", 195 | "@vscode/vsce": "^3.6.2", 196 | "@vscode/test-electron": "^2.5.2" 197 | }, 198 | "peerDependencies": { 199 | "tree-sitter": "^0.25.0" 200 | }, 201 | "peerDependenciesMeta": { 202 | "tree-sitter": { 203 | "optional": true 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/ts/extension.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as vscode from "vscode"; 18 | import { newPklSemanticTokenProvider } from "./providers/PklSemanticTokensProvider"; 19 | import { 20 | LanguageClient, 21 | LanguageClientOptions, 22 | ServerOptions, 23 | StreamInfo, 24 | } from "vscode-languageclient/node"; 25 | import { registerNotificationHandlers } from "./notifications"; 26 | import { 27 | COMMAND_DOWNLOAD_PACKAGE, 28 | COMMAND_OPEN_WORKSPACE_SETTINGS, 29 | COMMAND_PKL_CONFIGURE, 30 | COMMAND_PKL_OPEN_FILE, 31 | COMMAND_RELOAD_WORKSPACE_WINDOW, 32 | COMMAND_SYNC_PROJECTS, 33 | CONFIG_JAVA_PATH, 34 | CONFIG_LSP_PATH, 35 | } from "./consts"; 36 | import config from "./config"; 37 | import { pklDownloadPackageRequest, pklSyncProjectsRequest } from "./requests"; 38 | import PklTextDocumentContentProvider from "./providers/PklTextDocumentContentProvider"; 39 | import { getJavaDistribution, onDidChangeJavaDistribution } from "./javaDistribution"; 40 | import { getLspDistribution, onDidChangeLspDistribution } from "./pklLspDistribution"; 41 | import { queryForLatestLspDistribution } from "./pklLspDistributionUpdater"; 42 | import logger from "./clients/logger"; 43 | import net from "net"; 44 | 45 | export type LanguageClientRef = { 46 | client?: LanguageClient; 47 | }; 48 | 49 | let languageClientRef: LanguageClientRef = {}; 50 | 51 | async function getStreamInfo(): Promise { 52 | const socketPort = config.lspSocketPort!!; 53 | const socketHost = config.lspSocketHost || "localhost"; 54 | logger.log(`Connecting to socket ${socketHost}:${socketPort}`); 55 | const socket = net.createConnection(socketPort, config.lspSocketHost); 56 | await new Promise((resolve) => socket.once("connect", resolve)); 57 | logger.log(`Connected to socket ${socketHost}:${socketPort}`); 58 | return { 59 | reader: socket, 60 | writer: socket, 61 | detached: true, 62 | }; 63 | } 64 | 65 | async function getServerOptions(): Promise { 66 | if (config.lspSocketPort) { 67 | return getStreamInfo; 68 | } 69 | const [javaDistribution, lspDistribution] = await Promise.all([ 70 | getJavaDistribution(), 71 | getLspDistribution(), 72 | ]); 73 | return { 74 | run: { 75 | command: javaDistribution.path, 76 | args: ["-jar", lspDistribution.path], 77 | options: {}, 78 | }, 79 | debug: { 80 | command: javaDistribution.path, 81 | args: [ 82 | `-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,quiet=y,address=*:${config.lspDebugPort}`, 83 | "-jar", 84 | lspDistribution.path, 85 | "--verbose", 86 | ], 87 | options: {}, 88 | }, 89 | }; 90 | } 91 | 92 | async function createLanguageClient() { 93 | const serverOptions = await getServerOptions(); 94 | const clientOptions: LanguageClientOptions = { 95 | documentSelector: [ 96 | { scheme: "file", language: "pkl" }, 97 | { scheme: "pkl-lsp", language: "pkl" }, 98 | ], 99 | markdown: { 100 | isTrusted: true, 101 | }, 102 | initializationOptions: { 103 | renderOpenFileCommandInDocs: true, 104 | extendedClientCapabilities: { 105 | actionableRuntimeNotifications: true, 106 | pklConfigureCommand: true, 107 | }, 108 | }, 109 | }; 110 | return new LanguageClient("Pkl", "Pkl Language Server", serverOptions, clientOptions); 111 | } 112 | 113 | async function nofityReloadNeeded() { 114 | const response = await vscode.window.showInformationMessage( 115 | "The java path has changed, and the VSCode window needs to be reloaded to take effect.", 116 | "Reload Window", 117 | ); 118 | if (response === "Reload Window") { 119 | vscode.commands.executeCommand(COMMAND_RELOAD_WORKSPACE_WINDOW); 120 | } 121 | } 122 | 123 | async function startLspServer() { 124 | if (languageClientRef.client?.needsStop() === true) { 125 | // Calling `LanguageClient#stop()` causes all sorts of havoc for some reason, so we'll just ask users to reload the window. 126 | nofityReloadNeeded(); 127 | return; 128 | } 129 | logger.log("Starting language server"); 130 | const client = await createLanguageClient(); 131 | languageClientRef.client = client; 132 | await client.start(); 133 | registerNotificationHandlers(client); 134 | } 135 | 136 | async function registerSubscriptions(context: vscode.ExtensionContext) { 137 | const semanticTokensProvider = await newPklSemanticTokenProvider(); 138 | 139 | context.subscriptions.push( 140 | vscode.languages.registerDocumentSemanticTokensProvider( 141 | { language: "pkl" }, 142 | semanticTokensProvider, 143 | semanticTokensProvider.legend, 144 | ), 145 | vscode.languages.registerFoldingRangeProvider({ language: "pkl" }, semanticTokensProvider), 146 | ); 147 | 148 | context.subscriptions.push( 149 | vscode.workspace.registerTextDocumentContentProvider( 150 | "pkl-lsp", 151 | new PklTextDocumentContentProvider(languageClientRef), 152 | ), 153 | ); 154 | 155 | context.subscriptions.push( 156 | vscode.commands.registerCommand( 157 | COMMAND_PKL_OPEN_FILE, 158 | async (path: string, maybeLine: number | undefined, maybeCol: number | undefined) => { 159 | const parsedUri = vscode.Uri.parse(path); 160 | const editor = await vscode.window.showTextDocument(parsedUri); 161 | 162 | let line = maybeLine ?? 0; 163 | if (Number.isNaN(line)) { 164 | line = 1; 165 | } 166 | let col = maybeCol ?? 0; 167 | if (Number.isNaN(col)) { 168 | col = 1; 169 | } 170 | const pos = new vscode.Position(line - 1, col - 1); 171 | 172 | const range = new vscode.Range(pos, pos); 173 | editor.revealRange(range, vscode.TextEditorRevealType.AtTop); 174 | editor.selections = [new vscode.Selection(pos, pos)]; 175 | }, 176 | ), 177 | ); 178 | 179 | context.subscriptions.push( 180 | vscode.commands.registerCommand(COMMAND_DOWNLOAD_PACKAGE, async (packageUri: string) => { 181 | if (languageClientRef.client === undefined) { 182 | return; 183 | } 184 | await languageClientRef.client.sendRequest(pklDownloadPackageRequest, packageUri); 185 | }), 186 | ); 187 | 188 | context.subscriptions.push( 189 | vscode.commands.registerCommand(COMMAND_SYNC_PROJECTS, async () => { 190 | if (languageClientRef.client === undefined) { 191 | return; 192 | } 193 | await languageClientRef.client.sendRequest(pklSyncProjectsRequest, null); 194 | }), 195 | ); 196 | 197 | context.subscriptions.push( 198 | vscode.commands.registerCommand(COMMAND_PKL_CONFIGURE, async (configurationPath: string) => { 199 | await vscode.commands.executeCommand(COMMAND_OPEN_WORKSPACE_SETTINGS, configurationPath); 200 | }), 201 | ); 202 | } 203 | 204 | const showRestartMessage = (configPath: string) => async () => { 205 | if (languageClientRef.client?.needsStop() === true) { 206 | const response = await vscode.window.showInformationMessage( 207 | `The configuration value "${configPath}" has changed, and the VSCode window needs to be reloaded to take effect.`, 208 | "Reload Window", 209 | ); 210 | // Calling `LanguageClient#stop()` causes all sorts of havoc for some reason. 211 | if (response === "Reload Window") { 212 | vscode.commands.executeCommand(COMMAND_RELOAD_WORKSPACE_WINDOW); 213 | return; 214 | } 215 | } 216 | }; 217 | 218 | export async function activate(context: vscode.ExtensionContext) { 219 | await registerSubscriptions(context); 220 | await startLspServer(); 221 | onDidChangeJavaDistribution(showRestartMessage(CONFIG_JAVA_PATH)); 222 | onDidChangeLspDistribution(showRestartMessage(CONFIG_LSP_PATH)); 223 | queryForLatestLspDistribution(); 224 | } 225 | 226 | export function deactivate(): Thenable | undefined { 227 | if (languageClientRef.client?.needsStop() === true) { 228 | return languageClientRef.client.stop(); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------