├── .editorconfig ├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ ├── cd.yml │ ├── ci.yml │ ├── publish.yml │ └── test-results.yml ├── .gitignore ├── .prettierrc ├── .vscode-test.js ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── build.ts ├── docs └── troubleshoot.md ├── gulpfile.js ├── package-lock.json ├── package.json ├── src ├── bazelLangaugeServerTerminal.ts ├── bazelRunStatus.ts ├── bazelTaskManager.ts ├── bazelTerminal.ts ├── bazelprojectparser.ts ├── buildifier.ts ├── commands.ts ├── extension.api.ts ├── extension.ts ├── jdtls.extension.api.ts ├── loggingTCPServer.ts ├── projectViewManager.ts ├── provider │ ├── bazelRunTargetProvider.ts │ ├── bazelSyncStatusProvider.ts │ └── bazelTaskProvider.ts ├── resources │ ├── dark │ │ ├── error.svg │ │ ├── pass.svg │ │ └── warning.svg │ └── light │ │ ├── error.svg │ │ ├── pass.svg │ │ └── warning.svg ├── tsconfig.json ├── types.d.ts └── util.ts ├── syntaxes ├── bazelproject-language-configuration.json ├── bazelproject.tmLanguage.json ├── starlark-language-configuration.json └── starlark.tmLanguage.json ├── test ├── projects │ └── small │ │ ├── .bazelrc │ │ ├── .buildifier-tables.json │ │ ├── .buildifier.json │ │ ├── .gitignore │ │ ├── .vscode │ │ └── settings.json │ │ ├── MODULE.bazel │ │ ├── MODULE.bazel.lock │ │ ├── WORKSPACE │ │ ├── module1 │ │ ├── BUILD │ │ └── java │ │ │ ├── resources │ │ │ └── hello.txt │ │ │ └── src │ │ │ └── hello │ │ │ └── Hello.java │ │ ├── module2 │ │ ├── BUILD │ │ └── java │ │ │ ├── src │ │ │ └── library │ │ │ │ └── Greeting.java │ │ │ └── test │ │ │ └── library │ │ │ └── GreetingTest.java │ │ ├── module3 │ │ ├── BUILD │ │ └── java │ │ │ └── src │ │ │ └── log │ │ │ └── Logger.java │ │ ├── third_party │ │ └── maven │ │ │ ├── BUILD │ │ │ └── dependencies.bzl │ │ └── tools │ │ └── intellij │ │ └── runConfigurations │ │ └── Custom_Bazel_Target.xml ├── runTest.ts ├── suite │ ├── Jdtls.ts │ ├── extension.test.ts │ └── index.ts └── tsconfig.json └── tsconfig.base.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Tab indentation 2 | [*] 3 | indent_style = tab 4 | indent_size = 4 5 | trim_trailing_whitespace = true 6 | 7 | [{package.json}] 8 | indent_style = space 9 | indent_size = 4 10 | 11 | [*.yml] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 2020, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint", 10 | "prettier" 11 | ], 12 | "extends": [ 13 | "plugin:@typescript-eslint/recommended", 14 | "plugin:prettier/recommended" 15 | ], 16 | "rules": { 17 | "@typescript-eslint/naming-convention": [ 18 | "warn", 19 | { "selector": "default", "format": ["camelCase", "PascalCase", "UPPER_CASE"], "leadingUnderscore": "allow" } 20 | ], 21 | "@typescript-eslint/semi": "warn", 22 | "curly": "warn", 23 | "eqeqeq": "warn", 24 | "no-throw-literal": "warn", 25 | "semi": "off", 26 | "prettier/prettier": "warn", 27 | "@typescript-eslint/no-namespace": "off", 28 | "@typescript-eslint/ban-types": "off", 29 | "@typescript-eslint/no-explicit-any": "off", 30 | "@typescript-eslint/no-unused-vars": "off" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | directory: "/" # search in .github/workflows under root `/` 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["CI"] 6 | types: 7 | - completed 8 | branches: 9 | - main 10 | 11 | permissions: {} 12 | 13 | jobs: 14 | publish-latest-update-site: 15 | name: Publish Latest Extension # (from main branch only) 16 | runs-on: ubuntu-latest 17 | 18 | # we are very restrictive when this runs, i.e. only on main, only on success and only with the bazel-vscode-java repository (not on forks) 19 | if: > 20 | github.event.workflow_run.conclusion == 'success' && 21 | github.event.workflow_run.event != 'pull_request' && 22 | github.repository == 'salesforce/bazel-vscode-java' && 23 | github.ref == 'refs/heads/main' 24 | 25 | permissions: 26 | actions: write 27 | 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | 32 | - name: Get Time for BUILD_ID 33 | id: time 34 | uses: nanzm/get-time-action@v2.0 35 | with: 36 | format: "YYYYMMDD_HHmm" 37 | 38 | - name: Get Branch name for BUILD_LABEL 39 | id: branch_name 40 | shell: bash 41 | run: echo "branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT 42 | 43 | - name: Setup Node version 44 | uses: actions/setup-node@v4 45 | with: 46 | node-version: 16 47 | cache: 'npm' 48 | 49 | - name: 📦 Install dependencies 50 | run: npm ci 51 | 52 | - name: Download Bazel JDT Language Server extension 53 | uses: dawidd6/action-download-artifact@v8 54 | with: 55 | run_id: ${{ github.event.workflow_run.id }} 56 | name: server 57 | path: server 58 | 59 | - name: 🏗 Build project 60 | env: 61 | BUILD_ID: "${{ steps.time.outputs.time }}" 62 | BUILD_TYPE: "S" 63 | BUILD_LABEL: "CI ${{ steps.time.outputs.time }} (${{ steps.branch_name.outputs.branch }})" 64 | run: npm run build 65 | 66 | - name: Trigger VS Code Extension publish workflow to edge channel 67 | uses: actions/github-script@v7 68 | with: 69 | script: | 70 | await github.rest.actions.createWorkflowDispatch({ 71 | owner: context.repo.owner, 72 | repo: context.repo.repo, 73 | workflow_id: 'publish.yml', 74 | ref: 'main', 75 | inputs: { 76 | releaseChannel: 'edge' 77 | } 78 | }); 79 | 80 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | push: 9 | branches: 10 | - 'main' 11 | pull_request: 12 | branches: 13 | - 'main' 14 | 15 | permissions: {} 16 | 17 | jobs: 18 | build-and-test: 19 | name: Build and Test (OS ${{ matrix.os }}) 20 | runs-on: ${{ matrix.os }} 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | os: [ubuntu-latest, macos-latest] #, windows-latest] # https://github.com/coactions/setup-xvfb/issues/18 26 | 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | 31 | - name: Get Time for BUILD_ID 32 | id: time 33 | uses: nanzm/get-time-action@v2.0 34 | with: 35 | format: "YYYYMMDD_HHmm" 36 | 37 | - name: Get Branch name for BUILD_LABEL 38 | id: branch_name 39 | shell: bash 40 | run: echo "branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT 41 | 42 | - name: Set up JDK 43 | uses: actions/setup-java@v4 44 | with: 45 | java-version: '17' 46 | distribution: 'temurin' 47 | 48 | - name: Cache local Maven repository 49 | uses: actions/cache@v4 50 | with: 51 | path: ~/.m2/repository 52 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml', '**/*.target') }} 53 | restore-keys: | 54 | ${{ runner.os }}-m2 55 | 56 | # - name: Cache IntelliJ Aspects 57 | # uses: actions/cache@v4 58 | # with: 59 | # path: ./bundles/com.salesforce.bazel.sdk/aspects 60 | # key: ${{ runner.os }}-aspects-${{ hashFiles('**/aspects/import/import-and-build.sh', '**/aspects/import/WORKSPACE') }} 61 | 62 | - name: Cache Bazel repository cache 63 | uses: actions/cache@v4 64 | with: 65 | path: | 66 | ~/.cache/bazel 67 | ~/.cache/bazelisk 68 | key: ${{ runner.os }}-bazel 69 | 70 | - name: Setup Bazelisk 71 | uses: bazelbuild/setup-bazelisk@v3 72 | 73 | - name: Print Maven toolchains.xml 74 | shell: bash 75 | run: cat ~/.m2/toolchains.xml 76 | 77 | - name: Setup Node version 78 | uses: actions/setup-node@v4 79 | with: 80 | node-version: 22 81 | cache: 'npm' 82 | 83 | - name: 📦 Install dependencies 84 | run: npm ci 85 | 86 | - name: 🏗 Build project 87 | env: 88 | BUILD_ID: "${{ steps.time.outputs.time }}" 89 | BUILD_TYPE: "S" 90 | BUILD_LABEL: "CI ${{ steps.time.outputs.time }} (${{ steps.branch_name.outputs.branch }})" 91 | run: npm run build 92 | 93 | - name: 🧪 Run tests 94 | uses: GabrielBB/xvfb-action@v1.7 95 | env: 96 | PRINT_JDTLS_LOGS: true 97 | with: 98 | run: npm run test 99 | options: "-screen 0 1600x1200x24" 100 | 101 | - name: Upload Test Results 102 | uses: actions/upload-artifact@v4 103 | if: always() 104 | with: 105 | name: test-results-${{ matrix.os }} 106 | path: | 107 | test/**/*.xml 108 | test/**/*.json 109 | !test/**/tsconfig.json 110 | !test/**/runConfigurations/*.xml 111 | !test/projects/**/* 112 | 113 | - name: Upload screenshots on failure 114 | uses: actions/upload-artifact@v4 115 | if: failure() 116 | with: 117 | name: screenshots-${{ matrix.os }} 118 | path: | 119 | test/screenshots 120 | test/logs 121 | 122 | - name: Upload Bazel JDT Language Server extension 123 | uses: actions/upload-artifact@v4 124 | if: success() && matrix.os == 'ubuntu-latest' 125 | with: 126 | name: server 127 | path: server/ 128 | if-no-files-found: error 129 | 130 | event_file: 131 | name: "Event File" 132 | runs-on: ubuntu-latest 133 | 134 | # needed for publishing test results from forks 135 | steps: 136 | - name: Upload 137 | uses: actions/upload-artifact@v4 138 | with: 139 | name: ci-event-file 140 | path: ${{ github.event_path }} 141 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Extension 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | releaseChannel: 7 | description: "Release Channel" 8 | required: true 9 | type: choice 10 | default: stable 11 | options: 12 | - stable 13 | - edge 14 | 15 | permissions: {} 16 | 17 | jobs: 18 | publish-latest-update-site: 19 | name: Publish Latest Extension 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | 26 | - name: Get Time for BUILD_ID 27 | id: time 28 | uses: nanzm/get-time-action@v2.0 29 | with: 30 | format: "YYYYMMDD_HHmm" 31 | 32 | - name: Get Branch name for BUILD_LABEL 33 | id: branch_name 34 | shell: bash 35 | run: echo "branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT 36 | 37 | - name: Setup Node version 38 | uses: actions/setup-node@v4 39 | with: 40 | node-version: 22 41 | cache: 'npm' 42 | 43 | - name: 📦 Install dependencies 44 | run: | 45 | npm ci 46 | npm install -g vsce 47 | 48 | - name: Download Bazel JDT Language Server extension 49 | run: npx gulp download_server 50 | 51 | - name: 🏗 Build project 52 | env: 53 | BUILD_ID: "${{ steps.time.outputs.time }}" 54 | BUILD_TYPE: "S" 55 | BUILD_LABEL: "CI ${{ steps.time.outputs.time }} (${{ steps.branch_name.outputs.branch }})" 56 | run: npm run build 57 | 58 | - name: 📦 Prepare packaging of pre-release extension 59 | if: ${{ github.event.inputs.releaseChannel == 'edge' }} 60 | run: npx gulp prepare_pre_release 61 | 62 | - name: 🚀 Publish to Visual Studio Marketplace 63 | uses: HaaLeo/publish-vscode-extension@v1 64 | id: publishToVscMktp 65 | with: 66 | preRelease: ${{ github.event.inputs.releaseChannel == 'edge' && true || false }} 67 | pat: ${{ secrets.VSC_MKTP_TOKEN }} 68 | registryUrl: https://marketplace.visualstudio.com 69 | 70 | - name: 🚀 Publish to Open VSX Registry 71 | uses: HaaLeo/publish-vscode-extension@v1 72 | with: 73 | preRelease: ${{ github.event.inputs.releaseChannel == 'edge' && true || false }} 74 | pat: ${{ secrets.OPEN_VSX_TOKEN }} 75 | extensionFile: ${{ steps.publishToVscMktp.outputs.vsixPath }} 76 | 77 | -------------------------------------------------------------------------------- /.github/workflows/test-results.yml: -------------------------------------------------------------------------------- 1 | name: Publish Test Results 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["CI"] 6 | types: 7 | - completed 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | test-results: 13 | name: Test Results 14 | runs-on: ubuntu-latest 15 | if: github.event.workflow_run.conclusion != 'skipped' 16 | 17 | permissions: 18 | checks: write 19 | pull-requests: write 20 | actions: read 21 | 22 | steps: 23 | - name: Download and Extract Artifacts 24 | uses: dawidd6/action-download-artifact@v8 25 | with: 26 | run_id: ${{ github.event.workflow_run.id }} 27 | name: (test-results.*|ci-event-file) 28 | name_is_regexp: true 29 | path: artifacts 30 | 31 | - name: 📣 Publish Test Results 32 | uses: EnricoMi/publish-unit-test-result-action@v2 33 | with: 34 | commit: ${{ github.event.workflow_run.head_sha }} 35 | event_file: artifacts/ci-event-file/event.json 36 | event_name: ${{ github.event.workflow_run.event }} 37 | files: | 38 | artifacts/**/*.xml 39 | artifacts/**/*.json 40 | !artifacts/ci-event-file/event.json 41 | 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bazel-eclipse 3 | out 4 | dist 5 | .vscode-test/ 6 | *.vsix 7 | meta.json 8 | stats.html 9 | server 10 | test/result 11 | *.bazel.lock -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "tabWidth": 2, 5 | "trailingComma": "es5", 6 | "printWidth": 80 7 | } 8 | -------------------------------------------------------------------------------- /.vscode-test.js: -------------------------------------------------------------------------------- 1 | // .vscode-test.js 2 | const { defineConfig } = require('@vscode/test-cli'); 3 | 4 | module.exports = defineConfig([ 5 | { 6 | installExtensions: ['redhat.java@prerelease'], 7 | workspaceFolder: 'test/projects/small', 8 | files: 'out/test/**/*.test.js' 9 | } 10 | ]); 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "EditorConfig.EditorConfig", 7 | "connor4312.esbuild-problem-matchers" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Launch Extension - Remote Server", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ], 16 | "outFiles": [ 17 | "${workspaceFolder}/out/**/*.js" 18 | ], 19 | "env": { 20 | "SERVER_PORT": "3333", 21 | "DEBUG_VSCODE_JAVA": "true" 22 | }, 23 | "preLaunchTask": "${defaultBuildTask}" 24 | }, 25 | { 26 | "name": "Launch Bazel VSCode Java Extension - JDTLS Client", 27 | "type": "extensionHost", 28 | "request": "launch", 29 | "runtimeExecutable": "${execPath}", 30 | "debugWebviews": true, 31 | "args": [ 32 | "--extensionDevelopmentPath=${workspaceRoot}" 33 | ], 34 | "sourceMaps": true, 35 | "outFiles": [ 36 | "${workspaceRoot}/out/**/*.js" 37 | ], 38 | "env": { 39 | "JDTLS_CLIENT_PORT": "5036", 40 | "DEBUG_VSCODE_JAVA": "true", 41 | }, 42 | "preLaunchTask": "npm: watch", 43 | "rendererDebugOptions": { 44 | "urlFilter": "*redhat.java*", 45 | "sourceMaps": true, 46 | } 47 | }, 48 | { 49 | "name": "Launch Bazel VSCode Java Extension", 50 | "type": "extensionHost", 51 | "request": "launch", 52 | "runtimeExecutable": "${execPath}", 53 | "debugWebviews": true, 54 | "args": [ 55 | "--extensionDevelopmentPath=${workspaceRoot}" 56 | ], 57 | "sourceMaps": true, 58 | "outFiles": [ 59 | "${workspaceRoot}/out/**/*.js" 60 | ], 61 | "env": { 62 | "DEBUG_VSCODE_JAVA": "true", 63 | }, 64 | "preLaunchTask": "npm: watch", 65 | "rendererDebugOptions": { 66 | "urlFilter": "*redhat.java*", 67 | "sourceMaps": true, 68 | } 69 | }, 70 | { 71 | "name": "Launch Bazel VSCode WITH RedHat Java Extension", 72 | "type": "extensionHost", 73 | "request": "launch", 74 | "runtimeExecutable": "${execPath}", 75 | "debugWebviews": true, 76 | "args": [ 77 | "--extensionDevelopmentPath=${workspaceRoot}", 78 | "--extensionDevelopmentPath=${workspaceRoot}/../vscode-java" 79 | ], 80 | "sourceMaps": true, 81 | "outFiles": [ 82 | "${workspaceRoot}/out/**/*.js" 83 | ], 84 | "env": { 85 | "DEBUG_VSCODE_JAVA": "true", 86 | }, 87 | "preLaunchTask": "npm: watch", 88 | "rendererDebugOptions": { 89 | "urlFilter": "*redhat.java*", 90 | "sourceMaps": true, 91 | } 92 | }, 93 | { 94 | "name": "Run Bazel Extension Tests", 95 | "type": "extensionHost", 96 | "request": "launch", 97 | "runtimeExecutable": "${execPath}", 98 | "args": [ 99 | "--disable-workspace-trust", 100 | "--extensionDevelopmentPath=${workspaceFolder}", 101 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index", 102 | "${workspaceFolder}/test/projects/small" 103 | ], 104 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"], 105 | "preLaunchTask": "npm: test:compile" 106 | } 107 | ] 108 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | "typescript.tsdk": "./node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version 10 | "git.alwaysSignOff": true, 11 | "vsicons.presets.angular": false, 12 | "editor.codeActionsOnSave": { 13 | "source.fixAll": "explicit", 14 | "source.organizeImports": "explicit" 15 | }, 16 | "java.configuration.updateBuildConfiguration": "disabled" 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "npm: watch", 8 | "type": "npm", 9 | "script": "esbuild:watch", 10 | "problemMatcher": ["$esbuild-watch", "$eslint-stylish"], 11 | "isBackground": true, 12 | "presentation": { 13 | "reveal": "never" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | **/jsconfig.json 2 | **/tsconfig.json 3 | **/.eslintrc.json 4 | **/*.map 5 | **/*.ts 6 | 7 | tsconfig.base.json 8 | .eslintignore 9 | .gitignore 10 | .github/** 11 | .vscode/** 12 | gulpfile.js 13 | node_modules 14 | build.ts 15 | src 16 | test 17 | 18 | CODE_OF_CONDUCT.md 19 | CONTRIBUTING.md 20 | CODEOWNERS 21 | 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Comment line immediately above ownership line is reserved for related gus information. Please be careful while editing. 2 | #ECCN:Open Source 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Salesforce Open Source Community Code of Conduct 2 | 3 | ## About the Code of Conduct 4 | 5 | Equality is a core value at Salesforce. We believe a diverse and inclusive 6 | community fosters innovation and creativity, and are committed to building a 7 | culture where everyone feels included. 8 | 9 | Salesforce open-source projects are committed to providing a friendly, safe, and 10 | welcoming environment for all, regardless of gender identity and expression, 11 | sexual orientation, disability, physical appearance, body size, ethnicity, nationality, 12 | race, age, religion, level of experience, education, socioeconomic status, or 13 | other similar personal characteristics. 14 | 15 | The goal of this code of conduct is to specify a baseline standard of behavior so 16 | that people with different social values and communication styles can work 17 | together effectively, productively, and respectfully in our open source community. 18 | It also establishes a mechanism for reporting issues and resolving conflicts. 19 | 20 | All questions and reports of abusive, harassing, or otherwise unacceptable behavior 21 | in a Salesforce open-source project may be reported by contacting the Salesforce 22 | Open Source Conduct Committee at ossconduct@salesforce.com. 23 | 24 | ## Our Pledge 25 | 26 | In the interest of fostering an open and welcoming environment, we as 27 | contributors and maintainers pledge to making participation in our project and 28 | our community a harassment-free experience for everyone, regardless of gender 29 | identity and expression, sexual orientation, disability, physical appearance, 30 | body size, ethnicity, nationality, race, age, religion, level of experience, education, 31 | socioeconomic status, or other similar personal characteristics. 32 | 33 | ## Our Standards 34 | 35 | Examples of behavior that contributes to creating a positive environment 36 | include: 37 | 38 | * Using welcoming and inclusive language 39 | * Being respectful of differing viewpoints and experiences 40 | * Gracefully accepting constructive criticism 41 | * Focusing on what is best for the community 42 | * Showing empathy toward other community members 43 | 44 | Examples of unacceptable behavior by participants include: 45 | 46 | * The use of sexualized language or imagery and unwelcome sexual attention or 47 | advances 48 | * Personal attacks, insulting/derogatory comments, or trolling 49 | * Public or private harassment 50 | * Publishing, or threatening to publish, others' private information—such as 51 | a physical or electronic address—without explicit permission 52 | * Other conduct which could reasonably be considered inappropriate in a 53 | professional setting 54 | * Advocating for or encouraging any of the above behaviors 55 | 56 | ## Our Responsibilities 57 | 58 | Project maintainers are responsible for clarifying the standards of acceptable 59 | behavior and are expected to take appropriate and fair corrective action in 60 | response to any instances of unacceptable behavior. 61 | 62 | Project maintainers have the right and responsibility to remove, edit, or 63 | reject comments, commits, code, wiki edits, issues, and other contributions 64 | that are not aligned with this Code of Conduct, or to ban temporarily or 65 | permanently any contributor for other behaviors that they deem inappropriate, 66 | threatening, offensive, or harmful. 67 | 68 | ## Scope 69 | 70 | This Code of Conduct applies both within project spaces and in public spaces 71 | when an individual is representing the project or its community. Examples of 72 | representing a project or community include using an official project email 73 | address, posting via an official social media account, or acting as an appointed 74 | representative at an online or offline event. Representation of a project may be 75 | further defined and clarified by project maintainers. 76 | 77 | ## Enforcement 78 | 79 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 80 | reported by contacting the Salesforce Open Source Conduct Committee 81 | at ossconduct@salesforce.com. All complaints will be reviewed and investigated 82 | and will result in a response that is deemed necessary and appropriate to the 83 | circumstances. The committee is obligated to maintain confidentiality with 84 | regard to the reporter of an incident. Further details of specific enforcement 85 | policies may be posted separately. 86 | 87 | Project maintainers who do not follow or enforce the Code of Conduct in good 88 | faith may face temporary or permanent repercussions as determined by other 89 | members of the project's leadership and the Salesforce Open Source Conduct 90 | Committee. 91 | 92 | ## Attribution 93 | 94 | This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant-home], 95 | version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html. 96 | It includes adaptions and additions from [Go Community Code of Conduct][golang-coc], 97 | [CNCF Code of Conduct][cncf-coc], and [Microsoft Open Source Code of Conduct][microsoft-coc]. 98 | 99 | This Code of Conduct is licensed under the [Creative Commons Attribution 3.0 License][cc-by-3-us]. 100 | 101 | [contributor-covenant-home]: https://www.contributor-covenant.org (https://www.contributor-covenant.org/) 102 | [golang-coc]: https://golang.org/conduct 103 | [cncf-coc]: https://github.com/cncf/foundation/blob/master/code-of-conduct.md 104 | [microsoft-coc]: https://opensource.microsoft.com/codeofconduct/ 105 | [cc-by-3-us]: https://creativecommons.org/licenses/by/3.0/us/ 106 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | ## Setup 4 | 5 | For a proper development setup there are four components required: 6 | 7 | 1. This VSCode extension (Bazel VSCode Java) 8 | 2. [The Bazel JDT Language Server extension](https://github.com/salesforce/bazel-eclipse) 9 | 3. [The RedHat VSCode Java extension](https://github.com/redhat-developer/vscode-java) 10 | 4. [The Eclipse JDT Language Server](https://github.com/eclipse-jdtls/eclipse.jdt.ls) 11 | 12 | The model we follow here is follows the pattern found in the RedHat VSCode Java extension. 13 | Allthough steps 3 and 4 are optional, they are highly recommended for a full setup which allows contributions upstream to those. 14 | 15 | All repositories must be cloned into the same parent directory, i.e. they all must be siblings and use the default directory name. 16 | 17 | ## Build 18 | This project uses [esbuild](https://esbuild.github.io/) for bundling and standard vscode tooling to generate vsix packages. The relevant targets you should be aware of are: 19 | - `npm run build` 20 | - compiles everything under the `src` dir and downloads/builds any of the required `jar` files. Setups everything required to package the extension. 21 | - `npm run test` 22 | - compiles everything under the `test` dir and executes any [mocha](https://mochajs.org/) test found 23 | - `npm run lint` 24 | - runs [eslint](https://eslint.org/) against everything in the `src` folder. 25 | - `npm run analyze` 26 | - analyzes the project and displays a report. Useful for understanding how the project is structured 27 | - `npm run package` 28 | - uses [vsce](https://github.com/microsoft/vscode-vsce) to generate a VSIX artifact 29 | 30 | * The bulk of the build is handled via the `build.ts` wrapper script. This script encapsulates all of our defaults used in bundling the project. If you need to tweak the way the build works you should start first in the `package.json` -> `scripts` subsection, then the `build.ts` script. 31 | 32 | ## VSCode Launch configs 33 | We have several different launch config setup for this project. All are used for slightly different uses cases. 34 | 35 | - `Launch Extension - Remote Server` 36 | - launch the extension and debug against a remote java runtime (running on port 3333) 37 | - `Launch Bazel VSCode Java Extension - JDTLS Client` 38 | - launch the extension and use a separately running eclipse LS (running on port 5036) 39 | - `Launch Bazel VSCode Java Extension` 40 | - launch the extension in it's default configuration. 41 | - `Launch Bazel VSCode WITH RedHat Java Extension` 42 | - launch the extension and use the redhat java extension stored in the shared parent directory 43 | - `Run Bazel Extension Tests` 44 | - execute all mocha tests 45 | 46 | ## Best Practices 47 | 48 | * Always run `npm run test` before pushing/creating a PR 49 | * Always run `npm run lint` before pushing/creating a PR 50 | 51 | ## Release 52 | 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Salesforce 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bazel extension for Java™️ Language Support for VS Code 2 | 3 | [![Build](https://github.com/salesforce/bazel-vscode-java/actions/workflows/ci.yml/badge.svg)](https://github.com/salesforce/bazel-vscode-java/actions/workflows/ci.yml) 4 | [![License](https://img.shields.io/github/license/salesforce/bazel-vscode-java?style=for-the-badge)](https://github.com/salesforce/bazel-vscode-java/blob/master/LICENSE) 5 | 6 | This extension adds support for Bazel to the Java™️ Language Support for VS Code. 7 | It plugs into the Eclipse Java Language server and computes project dependencies and classpath information using Bazel `BUILD` files. 8 | 9 | ## Getting Started 10 | 11 | Go and [install the extension](vscode:extension/sfdc.bazel-vscode-java) from the VSCode Marketplace (see [listing here](https://marketplace.visualstudio.com/items?itemName=sfdc.bazel-vscode-java)) or OpenVSX Registry (see [listing here](https://open-vsx.org/extension/sfdc/bazel-vscode-java)). 12 | 13 | Once installed, open VSCode in any Bazel Workspace with Java targets. 14 | The extension will look for a `WORKSPACE` (`WORKSPACE.bazel`) file to identify a Bazel workspace. 15 | Next it will look for [a `.bazelproject` file](https://github.com/salesforce/bazel-eclipse/blob/main/docs/common/projectviews.md) to look for directories and targets to resolve. 16 | If no `.bazelproject` file can be found a default one will be created. 17 | For details of the lookup sequence please have a look at the latest implementation of [BazelProjectImporter.java in the language server](https://github.com/salesforce/bazel-eclipse/blob/0f526c8bd9cf970c4720240314b898218447ddc1/bundles/com.salesforce.bazel.eclipse.jdtls/src/main/java/com/salesforce/bazel/eclipse/jdtls/managers/BazelProjectImporter.java#L108). 18 | 19 | [Troubleshoot tips](docs/troubleshoot.md) may be useful if it doesn't "just work". 20 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security 2 | 3 | Please report any security issue to [security@salesforce.com](mailto:security@salesforce.com) 4 | as soon as it is discovered. This library limits its runtime dependencies in 5 | order to reduce the total cost of ownership as much as can be, but all consumers 6 | should remain vigilant and have their security stakeholders review all third-party 7 | products (3PP) like this one and their dependencies. 8 | -------------------------------------------------------------------------------- /build.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tsx 2 | 3 | import fs from 'node:fs/promises'; 4 | import path from 'node:path'; 5 | 6 | import type { BuildContext, BuildOptions } from 'esbuild'; 7 | import esbuild from 'esbuild'; 8 | 9 | const isWatchMode = process.argv.includes('--watch'); 10 | const options: BuildOptions = { 11 | color: true, 12 | logLevel: 'info', 13 | entryPoints: ['src/extension.ts'], 14 | bundle: true, 15 | metafile: process.argv.includes('--metafile'), 16 | outdir: './out/src', 17 | external: [ 18 | 'vscode', 19 | 'typescript', // vue-component-meta 20 | ], 21 | format: 'cjs', 22 | platform: 'node', 23 | target: 'ESNext', 24 | tsconfig: 'src/tsconfig.json', 25 | sourcemap: process.argv.includes('--sourcemap'), 26 | minify: process.argv.includes('--minify'), 27 | plugins: [ 28 | { 29 | name: 'umd2esm', 30 | setup(build) { 31 | build.onResolve({ filter: /^(vscode-.*|estree-walker|jsonc-parser)/ }, (args) => { 32 | const pathUmdMay = require.resolve(args.path, { 33 | paths: [args.resolveDir], 34 | }); 35 | // Call twice the replace is to solve the problem of the path in Windows 36 | const pathEsm = pathUmdMay 37 | .replace('/umd/', '/esm/') 38 | .replace('\\umd\\', '\\esm\\'); 39 | return { path: pathEsm }; 40 | }); 41 | }, 42 | }, 43 | { 44 | name: 'meta', 45 | setup(build) { 46 | build.onEnd(async (result) => { 47 | if (result.metafile && result.errors.length === 0) { 48 | return fs.writeFile( 49 | path.resolve(__dirname, './meta.json'), 50 | JSON.stringify(result.metafile), 51 | ); 52 | } 53 | }); 54 | }, 55 | }, 56 | ], 57 | }; 58 | 59 | async function main() { 60 | let ctx: BuildContext | undefined; 61 | try { 62 | if (isWatchMode) { 63 | ctx = await esbuild.context(options); 64 | await ctx.watch(); 65 | } else { 66 | const result = await esbuild.build(options); 67 | if (process.argv.includes('--analyze')) { 68 | const chunksTree = await esbuild.analyzeMetafile(result.metafile!, { color: true }); 69 | console.log(chunksTree); 70 | } 71 | } 72 | } catch (error) { 73 | console.error(error); 74 | ctx?.dispose(); 75 | process.exit(1); 76 | } 77 | } 78 | 79 | main(); -------------------------------------------------------------------------------- /docs/troubleshoot.md: -------------------------------------------------------------------------------- 1 | # Troubleshoot 2 | 3 | ## Expected UI 4 | 5 | If this Bazel Java extension managed to correctly configure your workspace, then: 6 | 7 | * The _Explorer_ view will have a _Java Projects_ tab 8 | * The _Java Projects_ will contain a _Bazel dependencies_ in addition to the _JRE System Library_ 9 | * The _Java Projects_ will **NOT** contain _Referenced Libraries_ after the _JRE System Library_ 10 | * Run (Alt/Cmd+Shift+P) the _Java: Configure Java Runtime_ command; the _Type_ should be _Unmanaged folder,_ **NOT** _Maven_ or _Gradle._ 11 | * Run the _Java: Configure Classpath_ command; the _Sources_ should include the correct paths 12 | 13 | Note that VSC only starts to initialize things when you open the first `.java` file/s. 14 | So it's normal in VSC for e.g. _Sources_ to get "gradually initialized" as you open more Java files. 15 | 16 | ## Pre-Release Version 17 | 18 | Please note that this extension's is currently still in _Preview._ 19 | 20 | The last _Release Version_ may not include the latest required bug fixes. 21 | 22 | Remember to click _Switch to Pre-Release Version_ on the extension installation page! 23 | 24 | ## Lightweight Java Mode 25 | 26 | When the status bar shows _{·} Java_ or _Java ✈️_ (for 27 | [Lightweight Java Mode](https://code.visualstudio.com/docs/java/java-project#_lightweight-mode) 28 | instead of _{ } Java_ when an `X.java` editor is open, then click on that to make VSC switch to 29 | _Standard_ Java Mode. 30 | 31 | We recommend to add `"java.server.launchMode": "Standard"` to your repo's `.vscode/settings.json`. 32 | 33 | ## VSC Web UI 34 | 35 | This extension can well in a VS Code Web and/or Remote Development set-up. 36 | 37 | If it doesn't seem to work, check the Extension's _Runtime Status_ tag UI; if that shows 38 | _Runtime Status: Not yet activated._ then you have be facing some permission (?) problems 39 | in your Web browser (Brave? Firefox?) and can try with another one (Chrome). 40 | 41 | [Issue #94](https://github.com/salesforce/bazel-vscode-java/issues/94) has some related background. 42 | 43 | ## Hard Reset 44 | 45 | 1. Close (all of) your VSC windows 46 | 1. `killall java` (check that `jps` only shows _Jps_ itself) 47 | 1. `cd YOURPROJECT` 48 | 1. `bazel build //...` must first work, obviously (double check!) 49 | 1. `code .` 50 | 1. Double check that the [the extension](vscode:extension/sfdc.bazel-vscode-java) is installed **and not disabled** 51 | 1. Run (Alt/Cmd+Shift+P) the _Java: Clean Java Language Server Workspace_ command (this clears the Logs, see below) 52 | 1. Open (Ctrl-P) any `X.java` file in the editor (this is required to trigger the extension to kick in, see above) 53 | 1. Run the _Java: Show Build Job Status_ command, and wait for it to "quiet down" and everything in it to be `[Done]` 54 | 1. Run the _Java: Synchronize Projects with Bazel Project View_ command 55 | 1. Run the _Java: Refresh Classpath from Bazel BUILD file_ command 56 | 57 | You could also try to uninstall and reinstall the extension, to see if that helps. 58 | 59 | ## Logs 60 | 61 | Check out the following places for log-like details to spot any problems: 62 | 63 | * As always, check the _Problems_ view; NB you can _Filter_ it for _Bazel._ 64 | 65 | * Run the _Java: Open All Log Files_ command to open the `.../redhat.java/jdt_ws/.metadata/.log`. 66 | 67 | * The command above should also have opened the `client.log.YYYY-MM-DD` 68 | 69 | Please attach (or copy/paste) all x3 if you file issues for support. 70 | (Rename `.log` to e.g. `log.txt` and `client.log.YYYY-MM-DD` to e.g. `client.log.YYYY-MM-DD.json` 71 | in order to be able to upload as attachment to GitHub issues.) 72 | 73 | The Bazel and JDT Job status is not in these log files, but visible 74 | on the _Terminal_ view _Bazel Build Status_ and _Java Build Status_ tabs; 75 | if in doubt, have a look at that as well (see also [issue #100](https://github.com/salesforce/bazel-vscode-java/issues/100)). 76 | 77 | There is a known limitation during initialization of the Java Language Server. 78 | While the initialization is ongoing the _Bazel Build Status_ will remain empty. 79 | It only starts working _after_ initialization completed. 80 | To work around this limitation you should configure a _static_ port for the extension to use. 81 | This can be done by adding `-Djava.bazel.staticProcessStreamSocket=22222` to `"java.jdt.ls.vmargs":` option in `.vscode/settings.json`. 82 | 83 | ## Extensions 84 | 85 | Please note that this _Bazel for Java_ extension (`sfdc.bazel-vscode-java`, which adds support for Bazel's **Java** rules to VSC), is technically completely independent of _[the VSC Bazel](https://marketplace.visualstudio.com/items?itemName=BazelBuild.vscode-bazel)_ extension (`BazelBuild.vscode-bazel`, which adds generic support for Bazel `BUILD` files editing, and "externally running" any Bazel targets; but specific nothing for Java). 86 | 87 | It's therefore totally possible to run the former (this) without the latter. 88 | 89 | When troubleshooting, it can sometimes be slightly confusing which extension issues what message. For example, notification pop ups with `Command failed: bazel (...)` errors are from the other extension, not this one. 90 | 91 | If in doubt, it is therefore recommended to temporarily _Disable in Workspace_ that other Bazel extension when debugging problems with this extension. 92 | 93 | ## Java Version 94 | 95 | ```java 96 | !ENTRY org.eclipse.jdt.ls.core 4 0 2024-01-05 12:39:25.798 97 | !MESSAGE Error occured while building workspace. Details: 98 | message: Preview features enabled at an invalid source release level 11, preview can be enabled only at source level 21; code: 2098258; resource: /home/vorburger/git/github.com/vorburger/LearningBazel/java-one/src/main/java/ch/vorburger/learningbazel/Main.java; 99 | ``` 100 | 101 | This error (visible in `.log`) means that you need to use Java 21 to use this extension. 102 | 103 | More background in [issue #85](https://github.com/salesforce/bazel-vscode-java/issues/85) and [issue #74](https://github.com/salesforce/bazel-vscode-java/issues/74). 104 | 105 | ## Menus 106 | 107 | If the _Synchronize Projects with Bazel View_ and _Refresh Classpath from Bazel BUILD file_ 108 | menus are not available on right-click on Folder, then the extension is not correct installed, 109 | or has been manually disabled on the workspace. 110 | 111 | ## Bazelisk 112 | 113 | ```java 114 | Error (...) Unable to detect Bazel version of binary 'bazel'! 115 | Cannot run program "bazel": error=2, No such file or directory 116 | java.io.IOException: Cannot run program "bazel": error=2, No such file or directory 117 | at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1143) 118 | at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1073) 119 | at com.salesforce.bazel.sdk.command.BazelBinaryVersionDetector.detectVersion(BazelBinaryVersionDetector.java:57) 120 | at com.salesforce.bazel.eclipse.core.extensions.DetectBazelVersionAndSetBinaryJob.run(DetectBazelVersionAndSetBinaryJob.java:52) 121 | at org.eclipse.core.internal.jobs.Worker.run(Worker.java:63) 122 | Caused by: java.io.IOException: error=2, No such file or directory 123 | ``` 124 | 125 | The extension attempts to launch `bazel`, but it's not on your `$PATH`. 126 | 127 | If you use [Bazelisk](https://github.com/bazelbuild/bazelisk) (which we recommend!), 128 | make sure you have a _symlink_ from `bazel` to `bazelisk` on `$PATH`. On macOS, 129 | Homebrew sets this up. On Linux, you need to [do this yourself e.g. in your dotfiles](https://github.com/vorburger/vorburger-dotfiles-bin-etc/commit/b8dea1dcf465db6f201d1cfa4302b748a08fc3b5). 130 | 131 | Alternatively, add `bazel_binary: bazelisk` in the project's `.bazelproject` configuration file (but see [issue #88](https://github.com/salesforce/bazel-vscode-java/issues/88)). 132 | 133 | More background in [issue #477](https://github.com/salesforce/bazel-eclipse/issues/477). 134 | 135 | ## Maven or Gradle 136 | 137 | If there is a _Build tool conflicts are detected in workspace. Which one would you like to use? Maven, or Gradle?_ pop-up: 138 | 139 | Just click _Maven_ - and this extension will still correctly kick-in and initialize classpaths and source folders etc. from Bazel. 140 | 141 | We recommend to add the following to your repo's `.vscode/settings.json` if you run into this problem: 142 | 143 | ```json 144 | "java.import.bazel.enabled": true, 145 | "java.import.maven.enabled": false, 146 | "java.import.gradle.enabled": false, 147 | ``` 148 | 149 | More background in [issue #82](https://github.com/salesforce/bazel-vscode-java/issues/82). 150 | 151 | ## Alternatives 152 | 153 | To narrow down root causes of problems, it may be interesting to try opening the same project 154 | using the [Bazel Eclipse Feature](https://github.com/salesforce/bazel-eclipse/blob/main/docs/bef/README.md) 155 | and see if that works. 156 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // Licensed under the MIT license. 4 | 5 | const gulp = require('gulp'); 6 | const cp = require('child_process'); 7 | const decompress = require('gulp-decompress'); 8 | const download = require('gulp-download'); 9 | const rename = require('gulp-rename'); 10 | const filter = require('gulp-filter'); 11 | const gRegexRename = require('gulp-regex-rename'); 12 | const fs = require('node:fs'); 13 | const BAZEL_ECLIPSE_DIR = '../bazel-eclipse'; 14 | const BAZEL_ECLIPSE_LATEST_URL = 15 | 'https://opensource.salesforce.com/bazel-eclipse/latest/p2-repository.zip'; 16 | const NON_NPM_REPOSITORY_RE = new RegExp( 17 | String.raw`"resolved":\s*"https://(?!(registry\.npmjs\.org\/?))`, 18 | 'g' 19 | ); 20 | 21 | // a little helper to drop OSGi versions from bundle jar file name 22 | const DROP_JAR_VERSION = gRegexRename(/_\d+\.\d+\.\d+(\.[^\.]+)?\.jar/, '.jar'); 23 | 24 | // read the package.json once so we can use it in the gulp script 25 | const packageJson = JSON.parse(fs.readFileSync('./package.json').toString()); 26 | 27 | // we only need the headless jars of the Bazel JDT Language Server extension 28 | const declaredServerJars = new Set( 29 | packageJson.contributes.javaExtensions.map( 30 | (path) => path.split('/').reverse()[0] 31 | ) 32 | ); 33 | const jarIsIncludedInPackageJson = filter((file) => { 34 | return declaredServerJars.has(file.basename); 35 | }); 36 | 37 | gulp.task('download_server', function (done) { 38 | downloadServerImpl(); 39 | done(); 40 | }); 41 | 42 | gulp.task('build_server', function (done) { 43 | buildServerImpl(); 44 | done(); 45 | }); 46 | 47 | gulp.task('build_or_download', function (done) { 48 | if (!fs.existsSync(BAZEL_ECLIPSE_DIR)) { 49 | console.log( 50 | 'NOTE: bazel-eclipse is not found as a sibling directory, downloading the latest snapshot of the Bazel JDT Language Server extension...' 51 | ); 52 | downloadServerImpl(); 53 | } else { 54 | buildServerImpl(); 55 | } 56 | done(); 57 | }); 58 | 59 | gulp.task('prepare_pre_release', function (done) { 60 | // parse existing version (using ECMA script regex from https://semver.org/) 61 | const stableVersion = packageJson.version.match( 62 | /^(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-]+)*))?$/ 63 | ); 64 | const major = stableVersion[1]; 65 | // unfortunately, VS Code Marketplace does not support full semver 66 | // also, the limit is < 2147483647 on VS Code Marketplace 67 | // thus, we use year (just the last two digits) as minor 68 | // and patch that is based starting with the month up to the minute (for granularity) 69 | const date = new Date(); 70 | const year = date.getUTCFullYear() - 2000; 71 | const month = date.getUTCMonth() + 1; 72 | const day = date.getUTCDate(); 73 | const hours = date.getUTCHours(); 74 | const minutes = date.getUTCMinutes(); 75 | const patch = `1${prependZero(month)}${prependZero(day)}${prependZero( 76 | hours 77 | )}${prependZero(minutes)}`; 78 | const insiderPackageJson = Object.assign(packageJson, { 79 | version: `${major}.${year}.${patch}`, 80 | }); 81 | console.log(`Applying pre-release version '${major}.${year}.${patch}'...`); 82 | fs.writeFileSync( 83 | './package.json', 84 | JSON.stringify(insiderPackageJson, null, '\t') 85 | ); 86 | done(); 87 | }); 88 | 89 | gulp.task('repo_check', function (done) { 90 | const data = fs.readFileSync('./package-lock.json', { encoding: 'utf-8' }); 91 | 92 | if (NON_NPM_REPOSITORY_RE.test(data)) { 93 | done( 94 | new Error( 95 | "Found references to the internal registry in the file package-lock.json. Please fix it with replacing all URLs using 'https://registry.npmjs.org'!" 96 | ) 97 | ); 98 | } else { 99 | done(); 100 | } 101 | }); 102 | 103 | function isWin() { 104 | return /^win/.test(process.platform); 105 | } 106 | 107 | function isMac() { 108 | return /^darwin/.test(process.platform); 109 | } 110 | 111 | function isLinux() { 112 | return /^linux/.test(process.platform); 113 | } 114 | 115 | function mvnw() { 116 | return isWin() ? 'mvnw.cmd' : './mvnw'; 117 | } 118 | 119 | function prependZero(num) { 120 | if (num > 99) { 121 | throw new Error('Unexpected value to prepend with zero'); 122 | } 123 | return `${num < 10 ? '0' : ''}${num}`; 124 | } 125 | 126 | function downloadServerImpl() { 127 | fs.rmSync('./server', { recursive: true, force: true }); 128 | download(BAZEL_ECLIPSE_LATEST_URL) 129 | .pipe(decompress()) 130 | .pipe(filter(['plugins/*.jar'])) 131 | .pipe( 132 | rename(function (path) { 133 | return { 134 | dirname: '', // flatten 135 | basename: path.basename, 136 | extname: path.extname, 137 | }; 138 | }) 139 | ) 140 | .pipe(DROP_JAR_VERSION) 141 | .pipe(jarIsIncludedInPackageJson) 142 | .pipe(gulp.dest('./server')); 143 | } 144 | 145 | function buildServerImpl() { 146 | fs.rmSync('./server', { recursive: true, force: true }); 147 | cp.execSync(mvnw() + ' clean package -DskipTests=true', { 148 | cwd: BAZEL_ECLIPSE_DIR, 149 | stdio: [0, 1, 2], 150 | }); 151 | gulp 152 | .src( 153 | BAZEL_ECLIPSE_DIR + '/releng/p2repository/target/repository/plugins/*.jar' 154 | ) 155 | .pipe(DROP_JAR_VERSION) 156 | .pipe(jarIsIncludedInPackageJson) 157 | .pipe(gulp.dest('./server')); 158 | } 159 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bazel-vscode-java", 3 | "displayName": "Bazel extension for Java(TM) Language Support by Salesforce Engineering", 4 | "description": "Bazel support for Java Linting, Intellisense, formatting, refactoring and more...", 5 | "author": "Salesforce Engineering", 6 | "license": "BSD-3-Clause", 7 | "version": "1.4.1", 8 | "publisher": "sfdc", 9 | "bugs": "https://github.com/salesforce/bazel-vscode-java/issues", 10 | "preview": true, 11 | "engines": { 12 | "vscode": "^1.80.0" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/salesforce/bazel-vscode-java" 17 | }, 18 | "categories": [ 19 | "Programming Languages", 20 | "Linters", 21 | "Formatters", 22 | "Snippets", 23 | "Other" 24 | ], 25 | "keywords": [ 26 | "multi-root ready", 27 | "Bazel", 28 | "Java" 29 | ], 30 | "activationEvents": [ 31 | "onLanguage:java", 32 | "workspaceContains:**/*.bazelproject", 33 | "workspaceContains:**/WORKSPACE", 34 | "workspaceContains:**/BUILD", 35 | "workspaceContains:**/*.bazel" 36 | ], 37 | "main": "./out/src/extension.js", 38 | "contributes": { 39 | "javaExtensions": [ 40 | "./server/org.eclipse.equinox.event.jar", 41 | "./server/com.github.ben-manes.caffeine.jar", 42 | "./server/org.jsr-305.jar", 43 | "./server/org.fusesource.jansi.jar", 44 | "./server/com.google.protobuf.jar", 45 | "./server/com.salesforce.bazel.importedsource.jar", 46 | "./server/com.salesforce.bazel.sdk.jar", 47 | "./server/com.salesforce.bazel.eclipse.core.jar", 48 | "./server/com.salesforce.bazel.eclipse.jdtls.jar" 49 | ], 50 | "javaBuildFilePatterns": [ 51 | "^WORKSPACE(\\.bazel)?$", 52 | "^BUILD(\\.bazel)?$", 53 | ".*\\.bazelproject$", 54 | ".*\\.java$" 55 | ], 56 | "languages": [ 57 | { 58 | "id": "bazelproject", 59 | "aliases": [ 60 | "bazelproject" 61 | ], 62 | "extensions": [ 63 | ".bazelproject" 64 | ], 65 | "configuration": "./syntaxes/bazelproject-language-configuration.json" 66 | }, 67 | { 68 | "id": "starlark", 69 | "aliases": [ 70 | "Starlark", 71 | "starlark", 72 | "Bazel" 73 | ], 74 | "extensions": [ 75 | ".BUILD", 76 | ".WORKSPACE", 77 | ".bazel", 78 | ".bzl", 79 | ".bzlmod", 80 | ".sky", 81 | ".star" 82 | ], 83 | "filenames": [ 84 | "BUILD", 85 | "WORKSPACE" 86 | ], 87 | "configuration": "./syntaxes/starlark-language-configuration.json" 88 | } 89 | ], 90 | "grammars": [ 91 | { 92 | "language": "bazelproject", 93 | "scopeName": "source.bazelproject", 94 | "path": "./syntaxes/bazelproject.tmLanguage.json" 95 | }, 96 | { 97 | "language": "starlark", 98 | "scopeName": "source.starlark", 99 | "path": "./syntaxes/starlark.tmLanguage.json" 100 | } 101 | ], 102 | "taskDefinitions": [ 103 | { 104 | "type": "bazel", 105 | "required": [ 106 | "name", 107 | "task" 108 | ], 109 | "properties": { 110 | "name": { 111 | "type": "string", 112 | "description": "User readable bazel run target name" 113 | }, 114 | "task": { 115 | "type": "string", 116 | "description": "The full bazel command to be executed" 117 | } 118 | } 119 | } 120 | ], 121 | "configuration": { 122 | "title": "Bazel Java", 123 | "properties": { 124 | "java.import.bazel.disabled": { 125 | "type": "boolean", 126 | "default": false, 127 | "description": "Disable the Bazel importer.", 128 | "scope": "window" 129 | }, 130 | "java.bazel.log.level": { 131 | "type": "string", 132 | "enum": [ 133 | "debug", 134 | "warn", 135 | "info", 136 | "error", 137 | "trace" 138 | ], 139 | "default": "info", 140 | "description": "Configure detailed logging (debug, warn, info, error)", 141 | "scope": "window" 142 | }, 143 | "bazel.projectview.open": { 144 | "type": "boolean", 145 | "default": true, 146 | "description": "Open the Bazel Project View file on extension activation", 147 | "scope": "window" 148 | }, 149 | "bazel.projectview.updateFileWatcherExclusion": { 150 | "type": "boolean", 151 | "default": true, 152 | "description": "update the files.watcherExclude setting to only watch directories specified in the .bazelproject file.", 153 | "scope": "window" 154 | }, 155 | "bazel.projectview.notification": { 156 | "type": "boolean", 157 | "default": true, 158 | "description": "Display 'sync project view' notification info window on .bazelproject edit", 159 | "scope": "window" 160 | }, 161 | "bazel.buildifier.enable": { 162 | "type": "boolean", 163 | "default": true, 164 | "description": "Enable buildifier formatting tool on save", 165 | "scope": "window" 166 | }, 167 | "bazel.buildifier.binary": { 168 | "type": [ 169 | "string", 170 | "null" 171 | ], 172 | "default": null, 173 | "description": "path to buildifier binary. If not set buildifier from your PATH will be used", 174 | "scope": "window" 175 | } 176 | } 177 | }, 178 | "commands": [ 179 | { 180 | "command": "java.bazel.syncProjects.command", 181 | "title": "Synchronize Projects with Bazel Project View", 182 | "category": "Java" 183 | }, 184 | { 185 | "command": "java.bazel.syncDirectoriesOnly.command", 186 | "title": "Synchronize only directories", 187 | "category": "Bazel" 188 | }, 189 | { 190 | "command": "java.bazel.updateClasspaths.command", 191 | "title": "Refresh classpath from Bazel BUILD file", 192 | "category": "Java" 193 | }, 194 | { 195 | "command": "java.bazel.debug.command", 196 | "title": "Debug LS", 197 | "category": "Bazel" 198 | }, 199 | { 200 | "command": "java.bazel.showStatus", 201 | "title": "Show Bazel Build Status", 202 | "category": "Bazel" 203 | }, 204 | { 205 | "command": "bazelTaskOutline.refresh", 206 | "title": "Refresh Bazel Run Configs", 207 | "category": "Bazel", 208 | "icon": "$(sync)" 209 | }, 210 | { 211 | "command": "bazelTaskOutline.run", 212 | "title": "Run Bazel Target", 213 | "category": "Bazel", 214 | "icon": "$(debug-start)" 215 | }, 216 | { 217 | "command": "bazelTaskOutline.kill", 218 | "title": "Kill Bazel Target", 219 | "category": "Bazel", 220 | "icon": "$(debug-stop)" 221 | }, 222 | { 223 | "command": "bazel.projectview.open", 224 | "title": "Open the Bazel Project View file", 225 | "category": "Bazel" 226 | }, 227 | { 228 | "command": "bazel.convert.workspace", 229 | "title": "Convert to Multi-Root workspace", 230 | "category": "Bazel" 231 | } 232 | ], 233 | "views": { 234 | "explorer": [ 235 | { 236 | "id": "bazelTaskOutline", 237 | "name": "Bazel Run Targets" 238 | } 239 | ] 240 | }, 241 | "menus": { 242 | "commandPalette": [ 243 | { 244 | "command": "java.bazel.syncProjects.command", 245 | "when": "javaLSReady" 246 | }, 247 | { 248 | "command": "java.bazel.updateClasspaths.command", 249 | "when": "javaLSReady && isFileSystemResource" 250 | }, 251 | { 252 | "command": "java.bazel.debug.command", 253 | "when": "javaLSReady && isFileSystemResource" 254 | }, 255 | { 256 | "command": "bazelTaskOutline.refresh", 257 | "when": "false" 258 | }, 259 | { 260 | "command": "bazelTaskOutline.run", 261 | "when": "false" 262 | }, 263 | { 264 | "command": "bazelTaskOutline.kill", 265 | "when": "false" 266 | }, 267 | { 268 | "command": "bazel.projectview.open", 269 | "when": "isBazelWorkspaceRoot" 270 | } 271 | ], 272 | "explorer/context": [ 273 | { 274 | "command": "java.bazel.syncProjects.command", 275 | "when": "javaLSReady", 276 | "group": "1_javaactions@10" 277 | }, 278 | { 279 | "command": "java.bazel.updateClasspaths.command", 280 | "when": "javaLSReady && isFileSystemResource", 281 | "group": "1_javaactions@11" 282 | } 283 | ], 284 | "view/title": [ 285 | { 286 | "command": "bazelTaskOutline.refresh", 287 | "when": "view == bazelTaskOutline", 288 | "group": "navigation@1" 289 | } 290 | ], 291 | "view/item/context": [ 292 | { 293 | "command": "bazelTaskOutline.run", 294 | "when": "view == bazelTaskOutline && viewItem == task", 295 | "group": "inline@1" 296 | }, 297 | { 298 | "command": "bazelTaskOutline.kill", 299 | "when": "view == bazelTaskOutline && viewItem == runningTask", 300 | "group": "inline@2" 301 | } 302 | ] 303 | } 304 | }, 305 | "scripts": { 306 | "vscode:prepublish": "run-s clean esbuild:base -- --minify", 307 | "build": "run-s clean build:server esbuild:base", 308 | "build:server": "gulp build_or_download", 309 | "clean": "npx rimraf out meta.json stats.html *.vsix", 310 | "esbuild:base": "tsx build.ts", 311 | "esbuild:watch": "npm run esbuild:base -- --sourcemap --watch", 312 | "analyze": "npm run esbuild:base -- --minify --metafile --analyze && esbuild-visualizer --metadata ./meta.json --open", 313 | "lint": "eslint src --ext ts", 314 | "lint:fix": "eslint src --ext ts --fix", 315 | "test": "run-s clean test:*", 316 | "test:compile": "tsc -b ./test/tsconfig.json", 317 | "test:lint": "eslint --ext .js,.ts,.tsx src", 318 | "test:repo": "gulp repo_check", 319 | "test:extension": "vscode-test", 320 | "package": "run-s clean build vsce:package", 321 | "vsce:package": "npx vsce package --no-dependencies" 322 | }, 323 | "extensionDependencies": [ 324 | "redhat.java" 325 | ], 326 | "devDependencies": { 327 | "@types/glob": "^8.1.0", 328 | "@types/mocha": "^8.0.4", 329 | "@types/node": "^20.7.1", 330 | "@types/vscode": "^1.80.0", 331 | "@typescript-eslint/eslint-plugin": "^6.0.0", 332 | "@typescript-eslint/parser": "^6.0.0", 333 | "@vscode/test-cli": "^0.0.10", 334 | "@vscode/test-electron": "^2.4.1", 335 | "@vscode/vsce": "^3.2.1", 336 | "esbuild": "0.19.8", 337 | "esbuild-plugin-eslint": "^0.3.7", 338 | "esbuild-visualizer": "^0.4.1", 339 | "eslint": "^8.45.0", 340 | "eslint-config-prettier": "^9.1.0", 341 | "eslint-plugin-prettier": "^5.1.2", 342 | "glob": "^10.3.4", 343 | "gulp": "^5.0.0", 344 | "gulp-decompress": "^3.0.0", 345 | "gulp-download": "^0.0.1", 346 | "gulp-filter": "^7.0.0", 347 | "gulp-regex-rename": "^0.1.0", 348 | "gulp-rename": "^2.0.0", 349 | "gulp-util": "^3.0.8", 350 | "mocha": "^10.2.0", 351 | "npm-run-all": "^4.1.5", 352 | "prettier": "^3.1.1", 353 | "rimraf": "^5.0.5", 354 | "tsx": "^4.6.0", 355 | "typescript": "^5.3.0", 356 | "vscode-languageclient": "^9.0.1" 357 | }, 358 | "dependencies": { 359 | "fast-xml-parser": "^4.4.1" 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /src/bazelLangaugeServerTerminal.ts: -------------------------------------------------------------------------------- 1 | import { Writable } from 'stream'; 2 | import { Terminal, window, workspace } from 'vscode'; 3 | import { BazelTerminal } from './bazelTerminal'; 4 | 5 | const BAZEL_TERMINAL_NAME = 'Bazel Build Status'; 6 | 7 | export namespace BazelLanguageServerTerminal { 8 | export function stream(): Writable { 9 | const s = new Writable(); 10 | s._write = (chunk: Buffer, encoding, next) => { 11 | getBazelTerminal().sendText(chunk.toString()); 12 | next(); 13 | }; 14 | s.on('unpipe', () => s.end()); 15 | 16 | return s; 17 | } 18 | 19 | // good reference if you want to change any colors https://misc.flogisoft.com/bash/tip_colors_and_formatting 20 | export function info(msg: string) { 21 | getBazelTerminal().sendText(`\u001b[32m${msg}\u001b[0m`); 22 | } // green 23 | export function warn(msg: string) { 24 | if (getLogLevel() >= LogLevel.WARN) { 25 | getBazelTerminal().sendText(`\u001b[33m${msg}\u001b[0m`); 26 | } 27 | } // yellow 28 | export function debug(msg: string) { 29 | if (getLogLevel() >= LogLevel.WARN) { 30 | getBazelTerminal().sendText(`\u001b[34m${msg}\u001b[0m`); 31 | } 32 | } // blue 33 | export function error(msg: string) { 34 | getBazelTerminal().sendText(`\u001b[31m${msg}\u001b[0m`); 35 | } // red 36 | export function trace(msg: string) { 37 | if (getLogLevel() >= LogLevel.WARN) { 38 | getBazelTerminal().sendText(`\u001b[37m${msg}\u001b[0m`); 39 | } 40 | } // gray 41 | } 42 | 43 | export function getBazelTerminal(): Terminal { 44 | const term = window.terminals.find( 45 | (term) => term.name === BAZEL_TERMINAL_NAME 46 | ); 47 | if (!term) { 48 | return window.createTerminal({ 49 | name: BAZEL_TERMINAL_NAME, 50 | pty: new BazelTerminal(), 51 | }); 52 | } 53 | return term; 54 | } 55 | 56 | enum LogLevel { 57 | INFO, 58 | ERROR = 0, 59 | WARN = 1, 60 | DEBUG = 2, 61 | TRACE = 3, 62 | } 63 | 64 | function getLogLevel(): LogLevel { 65 | const levelVal = workspace.getConfiguration('java.bazel').get('log.level'); 66 | 67 | switch (levelVal) { 68 | case 'debug': 69 | return LogLevel.DEBUG; 70 | case 'warn': 71 | return LogLevel.WARN; 72 | case 'trace': 73 | return LogLevel.TRACE; 74 | default: 75 | return LogLevel.INFO; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/bazelRunStatus.ts: -------------------------------------------------------------------------------- 1 | import { StatusBarAlignment, ThemeColor, window } from 'vscode'; 2 | import { Commands } from './commands'; 3 | 4 | export namespace BazelRunStatus { 5 | const bazelStatus = window.createStatusBarItem(StatusBarAlignment.Left, 1); 6 | 7 | bazelStatus.command = Commands.OPEN_BAZEL_BUILD_STATUS_CMD; 8 | bazelStatus.text = `$(sync~spin) bazel building`; 9 | bazelStatus.backgroundColor = new ThemeColor( 10 | 'statusBarItem.warningBackground' 11 | ); 12 | 13 | export const show = () => bazelStatus.show(); 14 | export const hide = () => bazelStatus.hide(); 15 | } 16 | -------------------------------------------------------------------------------- /src/bazelTaskManager.ts: -------------------------------------------------------------------------------- 1 | import { tasks } from 'vscode'; 2 | import { 3 | BazelRunTarget, 4 | BazelRunTargetProvider, 5 | } from './provider/bazelRunTargetProvider'; 6 | 7 | export namespace BazelTaskManager { 8 | export function refreshTasks() { 9 | BazelRunTargetProvider.instance.refresh(); 10 | } 11 | 12 | export function runTask(bazelTarget: BazelRunTarget) { 13 | tasks.executeTask(bazelTarget.task!); 14 | } 15 | 16 | export function killTask(bazelTarget: BazelRunTarget) { 17 | bazelTarget.execution?.terminate(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/bazelTerminal.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Event, 3 | EventEmitter, 4 | Pseudoterminal, 5 | TerminalDimensions, 6 | } from 'vscode'; 7 | 8 | let backtickHighlight = true; 9 | 10 | export class BazelTerminal implements Pseudoterminal { 11 | private writeEmitter = new EventEmitter(); 12 | 13 | onDidWrite = this.writeEmitter.event; 14 | onDidOverrideDimensions?: Event | undefined; 15 | onDidClose?: Event | undefined; 16 | onDidChangeName?: Event | undefined; 17 | 18 | open(initialDimensions: TerminalDimensions | undefined): void { 19 | this.writeEmitter.fire(''); 20 | } 21 | close(): void { 22 | this.writeEmitter.dispose(); 23 | } 24 | async handleInput?(data: string): Promise { 25 | this.writeEmitter.fire( 26 | data 27 | .replace(/\"/g, '\\"') 28 | .replace(/\`/g, highlightBacktick) 29 | .replace(/(\r|\n)+/g, '\r\n') 30 | ); 31 | } 32 | } 33 | 34 | function highlightBacktick(substring: string, ...args: any[]): string { 35 | backtickHighlight = !backtickHighlight; 36 | if (backtickHighlight) { 37 | return '\u001b[0m'; 38 | } 39 | return '\u001b[33m'; 40 | } 41 | -------------------------------------------------------------------------------- /src/bazelprojectparser.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync } from 'fs'; 2 | import { FileType, Uri, workspace } from 'vscode'; 3 | import { BazelLanguageServerTerminal } from './bazelLangaugeServerTerminal'; 4 | import { BazelProjectView, ParseConfig, RawSection } from './types'; 5 | import { getWorkspaceRoot } from './util'; 6 | 7 | const COMMENT_REGEX = /#(.)*(\n|\z)/gm; 8 | const HEADER_REGEX = /^[^:\-\/*\s]+[: ]/gm; 9 | const WHITESPACE_CHAR_REGEX = /\s+/; 10 | const EXCLUDED_ENTRY_PREFIX = '-'; 11 | 12 | function parseProjectFile(config: ParseConfig): BazelProjectView { 13 | let current: string | undefined; 14 | while ((current = config.imports.pop())) { 15 | const filePath = `${config.root}/${current}`; 16 | if (existsSync(filePath)) { 17 | if (config.processedImports.includes(current)) { 18 | throw new Error( 19 | `Recursive import detected for file ${current}, ${config.processedImports.join( 20 | '-> ' 21 | )}` 22 | ); 23 | } 24 | config.processedImports.push(current); 25 | 26 | let fileContent = readFileSync(filePath, { encoding: 'utf-8' }); 27 | fileContent = removeComments(fileContent); 28 | 29 | const rawSections = parseRawSections(fileContent).forEach((section) => { 30 | switch (section.name) { 31 | case 'directories': 32 | config.projectView.directories = Array.from( 33 | new Set( 34 | config.projectView.directories.concat( 35 | parseAsList(section.body).filter( 36 | (s) => !s.startsWith(EXCLUDED_ENTRY_PREFIX) 37 | ) 38 | ) 39 | ) 40 | ); 41 | break; 42 | case 'targets': 43 | config.projectView.targets = Array.from( 44 | new Set( 45 | config.projectView.targets.concat( 46 | parseAsList(section.body).filter( 47 | (s) => !s.startsWith(EXCLUDED_ENTRY_PREFIX) 48 | ) 49 | ) 50 | ) 51 | ); 52 | break; 53 | case 'import': 54 | config.imports = config.imports.concat(parseAsList(section.body)); 55 | break; 56 | case 'derive_targets_from_directories': 57 | config.projectView.deriveTargetsFromDirectories = parseAsBoolean( 58 | section.body 59 | ); 60 | break; 61 | case 'workspace_type': 62 | config.projectView.workspaceType = section.body; 63 | break; 64 | case 'additional_languages': 65 | config.projectView.additionalLanguages = Array.isArray( 66 | config.projectView.additionalLanguages 67 | ) 68 | ? config.projectView.additionalLanguages.concat( 69 | parseAsList(section.body) 70 | ) 71 | : parseAsList(section.body); 72 | break; 73 | case 'java_language_level': 74 | config.projectView.javaLanguageLevel = section.body; 75 | break; 76 | case 'ts_config_rules': 77 | config.projectView.tsConfigRules = Array.isArray( 78 | config.projectView.tsConfigRules 79 | ) 80 | ? config.projectView.tsConfigRules.concat( 81 | parseAsList(section.body) 82 | ) 83 | : parseAsList(section.body); 84 | break; 85 | case 'import_run_configurations': 86 | config.projectView.importRunConfigurations = Array.isArray( 87 | config.projectView.importRunConfigurations 88 | ) 89 | ? config.projectView.importRunConfigurations.concat( 90 | parseAsList(section.body) 91 | ) 92 | : parseAsList(section.body); 93 | break; 94 | case 'bazel_binary': 95 | config.projectView.bazelBinary = section.body; 96 | break; 97 | case 'project_mappings': 98 | config.projectView.projectMappings = Array.isArray( 99 | config.projectView.projectMappings 100 | ) 101 | ? config.projectView.projectMappings.concat(section.body) 102 | : parseAsList(section.body); 103 | break; 104 | case 'target_discovery_strategy': 105 | config.projectView.targetDiscoveryStrategy = section.body; 106 | break; 107 | case 'target_provisioning_strategy': 108 | config.projectView.targetProvisioningStrategy = section.body; 109 | break; 110 | default: 111 | BazelLanguageServerTerminal.warn( 112 | `unexpected section '${section.name}' while reading '${current}'` 113 | ); 114 | } 115 | }); 116 | } else { 117 | BazelLanguageServerTerminal.warn(`unable to resolve import ${current}`); 118 | } 119 | } 120 | 121 | return config.projectView; 122 | } 123 | 124 | function parseAsList(value: string): string[] { 125 | return value.split(WHITESPACE_CHAR_REGEX).filter((v) => v !== ''); 126 | } 127 | 128 | function parseAsBoolean(value: string) { 129 | return /true/i.test(value); 130 | } 131 | 132 | function parseRawSections(projectFileContents: string): RawSection[] { 133 | const result = new Array(); 134 | 135 | const headers = projectFileContents 136 | .match(HEADER_REGEX) 137 | ?.map((h) => h.replace(':', '')) 138 | ?.map((h) => h.trim()); 139 | 140 | const bodies = projectFileContents.split(HEADER_REGEX); 141 | 142 | if (headers?.length !== bodies.length - 1) { 143 | throw new Error( 144 | `Syntax error in .bazelproject: The number of section headers doesn't match the number of section bodies (${headers?.length} != ${ 145 | bodies.length 146 | }; header: ${headers?.join(',')}).` 147 | ); 148 | } 149 | 150 | headers.forEach((value, idx) => 151 | result.push({ name: value, body: bodies[idx + 1].trim() }) 152 | ); 153 | 154 | return result; 155 | } 156 | 157 | function removeComments(bazelProjectFileContent: string): string { 158 | return bazelProjectFileContent.replace(COMMENT_REGEX, '\n'); 159 | } 160 | 161 | export async function getBazelProjectFile(): Promise { 162 | try { 163 | const bazelProjectFileStat = await workspace.fs.stat( 164 | Uri.parse(`${getWorkspaceRoot()}/.eclipse/.bazelproject`) 165 | ); 166 | if (bazelProjectFileStat.type === FileType.File) { 167 | return readBazelProject(`.eclipse/.bazelproject`); 168 | } 169 | throw new Error( 170 | `.eclipse/.bazelproject type is ${bazelProjectFileStat.type}, should be ${FileType.File}` 171 | ); 172 | } catch (err) { 173 | throw new Error(`Could not read .eclipse/.bazelproject file: ${err}`); 174 | } 175 | } 176 | 177 | export function readBazelProject(bazelProjectFile: string): BazelProjectView { 178 | return parseProjectFile({ 179 | root: getWorkspaceRoot(), 180 | imports: [bazelProjectFile], 181 | projectView: { 182 | directories: [], 183 | targets: [], 184 | deriveTargetsFromDirectories: false, 185 | }, 186 | processedImports: [], 187 | }); 188 | } 189 | -------------------------------------------------------------------------------- /src/buildifier.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process'; 2 | import { 3 | languages, 4 | Range, 5 | TextDocument, 6 | TextEdit, 7 | window, 8 | workspace, 9 | } from 'vscode'; 10 | import { getWorkspaceRoot } from './util'; 11 | 12 | export function registerBuildifierFormatter() { 13 | languages.registerDocumentFormattingEditProvider( 14 | { scheme: 'file', language: 'starlark' }, 15 | { 16 | async provideDocumentFormattingEdits( 17 | document: TextDocument 18 | ): Promise { 19 | if ( 20 | workspace.getConfiguration('bazel.buildifier').get('enable', false) 21 | ) { 22 | try { 23 | if (await buildifierExists()) { 24 | const updatedContent = await runBuildifier(document.fileName); 25 | 26 | // only return an edit if there is a value in `updatedContent` 27 | return !!updatedContent 28 | ? [ 29 | TextEdit.replace( 30 | new Range( 31 | 0, 32 | 0, 33 | document.lineCount - 1, 34 | document.lineAt( 35 | document.lineCount - 1 36 | ).rangeIncludingLineBreak.end.character 37 | ), 38 | updatedContent 39 | ), 40 | ] 41 | : []; 42 | } 43 | } catch (err) { 44 | window.showErrorMessage(`${err}`); 45 | return []; 46 | } 47 | } 48 | 49 | return []; 50 | }, 51 | } 52 | ); 53 | } 54 | 55 | function buildifierExists(): Promise { 56 | return new Promise((resolve, reject) => { 57 | exec( 58 | `${getBuildifierCmd()} -version`, 59 | { cwd: getWorkspaceRoot() }, 60 | (err, stdout, stderr) => { 61 | if (err) { 62 | return reject(err); 63 | } 64 | return resolve(!stderr); 65 | } 66 | ); 67 | }); 68 | } 69 | 70 | /** 71 | * Utility function used to fetch the formatted text from the `buildifier` 72 | * cmd. Uses `exec` since we want to get all of the cmd response in a single 73 | * text blob 74 | * @param bazelFile 75 | * @returns 76 | */ 77 | function runBuildifier(bazelFile: string): Promise { 78 | return new Promise((resolve, reject) => { 79 | exec( 80 | `${getBuildifierCmd()} -mode print_if_changed ${workspace.asRelativePath(bazelFile)}`, 81 | { 82 | cwd: getWorkspaceRoot(), 83 | }, 84 | (err, stdout, stderr) => { 85 | if (err) { 86 | console.error(stderr); 87 | return reject(err); 88 | } 89 | return resolve(stdout); 90 | } 91 | ); 92 | }); 93 | } 94 | 95 | function getBuildifierCmd(): string { 96 | return workspace.getConfiguration('bazel.buildifier').get('binary') 97 | ? workspace.getConfiguration('bazel.buildifier').get('binary', 'buildifier') 98 | : 'buildifier'; 99 | } 100 | -------------------------------------------------------------------------------- /src/commands.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { commands } from 'vscode'; 4 | 5 | /** 6 | * Commonly used commands 7 | */ 8 | export namespace Commands { 9 | /** 10 | * Update classpaths of one or more Bazel projects 11 | */ 12 | export const UPDATE_CLASSPATHS = 'java.bazel.updateClasspaths'; 13 | export const UPDATE_CLASSPATHS_CMD = 'java.bazel.updateClasspaths.command'; 14 | 15 | /** 16 | * Synchronize all projects of a Bazel workspace 17 | */ 18 | export const SYNC_PROJECTS = 'java.bazel.syncProjects'; 19 | export const SYNC_PROJECTS_CMD = 'java.bazel.syncProjects.command'; 20 | export const SYNC_DIRECTORIES_ONLY = 'java.bazel.syncDirectoriesOnly.command'; 21 | 22 | /** 23 | * Connect our output window 24 | */ 25 | export const REGISTER_BAZEL_TCP_SERVER_PORT = 26 | 'java.bazel.connectProcessStreamSocket'; 27 | 28 | // commands copied from vscode-java for interaction with JDT LS 29 | export const EXECUTE_WORKSPACE_COMMAND = 'java.execute.workspaceCommand'; 30 | export const JAVA_LS_LIST_SOURCEPATHS = 'java.project.listSourcePaths'; 31 | 32 | // commands copied from vscode-java for interaction with the extension 33 | export const JAVA_BUILD_WORKSPACE = 'java.workspace.compile'; 34 | export const JAVA_CLEAN_WORKSPACE = 'java.clean.workspace'; 35 | 36 | export const GET_ALL_JAVA_PROJECTS = 'java.project.getAll'; 37 | 38 | export const DEBUG_LS_CMD = 'java.bazel.debug.command'; 39 | 40 | export const OPEN_BAZEL_BUILD_STATUS_CMD = 'java.bazel.showStatus'; 41 | 42 | export const BAZEL_TARGET_REFRESH = 'bazelTaskOutline.refresh'; 43 | export const BAZEL_TARGET_RUN = 'bazelTaskOutline.run'; 44 | export const BAZEL_TARGET_KILL = 'bazelTaskOutline.kill'; 45 | 46 | export const OPEN_BAZEL_PROJECT_FILE = 'bazel.projectview.open'; 47 | 48 | export const CONVERT_PROJECT_WORKSPACE = 'bazel.convert.workspace'; 49 | } 50 | 51 | export function executeJavaLanguageServerCommand( 52 | ...rest: any[] 53 | ): Thenable { 54 | return executeJavaExtensionCommand( 55 | Commands.EXECUTE_WORKSPACE_COMMAND, 56 | ...rest 57 | ); 58 | } 59 | 60 | export function executeJavaExtensionCommand( 61 | commandName: string, 62 | ...rest: any[] 63 | ): Thenable { 64 | return commands.executeCommand(commandName, ...rest); 65 | } 66 | -------------------------------------------------------------------------------- /src/extension.api.ts: -------------------------------------------------------------------------------- 1 | import { BazelProjectView } from './types'; 2 | 3 | export interface BazelVscodeExtensionAPI { 4 | readonly parseProjectFile: BazelProjectView; 5 | } 6 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs'; 2 | import { join } from 'path'; 3 | import { format } from 'util'; 4 | import { 5 | ExtensionContext, 6 | TextDocument, 7 | commands, 8 | extensions, 9 | tasks, 10 | window, 11 | workspace, 12 | } from 'vscode'; 13 | import { 14 | BazelLanguageServerTerminal, 15 | getBazelTerminal, 16 | } from './bazelLangaugeServerTerminal'; 17 | import { getBazelProjectFile } from './bazelprojectparser'; 18 | import { BazelTaskManager } from './bazelTaskManager'; 19 | import { registerBuildifierFormatter } from './buildifier'; 20 | import { Commands, executeJavaLanguageServerCommand } from './commands'; 21 | import { BazelVscodeExtensionAPI } from './extension.api'; 22 | import { registerLSClient } from './loggingTCPServer'; 23 | import { ProjectViewManager } from './projectViewManager'; 24 | import { BazelRunTargetProvider } from './provider/bazelRunTargetProvider'; 25 | import { BazelTaskProvider } from './provider/bazelTaskProvider'; 26 | import { 27 | getWorkspaceRoot, 28 | initBazelProjectFile, 29 | isBazelWorkspaceRoot, 30 | } from './util'; 31 | 32 | const workspaceRoot = getWorkspaceRoot(); 33 | 34 | export async function activate( 35 | context: ExtensionContext 36 | ): Promise { 37 | // activates 38 | // LS processes current .eclipse/.bazelproject file 39 | // if it DNE create one 40 | // register TCP port with LS 41 | // project view should reflect what's in the LS 42 | // show any directories listed in the .bazelproject file 43 | // fetch all projects loaded into LS and display those as well 44 | // show .eclipse folder 45 | // 46 | 47 | window.registerTreeDataProvider( 48 | 'bazelTaskOutline', 49 | BazelRunTargetProvider.instance 50 | ); 51 | tasks.registerTaskProvider('bazel', new BazelTaskProvider()); 52 | 53 | BazelLanguageServerTerminal.trace('extension activated'); 54 | 55 | workspace.onDidSaveTextDocument((doc) => { 56 | if (doc.fileName.includes('bazelproject')) { 57 | toggleBazelProjectSyncStatus(doc); 58 | } 59 | }); 60 | 61 | context.subscriptions.push( 62 | commands.registerCommand( 63 | Commands.OPEN_BAZEL_BUILD_STATUS_CMD, 64 | getBazelTerminal().show 65 | ) 66 | ); 67 | 68 | commands.executeCommand( 69 | 'setContext', 70 | 'isBazelWorkspaceRoot', 71 | isBazelWorkspaceRoot() 72 | ); 73 | commands.executeCommand( 74 | 'setContext', 75 | 'isMultiRoot', 76 | workspace.workspaceFile?.fsPath.includes('code-workspace') 77 | ); 78 | // create .eclipse/.bazelproject file if DNE 79 | if (isBazelWorkspaceRoot()) { 80 | initBazelProjectFile(); 81 | const showBazelprojectConfig = 82 | workspace.getConfiguration('bazel.projectview'); 83 | if (showBazelprojectConfig.get('open')) { 84 | openBazelProjectFile(); 85 | showBazelprojectConfig.update('open', false); // only open this file on the first activation of this extension 86 | } 87 | syncProjectViewDirectories(); 88 | context.subscriptions.push( 89 | commands.registerCommand(Commands.OPEN_BAZEL_PROJECT_FILE, () => 90 | openBazelProjectFile() 91 | ) 92 | ); 93 | } 94 | 95 | context.subscriptions.push( 96 | commands.registerCommand(Commands.SYNC_PROJECTS_CMD, syncProjectView) 97 | ); 98 | context.subscriptions.push( 99 | commands.registerCommand( 100 | Commands.SYNC_DIRECTORIES_ONLY, 101 | syncProjectViewDirectories 102 | ) 103 | ); 104 | context.subscriptions.push( 105 | commands.registerCommand(Commands.UPDATE_CLASSPATHS_CMD, updateClasspaths) 106 | ); 107 | context.subscriptions.push( 108 | commands.registerCommand(Commands.DEBUG_LS_CMD, runLSCmd) 109 | ); 110 | context.subscriptions.push( 111 | commands.registerCommand( 112 | Commands.BAZEL_TARGET_REFRESH, 113 | BazelTaskManager.refreshTasks 114 | ) 115 | ); 116 | context.subscriptions.push( 117 | commands.registerCommand( 118 | Commands.BAZEL_TARGET_RUN, 119 | BazelTaskManager.runTask 120 | ) 121 | ); 122 | context.subscriptions.push( 123 | commands.registerCommand( 124 | Commands.BAZEL_TARGET_KILL, 125 | BazelTaskManager.killTask 126 | ) 127 | ); 128 | 129 | context.subscriptions.push( 130 | commands.registerCommand( 131 | Commands.CONVERT_PROJECT_WORKSPACE, 132 | ProjectViewManager.covertToMultiRoot 133 | ) 134 | ); 135 | 136 | registerBuildifierFormatter(); 137 | 138 | // trigger a refresh of the tree view when any task get executed 139 | tasks.onDidStartTask((_) => BazelRunTargetProvider.instance.refresh()); 140 | tasks.onDidEndTask((_) => BazelRunTargetProvider.instance.refresh()); 141 | 142 | // always update the project view after the initial project load 143 | registerLSClient(); 144 | 145 | return Promise.resolve({ 146 | parseProjectFile: await getBazelProjectFile(), 147 | }); 148 | } 149 | 150 | export function deactivate() {} 151 | 152 | function syncProjectView(): void { 153 | if (!isRedhatJavaReady()) { 154 | window.showErrorMessage( 155 | 'Unable to sync project view. Java language server is not ready' 156 | ); 157 | return; 158 | } 159 | 160 | const launchMode = workspace 161 | .getConfiguration('java.server') 162 | .get('launchMode'); 163 | // if the launchMode is not Standard it should be changed and the window reloaded to apply that change 164 | if (!launchMode || launchMode !== 'Standard') { 165 | workspace 166 | .getConfiguration('java.server') 167 | .update('launchMode', 'Standard') 168 | .then(() => commands.executeCommand('workbench.action.reloadWindow')); 169 | } 170 | 171 | executeJavaLanguageServerCommand(Commands.SYNC_PROJECTS).then( 172 | syncProjectViewDirectories 173 | ); 174 | } 175 | 176 | function updateClasspaths() { 177 | if (!isRedhatJavaReady()) { 178 | window.showErrorMessage( 179 | 'Unable to update classpath. Java language server is not ready' 180 | ); 181 | return; 182 | } 183 | } 184 | 185 | function runLSCmd() { 186 | if (!isRedhatJavaReady()) { 187 | window.showErrorMessage( 188 | 'Unable to execute LS cmd. Java language server is not ready' 189 | ); 190 | return; 191 | } 192 | window 193 | .showInputBox({ 194 | value: Commands.JAVA_LS_LIST_SOURCEPATHS, 195 | }) 196 | .then((cmd) => { 197 | if (cmd) { 198 | const [lsCmd, args] = cmd.trim().split(/\s(.*)/s); 199 | executeJavaLanguageServerCommand(lsCmd, args).then( 200 | (resp) => BazelLanguageServerTerminal.info(format(resp)), 201 | (err) => BazelLanguageServerTerminal.error(format(err)) 202 | ); 203 | } 204 | }); 205 | } 206 | 207 | function isRedhatJavaReady(): boolean { 208 | const javaExtension = extensions.getExtension('redhat.java')?.exports; 209 | if (javaExtension) { 210 | return javaExtension.status === 'Started'; 211 | } 212 | return false; 213 | } 214 | 215 | function toggleBazelProjectSyncStatus(doc: TextDocument) { 216 | if (workspace.getConfiguration('bazel.projectview').get('notification')) { 217 | window 218 | .showWarningMessage( 219 | `The Bazel Project View changed. Do you want to synchronize? [details](https://github.com/salesforce/bazel-eclipse/blob/main/docs/common/projectviews.md#project-views)`, 220 | ...['Java Projects', 'Only Directories', 'Do Nothing'] 221 | ) 222 | .then((val) => { 223 | if (val === 'Java Projects') { 224 | syncProjectView(); 225 | } else if (val === 'Only Directories') { 226 | syncProjectViewDirectories(); 227 | } else if (val === 'Do Nothing') { 228 | workspace 229 | .getConfiguration('bazel.projectview') 230 | .update('notification', false); 231 | } 232 | }); 233 | } 234 | } 235 | 236 | function syncProjectViewDirectories() { 237 | ProjectViewManager.updateProjectView(); 238 | } 239 | 240 | function openBazelProjectFile() { 241 | try { 242 | const projectViewPath = join(workspaceRoot, '.eclipse', '.bazelproject'); 243 | if (existsSync(projectViewPath)) { 244 | workspace 245 | .openTextDocument(projectViewPath) 246 | .then((f) => window.showTextDocument(f)); 247 | } else { 248 | window.showErrorMessage(`${projectViewPath} does not exist`); 249 | } 250 | } catch (err) { 251 | window.showErrorMessage( 252 | 'Unable to open the bazel project file; invalid workspace' 253 | ); 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/jdtls.extension.api.ts: -------------------------------------------------------------------------------- 1 | import { CancellationToken, Command, Event, ProviderResult, Uri } from 'vscode'; 2 | import { 3 | DefinitionParams, 4 | DocumentSymbol, 5 | DocumentSymbolParams, 6 | Location, 7 | LocationLink, 8 | SymbolInformation, 9 | TextDocumentPositionParams, 10 | } from 'vscode-languageclient'; 11 | 12 | type DocumentSymbolsResponse = DocumentSymbol[] | SymbolInformation[] | null; 13 | export type GetDocumentSymbolsCommand = ( 14 | params: DocumentSymbolParams, 15 | token?: CancellationToken 16 | ) => Promise; 17 | 18 | type GoToDefinitionResponse = Location | Location[] | LocationLink[] | null; 19 | export type GoToDefinitionCommand = ( 20 | params: DefinitionParams, 21 | token?: CancellationToken 22 | ) => Promise; 23 | 24 | /* eslint-disable @typescript-eslint/naming-convention */ 25 | export interface RequirementsData { 26 | tooling_jre: string; 27 | tooling_jre_version: number; 28 | java_home: string; 29 | java_version: number; 30 | } 31 | /* eslint-enable @typescript-eslint/naming-convention */ 32 | 33 | export enum ServerMode { 34 | standard = 'Standard', 35 | lightWeight = 'LightWeight', 36 | hybrid = 'Hybrid', 37 | } 38 | 39 | export type ProvideHoverCommandFn = ( 40 | params: TextDocumentPositionParams, 41 | token: CancellationToken 42 | ) => ProviderResult; 43 | export type RegisterHoverCommand = (callback: ProvideHoverCommandFn) => void; 44 | 45 | /** 46 | * Gets the project settings. This API is not supported in light weight server mode so far. 47 | * @param uri Uri of the file that needs to be queried. Accepted uris are: source file, class file and project root path. 48 | * @param OptionKeys the settings we want to query, for example: ["org.eclipse.jdt.core.compiler.compliance", "org.eclipse.jdt.core.compiler.source"]. 49 | * Besides the options defined in JavaCore, the following keys can also be used: 50 | * - "org.eclipse.jdt.ls.core.vm.location": Get the location of the VM assigned to build the given Java project 51 | * - "org.eclipse.jdt.ls.core.sourcePaths": Get the source root paths of the given Java project 52 | * - "org.eclipse.jdt.ls.core.outputPath": Get the default output path of the given Java project. Note that the default output path 53 | * may not be equal to the output path of each source root. 54 | * - "org.eclipse.jdt.ls.core.referencedLibraries": Get all the referenced library files of the given Java project 55 | * @returns An object with all the optionKeys. 56 | * @throws Will throw errors if the Uri does not belong to any project. 57 | */ 58 | 59 | export type GetProjectSettingsCommand = ( 60 | uri: string, 61 | SettingKeys: string[] 62 | ) => Promise; 63 | 64 | /** 65 | * Gets the classpaths and modulepaths. This API is not supported in light weight server mode so far. 66 | * @param uri Uri of the file that needs to be queried. Accepted uris are: source file, class file and project root path. 67 | * @param options Query options. 68 | * @returns ClasspathResult containing both classpaths and modulepaths. 69 | * @throws Will throw errors if the Uri does not belong to any project. 70 | */ 71 | 72 | export type GetClasspathsCommand = ( 73 | uri: string, 74 | options: ClasspathQueryOptions 75 | ) => Promise; 76 | export type ClasspathQueryOptions = { 77 | /** 78 | * Determines the scope of the classpath. Valid scopes are "runtime" and "test". 79 | * If the given scope is not supported, "runtime" will be used. 80 | */ 81 | scope: string; 82 | }; 83 | 84 | export type ClasspathResult = { 85 | /** 86 | * Uri string of the project root path. 87 | */ 88 | projectRoot: string; 89 | /** 90 | * File path array for the classpaths. 91 | */ 92 | classpaths: string[]; 93 | /** 94 | * File path array for the modulepaths. 95 | */ 96 | modulepaths: string[]; 97 | }; 98 | 99 | /** 100 | * Checks if the input uri is a test source file or not. This API is not supported in light weight server mode so far. 101 | * @param uri Uri of the file that needs to be queried. Accepted uris are: source file, class file and project root path. 102 | * @returns `true` if the input uri is a test file in its belonging project, otherwise returns false. 103 | * @throws Will throw errors if the Uri does not belong to any project. 104 | */ 105 | export type IsTestFileCommand = (uri: string) => Promise; 106 | 107 | export enum ClientStatus { 108 | uninitialized = 'Uninitialized', 109 | initialized = 'Initialized', 110 | starting = 'Starting', 111 | started = 'Started', 112 | error = 'Error', 113 | stopping = 'Stopping', 114 | } 115 | 116 | export interface TraceEvent { 117 | /** 118 | * Request type. 119 | */ 120 | type: string; 121 | /** 122 | * Time (in milliseconds) taken to process a request. 123 | */ 124 | duration?: number; 125 | /** 126 | * Error that occurs while processing a request. 127 | */ 128 | error?: any; 129 | /** 130 | * The number of results returned by a response. 131 | */ 132 | resultLength?: number | undefined; 133 | /** 134 | * Additional data properties, such as the completion trigger context. 135 | */ 136 | data?: any; 137 | } 138 | 139 | export interface SourceInvalidatedEvent { 140 | /** 141 | * The paths of the jar files that are linked to new source attachments. 142 | */ 143 | affectedRootPaths: string[]; 144 | /** 145 | * The opened editors with updated source. 146 | */ 147 | affectedEditorDocuments?: Uri[]; 148 | } 149 | 150 | export const extensionApiVersion = '0.12'; 151 | 152 | export interface JavaExtensionAPI { 153 | readonly apiVersion: string; 154 | readonly javaRequirement: RequirementsData; 155 | status: ClientStatus; 156 | readonly registerHoverCommand: RegisterHoverCommand; 157 | readonly getDocumentSymbols: GetDocumentSymbolsCommand; 158 | readonly getProjectSettings: GetProjectSettingsCommand; 159 | readonly getClasspaths: GetClasspathsCommand; 160 | readonly isTestFile: IsTestFileCommand; 161 | /** 162 | * An event which fires on classpath update. This API is not supported in light weight server mode so far. 163 | * 164 | * Note: 165 | * 1. This event will fire when the project's configuration file (e.g. pom.xml for Maven) get changed, 166 | * but the classpaths might still be the same as before. 167 | * 2. The Uri points to the project root path. 168 | */ 169 | readonly onDidClasspathUpdate: Event; 170 | /** 171 | * An event fires on projects imported. This API is not supported in light weight server mode so far. 172 | * The Uris in the array point to the project root path. 173 | */ 174 | readonly onDidProjectsImport: Event; 175 | readonly goToDefinition: GoToDefinitionCommand; 176 | /** 177 | * Indicates the current active mode for Java Language Server. Possible modes are: 178 | * - "Standard" 179 | * - "Hybrid" 180 | * - "LightWeight" 181 | */ 182 | serverMode: ServerMode; 183 | /** 184 | * An event which fires when the server mode has been switched. 185 | */ 186 | readonly onDidServerModeChange: Event; 187 | 188 | /** 189 | * A promise that will be resolved when the standard language server is ready. 190 | * Note: The server here denotes for the standard server, not the lightweight. 191 | * @since API version 0.7 192 | * @since extension version 1.7.0 193 | */ 194 | readonly serverReady: () => Promise; 195 | 196 | /** 197 | * An event that's fired when a request is about to send to language server. 198 | * @since API version 0.12 199 | * @since extension version 1.23.0 200 | */ 201 | readonly onWillRequestStart: Event; 202 | 203 | /** 204 | * An event that's fired when a request has been responded. 205 | * @since API version 0.8 206 | * @since extension version 1.16.0 207 | */ 208 | readonly onDidRequestEnd: Event; 209 | 210 | /** 211 | * Allow 3rd party trace handler to track the language client & server error events. 212 | * 213 | * @since API version 0.9 214 | * @since extension version 1.20.0 215 | */ 216 | readonly trackEvent: Event; 217 | 218 | /** 219 | * An event that occurs when the package fragment roots have updated source attachments. 220 | * The client should refresh the new source if it has previously requested the source 221 | * from them. 222 | * 223 | * @since API version 0.10 224 | * @since extension version 1.21.0 225 | */ 226 | readonly onDidSourceInvalidate: Event; 227 | } 228 | -------------------------------------------------------------------------------- /src/loggingTCPServer.ts: -------------------------------------------------------------------------------- 1 | import { AddressInfo, Server, Socket, createServer } from 'net'; 2 | import { setTimeout } from 'timers/promises'; 3 | import { commands, workspace } from 'vscode'; 4 | import { BazelLanguageServerTerminal } from './bazelLangaugeServerTerminal'; 5 | import { Commands } from './commands'; 6 | 7 | const SERVER_START_RETRIES = 10; 8 | const PORT_REGISTRATION_RETRIES = 10; 9 | const RETRY_INTERVAL = 5000; // ms 10 | 11 | let server: Server | undefined; 12 | 13 | function startTCPServer(attempts = 0): Promise { 14 | let port = 0; 15 | if (workspace.getConfiguration('java').has('jdt.ls.vmargs')) { 16 | const vmargs = workspace 17 | .getConfiguration('java') 18 | .get('jdt.ls.vmargs'); 19 | if (vmargs?.includes('java.bazel.staticProcessStreamSocket')) { 20 | port = parseInt( 21 | vmargs 22 | .split(/\s+/) 23 | .filter((x) => x.includes('java.bazel.staticProcessStreamSocket'))[0] 24 | .split('=')[1] 25 | ); 26 | } 27 | } 28 | 29 | return new Promise((resolve) => { 30 | if (!server) { 31 | server = createServer((sock: Socket) => { 32 | attempts = 0; 33 | 34 | sock.pipe(BazelLanguageServerTerminal.stream()); 35 | 36 | sock.on('end', () => { 37 | sock.unpipe(BazelLanguageServerTerminal.stream()); 38 | }); 39 | sock.on('error', (err: Error) => { 40 | BazelLanguageServerTerminal.error(err.message); 41 | sock.end(); 42 | }); 43 | }); 44 | } 45 | server.listen(port, 'localhost', () => { 46 | if (server) { 47 | const address = server.address(); 48 | if (address) { 49 | const port = (address as AddressInfo).port; 50 | BazelLanguageServerTerminal.debug( 51 | `Bazel log server listening on port ${port}` 52 | ); 53 | resolve(port); 54 | } 55 | } else { 56 | BazelLanguageServerTerminal.error(`Failed to start bazel TCP server`); 57 | setTimeout(1000 * attempts).then(() => 58 | startTCPServer(attempts + 1) 59 | ); 60 | } 61 | }); 62 | 63 | server.on('error', (err: Error) => { 64 | console.error(err.message); 65 | BazelLanguageServerTerminal.error(err.message); 66 | }); 67 | }); 68 | } 69 | 70 | export function registerLSClient(): Promise { 71 | return startTCPServer() 72 | .then((port) => registerPortWithLanguageServer(port)) 73 | .catch((err) => 74 | BazelLanguageServerTerminal.error( 75 | `Failed to register port with BLS: ${err.message}` 76 | ) 77 | ); 78 | } 79 | 80 | async function registerPortWithLanguageServer( 81 | port: number, 82 | attempts = 0, 83 | maxRetries = 50 84 | ): Promise { 85 | let error = null; 86 | for (let i = 0; i < maxRetries; i++) { 87 | try { 88 | return await commands 89 | .executeCommand( 90 | Commands.EXECUTE_WORKSPACE_COMMAND, 91 | Commands.REGISTER_BAZEL_TCP_SERVER_PORT, 92 | port 93 | ) 94 | .then(() => 95 | BazelLanguageServerTerminal.trace(`port ${port} registered with BLS`) 96 | ); 97 | } catch (err) { 98 | error = err; 99 | console.error(`register port failed ${attempts} : ${err}`); 100 | await setTimeout(i * 1000); 101 | } 102 | } 103 | return Promise.reject(error); 104 | } 105 | -------------------------------------------------------------------------------- /src/projectViewManager.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from 'fs'; 2 | import { 3 | commands, 4 | ConfigurationTarget, 5 | FileType, 6 | Uri, 7 | window, 8 | workspace, 9 | } from 'vscode'; 10 | import { BazelLanguageServerTerminal } from './bazelLangaugeServerTerminal'; 11 | import { getBazelProjectFile } from './bazelprojectparser'; 12 | import { ExcludeConfig, FileWatcherExcludeConfig } from './types'; 13 | import { getVscodeConfig, getWorkspaceRoot } from './util'; 14 | 15 | export namespace ProjectViewManager { 16 | const workspaceRoot = getWorkspaceRoot(); 17 | const workspaceRootName = workspaceRoot.split('/').reverse()[0]; 18 | 19 | function isMultiRoot(): boolean { 20 | return !!workspace.workspaceFile; 21 | } 22 | 23 | /** 24 | * Utility method to convert a single root workspace to a multi-root 25 | */ 26 | export function covertToMultiRoot() { 27 | if (!isMultiRoot()) { 28 | window 29 | .showWarningMessage( 30 | 'This will convert your project to a multi-root project. After this operation completes your window will be reloaded', 31 | 'Proceed', 32 | 'Cancel' 33 | ) 34 | .then(async (action) => { 35 | if (action === 'Proceed') { 36 | const workspaceFile = { 37 | folders: [{ path: '.eclipse' }], 38 | settings: await getVscodeConfig('settings').then(cleanSettings), 39 | launch: await getVscodeConfig('launch'), 40 | tasks: await getVscodeConfig('tasks'), 41 | extensions: await getVscodeConfig('extensions'), 42 | }; 43 | writeFileSync( 44 | `${workspaceRoot}/workspace.code-workspace`, 45 | Buffer.from(JSON.stringify(workspaceFile, null, 2)) 46 | ); 47 | 48 | // cleanup all old single root workspace files 49 | await workspace.fs.delete(Uri.file(`${workspaceRoot}/.vscode`), { 50 | recursive: true, 51 | }); 52 | 53 | // reload the window using the new workspace 54 | commands.executeCommand( 55 | 'vscode.openFolder', 56 | Uri.file(`${workspaceRoot}/workspace.code-workspace`) 57 | ); 58 | } 59 | return; 60 | }); 61 | } else { 62 | window.showInformationMessage( 63 | 'This project is already in multi-root mode.' 64 | ); 65 | } 66 | } 67 | 68 | export async function updateProjectView() { 69 | BazelLanguageServerTerminal.debug('Syncing bazel project view'); 70 | 71 | getDisplayFolders().then((df) => { 72 | if (isMultiRoot()) { 73 | return updateMultiRootProjectView(df) 74 | .then(rootDirOnly) 75 | .then(updateFileWatcherExclusion); 76 | } 77 | return updateSingleRootProjectView(rootDirOnly(df)).then( 78 | updateFileWatcherExclusion 79 | ); 80 | }); 81 | } 82 | 83 | async function getDisplayFolders(): Promise { 84 | let displayFolders = new Set(['.eclipse']); // TODO bubble this out to a setting 85 | if (isMultiRoot()) { 86 | displayFolders.add('.'); 87 | } 88 | try { 89 | const bazelProjectFile = await getBazelProjectFile(); 90 | if (bazelProjectFile.directories.includes('.')) { 91 | displayFolders = new Set(['.']); 92 | } else { 93 | bazelProjectFile.directories.forEach((d) => { 94 | displayFolders.add(d); 95 | }); 96 | bazelProjectFile.targets.forEach((t) => 97 | displayFolders.add( 98 | t.replace('//', '').replace(/:.*/, '').replace(/\/.*/, '') 99 | ) 100 | ); 101 | } 102 | 103 | return [...displayFolders]; 104 | } catch (err) { 105 | throw new Error(`Could not read bazelproject file: ${err}`); 106 | } 107 | } 108 | 109 | function updateSingleRootProjectView( 110 | displayFolders: string[] 111 | ): Thenable { 112 | const viewAll = displayFolders.includes('.'); 113 | const workspaceFilesConfig = workspace.getConfiguration('files'); 114 | return workspace.fs 115 | .readDirectory(Uri.parse(workspaceRoot)) 116 | .then((val) => val.filter((x) => x[1] !== FileType.File).map((d) => d[0])) 117 | .then((dirs) => { 118 | const filesExclude = workspaceFilesConfig.get( 119 | 'exclude' 120 | ) as ExcludeConfig; 121 | dirs.forEach( 122 | (d) => 123 | (filesExclude[d] = viewAll ? false : !displayFolders.includes(d)) 124 | ); 125 | return filesExclude; 126 | }) 127 | .then((filesExclude) => 128 | workspaceFilesConfig.update( 129 | 'exclude', 130 | filesExclude, 131 | ConfigurationTarget.Workspace 132 | ) 133 | ) 134 | .then(() => displayFolders); 135 | } 136 | 137 | function updateMultiRootProjectView( 138 | displayFolders: string[] 139 | ): Thenable { 140 | workspace.updateWorkspaceFolders( 141 | 0, 142 | workspace.workspaceFolders?.length, 143 | ...displayFolders.map((f) => { 144 | const moduleUri = Uri.file(`${workspaceRoot}/${f}`); 145 | let moduleName = f; 146 | if (moduleName === '.') { 147 | moduleName = workspaceRootName; 148 | } 149 | if (f.includes('/')) { 150 | moduleName = f.replaceAll('/', ' ⇾ '); 151 | } 152 | return { 153 | uri: moduleUri, 154 | name: moduleName, 155 | }; 156 | }) 157 | ); 158 | return Promise.resolve(displayFolders); 159 | } 160 | 161 | function updateFileWatcherExclusion( 162 | displayFolders: string[] 163 | ): Thenable { 164 | const workspaceFilesConfig = workspace.getConfiguration('files'); 165 | BazelLanguageServerTerminal.debug('updating files.watcherExclude setting'); 166 | 167 | const filesWatcherExclude = 168 | workspaceFilesConfig.get('watcherExclude', {}); 169 | 170 | const fileWatcherKeys = Object.keys(filesWatcherExclude); 171 | const hasOldEntry = fileWatcherKeys.filter((k) => 172 | k.includes('.eclipse') 173 | ).length; 174 | 175 | const viewAll = displayFolders.includes('.') && !isMultiRoot(); 176 | 177 | const fileWatcherExcludePattern = viewAll 178 | ? '' 179 | : `**/!(${Array.from(displayFolders.filter((s) => s !== '.').sort()).join('|')})/**`; 180 | 181 | if (viewAll) { 182 | // if viewAll and existing config doesn't contain .eclipse return 183 | if (!hasOldEntry) { 184 | return Promise.resolve(); 185 | } 186 | } else { 187 | // if !viewAll and existing config contains identical entry return 188 | if (fileWatcherKeys.includes(fileWatcherExcludePattern)) { 189 | return Promise.resolve(); 190 | } 191 | } 192 | 193 | // copy the old config obj, but remove any previous exclude based on the .bazelproject file 194 | const newFilesWatcherExclude: FileWatcherExcludeConfig = {}; 195 | for (const val in filesWatcherExclude) { 196 | if (!val.includes('.eclipse')) { 197 | newFilesWatcherExclude[val] = filesWatcherExclude[val]; 198 | } 199 | } 200 | 201 | if (fileWatcherExcludePattern) { 202 | newFilesWatcherExclude[fileWatcherExcludePattern] = true; 203 | } 204 | 205 | return workspaceFilesConfig 206 | .update('watcherExclude', newFilesWatcherExclude) 207 | .then((x) => 208 | window 209 | .showWarningMessage( 210 | 'File watcher exclusions are out of date. Please reload the window to apply the change', 211 | ...['Reload', 'Ignore'] 212 | ) 213 | .then((opt) => { 214 | if (opt === 'Reload') { 215 | commands.executeCommand('workbench.action.reloadWindow'); 216 | } 217 | if (opt === 'Ignore') { 218 | workspace 219 | .getConfiguration('bazel.projectview') 220 | .update('updateFileWatcherExclusion', false); 221 | } 222 | }) 223 | ); 224 | } 225 | 226 | /** 227 | * 228 | * @param settingsObj used to clear the 'old' files.exclude entry that 229 | * was used to modify the project view 230 | * @returns 231 | */ 232 | function cleanSettings(settingsObj: Object | undefined): Object | undefined { 233 | if (settingsObj) { 234 | if ('files.exclude' in settingsObj) { 235 | delete settingsObj['files.exclude']; 236 | } 237 | } 238 | 239 | return settingsObj; 240 | } 241 | 242 | function rootDirOnly(dirs: string[]): string[] { 243 | return dirs.map((d) => d.split('/')[0]); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/provider/bazelRunTargetProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Command, 3 | Event, 4 | EventEmitter, 5 | ProviderResult, 6 | Task, 7 | TaskExecution, 8 | ThemeIcon, 9 | TreeDataProvider, 10 | TreeItem, 11 | TreeItemCollapsibleState, 12 | tasks, 13 | } from 'vscode'; 14 | 15 | export class BazelRunTargetProvider 16 | implements TreeDataProvider 17 | { 18 | private static _instance: BazelRunTargetProvider; 19 | 20 | private _onDidChangeTreeData: EventEmitter< 21 | BazelRunTarget | undefined | void 22 | > = new EventEmitter(); 23 | readonly onDidChangeTreeData: Event = 24 | this._onDidChangeTreeData.event; 25 | 26 | private constructor() {} 27 | 28 | public static get instance(): BazelRunTargetProvider { 29 | if (!BazelRunTargetProvider._instance) { 30 | BazelRunTargetProvider._instance = new BazelRunTargetProvider(); 31 | } 32 | return BazelRunTargetProvider._instance; 33 | } 34 | 35 | refresh(...args: any[]): void { 36 | this._onDidChangeTreeData.fire(); 37 | } 38 | 39 | getTreeItem(element: BazelRunTarget): TreeItem | Thenable { 40 | return element; 41 | } 42 | getChildren( 43 | element?: BazelRunTarget | undefined 44 | ): ProviderResult { 45 | return tasks 46 | .fetchTasks({ type: 'bazel' }) 47 | .then((bt) => 48 | bt.map((t) => new BazelRunTarget(t, TreeItemCollapsibleState.None)) 49 | ); 50 | } 51 | } 52 | 53 | export class BazelRunTarget extends TreeItem { 54 | public readonly task: Task | undefined; 55 | public readonly execution: TaskExecution | undefined; 56 | 57 | constructor( 58 | task: Task, 59 | collapsibleState: TreeItemCollapsibleState, 60 | command?: Command 61 | ) { 62 | super(task.name, collapsibleState); 63 | this.task = task; 64 | this.command = command; 65 | this.execution = tasks.taskExecutions.find( 66 | (e) => e.task.name === this.task?.name && e.task.source === task.source 67 | ); 68 | this.contextValue = this.execution ? 'runningTask' : 'task'; 69 | if (this.execution) { 70 | this.iconPath = new ThemeIcon('sync~spin'); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/provider/bazelSyncStatusProvider.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, lstatSync, readdirSync } from 'fs'; 2 | import * as path from 'path'; 3 | import { 4 | CancellationToken, 5 | Command, 6 | Event, 7 | EventEmitter, 8 | FileSystemWatcher, 9 | ProviderResult, 10 | RelativePattern, 11 | TreeDataProvider, 12 | TreeItem, 13 | TreeItemCollapsibleState, 14 | Uri, 15 | window, 16 | workspace, 17 | } from 'vscode'; 18 | import { Commands, executeJavaLanguageServerCommand } from '../commands'; 19 | import { UpdateClasspathResponse } from '../types'; 20 | 21 | const SYNCED = 'synced'; 22 | const UNSYNCED = 'unsynced'; 23 | 24 | export class BazelSyncStatusProvider implements TreeDataProvider { 25 | private _onDidChangeTreeData: EventEmitter = 26 | new EventEmitter(); 27 | readonly onDidChangeTreeData: Event = 28 | this._onDidChangeTreeData.event; 29 | private treeData: SyncStatus[] = []; 30 | 31 | constructor(private workspaceRoot: string | undefined) { 32 | this.workspaceRoot = workspaceRoot; 33 | } 34 | 35 | refresh(): void { 36 | this._onDidChangeTreeData.fire(); 37 | } 38 | 39 | getTreeItem(element: SyncStatus): TreeItem | Thenable { 40 | return element; 41 | } 42 | getChildren(element?: SyncStatus | undefined): ProviderResult { 43 | if (!this.workspaceRoot) { 44 | window.showInformationMessage('empty workspace'); 45 | return Promise.resolve([]); 46 | } 47 | 48 | // we never have children of children, so we only care about the case where no element is passed in 49 | if (!element) { 50 | if (!this.treeData || !this.treeData.length) { 51 | return this.buildTreeData(); 52 | } 53 | 54 | return Promise.resolve(this.treeData); 55 | } 56 | 57 | return Promise.resolve([]); 58 | } 59 | getParent?(element: SyncStatus): ProviderResult { 60 | throw new Error('Method not implemented.'); 61 | } 62 | resolveTreeItem?( 63 | item: TreeItem, 64 | element: SyncStatus, 65 | token: CancellationToken 66 | ): ProviderResult { 67 | throw new Error('Method not implemented.'); 68 | } 69 | 70 | buildTreeData(): Promise { 71 | return new Promise((resolve, reject) => { 72 | executeJavaLanguageServerCommand( 73 | Commands.JAVA_LS_LIST_SOURCEPATHS 74 | ).then( 75 | (resp) => { 76 | this.treeData = resp.data.map((cp) => { 77 | const moduleBuildFile = getModuleBuildFile(cp.path); 78 | return new SyncStatus( 79 | cp.projectName, 80 | workspace.createFileSystemWatcher( 81 | new RelativePattern(path.dirname(moduleBuildFile), 'BUILD*') 82 | ), 83 | TreeItemCollapsibleState.None, 84 | { 85 | title: 'sync module', 86 | command: Commands.UPDATE_CLASSPATHS_CMD, 87 | arguments: [moduleBuildFile], 88 | } 89 | ); 90 | }); 91 | resolve(this.treeData); 92 | }, 93 | (err) => reject(err) 94 | ); 95 | }); 96 | } 97 | 98 | clearTreeData() { 99 | this.treeData.forEach((td) => { 100 | td.watcher.dispose(); 101 | }); 102 | this.treeData = []; 103 | } 104 | } 105 | 106 | export class SyncStatus extends TreeItem { 107 | public modifiedBuildFiles: Set = new Set(); 108 | 109 | constructor( 110 | public readonly label: string, 111 | public readonly watcher: FileSystemWatcher, 112 | public readonly collapsibleState: TreeItemCollapsibleState, 113 | public readonly command?: Command 114 | ) { 115 | super(label, collapsibleState); 116 | this.tooltip = this.label; 117 | this.description = SYNCED; 118 | 119 | this.watcher.onDidChange((uri) => { 120 | this.modifiedBuildFiles.add(uri); 121 | this.description = UNSYNCED; 122 | }); 123 | } 124 | 125 | iconPath = { 126 | light: path.join(__filename, '..', 'resources', 'light', 'pass.svg'), 127 | dark: path.join(__filename, '..', 'resources', 'dark', 'pass.svg'), 128 | }; 129 | 130 | clearState() { 131 | this.modifiedBuildFiles.clear(); 132 | this.description = SYNCED; 133 | } 134 | } 135 | 136 | // from a given starting dir, walk up the file tree until a BUILD file is found. return it 137 | function getModuleBuildFile(projectPath: string): string { 138 | if (existsSync(projectPath)) { 139 | if (lstatSync(projectPath).isDirectory()) { 140 | for (const x of readdirSync(projectPath)) { 141 | if ( 142 | lstatSync(path.join(projectPath, x)).isFile() && 143 | x.includes('BUILD') 144 | ) { 145 | return path.join(projectPath, x); 146 | } 147 | } 148 | } 149 | } else { 150 | throw new Error(`invalide path: ${projectPath}`); 151 | } 152 | return getModuleBuildFile(path.dirname(projectPath)); 153 | } 154 | -------------------------------------------------------------------------------- /src/provider/bazelTaskProvider.ts: -------------------------------------------------------------------------------- 1 | import { XMLParser } from 'fast-xml-parser'; 2 | import { readFileSync } from 'fs'; 3 | import { join } from 'path'; 4 | import { 5 | ShellExecution, 6 | Task, 7 | TaskDefinition, 8 | TaskProvider, 9 | TaskScope, 10 | } from 'vscode'; 11 | import { BazelLanguageServerTerminal } from '../bazelLangaugeServerTerminal'; 12 | import { getBazelProjectFile } from '../bazelprojectparser'; 13 | import { getWorkspaceRoot } from '../util'; 14 | 15 | const parser = new XMLParser({ 16 | ignoreAttributes: false, 17 | allowBooleanAttributes: true, 18 | }); 19 | 20 | export class BazelTaskProvider implements TaskProvider { 21 | static BazelType = 'bazel'; 22 | private bazelPromise: Thenable | undefined = undefined; 23 | 24 | public provideTasks(): Thenable | undefined { 25 | if (!this.bazelPromise) { 26 | this.bazelPromise = getBazelTasks(); 27 | } 28 | return this.bazelPromise; 29 | } 30 | 31 | public resolveTask(task: Task): Task | undefined { 32 | const taskDef = task.definition.task; 33 | if (taskDef) { 34 | // resolveTask requires that the same definition object be used. 35 | const definition: BazelTaskDefinition = task.definition; 36 | return new Task( 37 | definition, 38 | task.scope ?? TaskScope.Workspace, 39 | definition.name, 40 | definition.type, 41 | new ShellExecution(`${definition.task}`) 42 | ); 43 | } 44 | return undefined; 45 | } 46 | } 47 | 48 | class BazelTaskDefinition implements TaskDefinition { 49 | type = 'bazel'; 50 | name: string; 51 | task: string; 52 | 53 | constructor(name: string, task: string) { 54 | this.name = name; 55 | this.task = task; 56 | } 57 | } 58 | 59 | async function getBazelTasks(): Promise { 60 | // setup default bazel tasks 61 | const taskDefinitions: BazelTaskDefinition[] = []; 62 | 63 | // add any ij converted run targets to vscode tasks 64 | const bazelProjectFile = await getBazelProjectFile(); 65 | if (bazelProjectFile.importRunConfigurations) { 66 | const rootPath = getWorkspaceRoot(); 67 | bazelProjectFile.importRunConfigurations.forEach((runConfig) => { 68 | const rconf = getIJRunConfig(join(rootPath, runConfig)); 69 | if (rconf) { 70 | taskDefinitions.push(rconf); 71 | } 72 | }); 73 | } 74 | 75 | return taskDefinitions.map( 76 | (value) => 77 | new Task( 78 | value, 79 | TaskScope.Workspace, 80 | `${value.name}`, 81 | `${value.type}`, 82 | new ShellExecution(`${value.task}`), 83 | [] 84 | ) 85 | ); 86 | } 87 | 88 | function getIJRunConfig(configPath: string): BazelTaskDefinition | undefined { 89 | const ijRunConfig = parser.parse( 90 | readFileSync(configPath, { encoding: 'utf-8' }) 91 | ); 92 | if (typeof ijRunConfig === 'object') { 93 | if ('configuration' in ijRunConfig) { 94 | if ( 95 | '@_type' in ijRunConfig.configuration && 96 | ijRunConfig.configuration['@_type'] === 97 | 'BlazeCommandRunConfigurationType' 98 | ) { 99 | return new BazelTaskDefinition( 100 | ijRunConfig.configuration['@_name'], 101 | `bazel ${ijRunConfig.configuration['blaze-settings']['@_blaze-command']} ${ijRunConfig.configuration['blaze-settings']['blaze-target']}` 102 | ); 103 | } 104 | } 105 | } 106 | BazelLanguageServerTerminal.warn( 107 | `failed to convert intellj run config: ${configPath}}` 108 | ); 109 | return undefined; 110 | } 111 | -------------------------------------------------------------------------------- /src/resources/dark/error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/resources/dark/pass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/resources/dark/warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/resources/light/error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/resources/light/pass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/resources/light/warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../out/src" 5 | }, 6 | "include": ["**/*.ts"] 7 | } -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface UpdateClasspathResponse { 2 | data: Array; 3 | status: boolean; 4 | } 5 | 6 | export interface ClasspathInfo { 7 | path: string; 8 | displayPath: string; 9 | classpathEntry: string; 10 | projectName: string; 11 | projectType: string; 12 | } 13 | 14 | export interface ExcludeConfig { 15 | [x: string]: boolean; 16 | } 17 | 18 | export interface FileWatcherExcludeConfig { 19 | [x: string]: boolean; 20 | } 21 | 22 | export interface ParseConfig { 23 | root: string; 24 | imports: string[]; 25 | projectView: BazelProjectView; 26 | processedImports: string[]; 27 | } 28 | 29 | export interface BazelProjectView { 30 | directories: string[]; 31 | targets: string[]; 32 | deriveTargetsFromDirectories: boolean; 33 | workspaceType?: string; 34 | additionalLanguages?: string[]; 35 | javaLanguageLevel?: string; 36 | tsConfigRules?: string[]; 37 | importRunConfigurations?: string[]; 38 | bazelBinary?: string; 39 | projectMappings?: string[]; 40 | targetDiscoveryStrategy?: string; 41 | targetProvisioningStrategy?: string; 42 | } 43 | 44 | export interface RawSection { 45 | name: string; 46 | body: string; 47 | } 48 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdirSync, writeFileSync } from 'fs'; 2 | import { dirname, join } from 'path'; 3 | import { Uri, workspace } from 'vscode'; 4 | 5 | // TODO: pull this template out into a file 6 | const BAZELPROJECT_TEMPLATE = ` 7 | # The project view file (.bazelproject) is used to import targets into the IDE. 8 | # 9 | # See: https://ij.bazel.build/docs/project-views.html 10 | # 11 | # This files provides a default experience for developers working with the project. 12 | # You should customize it to suite your needs. 13 | 14 | directories: 15 | . # import everything (remove the dot if this is too much) 16 | 17 | derive_targets_from_directories: true 18 | `; 19 | 20 | export function getWorkspaceRoot(): string { 21 | if (workspace.workspaceFile) { 22 | return dirname(workspace.workspaceFile.path); 23 | } else { 24 | if (workspace.workspaceFolders && workspace.workspaceFolders.length > 0) { 25 | return workspace.workspaceFolders[0].uri.path; 26 | } 27 | } 28 | throw new Error('invalid workspace root'); 29 | } 30 | 31 | export function initBazelProjectFile() { 32 | const workspaceRoot = getWorkspaceRoot(); 33 | if (existsSync(join(workspaceRoot, '.eclipse', '.bazelproject'))) { 34 | return; 35 | } 36 | 37 | // only create a project view file if there's a bazel WORKSPACE file present in the workspace root 38 | if (isBazelWorkspaceRoot()) { 39 | mkdirSync(join(workspaceRoot, '.eclipse'), { recursive: true }); 40 | writeFileSync( 41 | join(workspaceRoot, '.eclipse', '.bazelproject'), 42 | BAZELPROJECT_TEMPLATE 43 | ); 44 | } 45 | } 46 | 47 | export function isBazelWorkspaceRoot(): boolean { 48 | const workspaceRoot = getWorkspaceRoot(); 49 | return ( 50 | existsSync(join(workspaceRoot, 'WORKSPACE')) || 51 | existsSync(join(workspaceRoot, 'WORKSPACE.bazel')) 52 | ); 53 | } 54 | 55 | export async function getVscodeConfig( 56 | name: 'settings' | 'launch' | 'tasks' | 'extensions' 57 | ): Promise { 58 | const root = getWorkspaceRoot(); 59 | const filePath = `${root}/.vscode/${name}.json`; 60 | return existsSync(filePath) 61 | ? await workspace.fs 62 | .readFile(Uri.file(`${`${root}/.vscode/${name}.json`}`)) 63 | .then((content) => JSON.parse(content.toString())) 64 | : undefined; 65 | } 66 | -------------------------------------------------------------------------------- /syntaxes/bazelproject-language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "#" 4 | } 5 | } -------------------------------------------------------------------------------- /syntaxes/bazelproject.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "name": "bazelproject", 4 | "patterns": [ 5 | { 6 | "include": "#keywords" 7 | }, 8 | { 9 | "include": "#comments" 10 | } 11 | ], 12 | "repository": { 13 | "keywords": { 14 | "patterns": [{ 15 | "name": "keyword.control.bazelproject", 16 | "match": "\\b(directories|targets|derive_targets_from_directories|import|workspace_type|additional_languages|java_language_level|test_sources|shard_sync|target_shard_size|exclude_library|build_flags|sync_flags|test_flags|import_run_configurations|bazel_binary|android_sdk_platform|android_min_sdk|generated_android_resource_directories|gazelle_target|ts_config_rules)\\b" 17 | }] 18 | }, 19 | "comments": { 20 | "name": "comment.line.number-sign.bazelproject", 21 | "begin": "#", 22 | "end": "\n" 23 | } 24 | }, 25 | "scopeName": "source.bazelproject" 26 | } -------------------------------------------------------------------------------- /syntaxes/starlark-language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "#" 4 | }, 5 | "brackets": [ 6 | ["{", "}"], 7 | ["[", "]"], 8 | ["(", ")"] 9 | ], 10 | "autoClosingPairs": [ 11 | ["{", "}"], 12 | ["[", "]"], 13 | ["(", ")"], 14 | { 15 | "open": "\"", 16 | "close": "\"", 17 | "notIn": ["string", "comment"] 18 | }, 19 | { 20 | "open": "'", 21 | "close": "'", 22 | "notIn": ["string", "comment"] 23 | } 24 | ], 25 | "surroundingPairs": [ 26 | ["{", "}"], 27 | ["[", "]"], 28 | ["(", ")"], 29 | ["\"", "\""], 30 | ["'", "'"] 31 | ] 32 | } -------------------------------------------------------------------------------- /syntaxes/starlark.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "name": "Starlark", 4 | "scopeName": "source.starlark", 5 | "fileTypes": ["BUILD", "WORKSPACE", "bazel", "bzl", "sky", "star"], 6 | "patterns": [ 7 | { 8 | "include": "#statement" 9 | }, 10 | { 11 | "include": "#expression" 12 | } 13 | ], 14 | "repository": { 15 | "statement": { 16 | "patterns": [ 17 | { 18 | "include": "#function-definition" 19 | }, 20 | { 21 | "include": "#statement-keyword" 22 | }, 23 | { 24 | "include": "#assignment-operator" 25 | }, 26 | { 27 | "include": "#docstring-statement" 28 | }, 29 | { 30 | "include": "#discouraged-semicolon" 31 | } 32 | ] 33 | }, 34 | "docstring-statement": { 35 | "begin": "^(?=\\s*r?('''|\"\"\"|'|\"))", 36 | "end": "(?<='''|\"\"\"|'|\")", 37 | "patterns": [ 38 | { 39 | "include": "#docstring" 40 | } 41 | ] 42 | }, 43 | "docstring": { 44 | "patterns": [ 45 | { 46 | "name": "comment.block.documentation.python.starlark", 47 | "begin": "('''|\"\"\")", 48 | "end": "(\\1)", 49 | "beginCaptures": { 50 | "1": { 51 | "name": "punctuation.definition.string.begin.python.starlark" 52 | } 53 | }, 54 | "endCaptures": { 55 | "1": { 56 | "name": "punctuation.definition.string.end.python.starlark" 57 | } 58 | }, 59 | "patterns": [ 60 | { 61 | "include": "#code-tag" 62 | }, 63 | { 64 | "include": "#docstring-content" 65 | } 66 | ] 67 | }, 68 | { 69 | "name": "comment.block.documentation.python.starlark", 70 | "begin": "(r)('''|\"\"\")", 71 | "end": "(\\2)", 72 | "beginCaptures": { 73 | "1": { 74 | "name": "storage.type.string.python.starlark" 75 | }, 76 | "2": { 77 | "name": "punctuation.definition.string.begin.python.starlark" 78 | } 79 | }, 80 | "endCaptures": { 81 | "1": { 82 | "name": "punctuation.definition.string.end.python.starlark" 83 | } 84 | }, 85 | "patterns": [ 86 | { 87 | "include": "#string-consume-escape" 88 | }, 89 | { 90 | "include": "#code-tag" 91 | } 92 | ] 93 | }, 94 | { 95 | "name": "comment.line.documentation.python.starlark", 96 | "begin": "('|\")", 97 | "end": "(\\1)|((?=|<=|<|>)(?# 4)", 392 | "captures": { 393 | "1": { 394 | "name": "keyword.operator.logical.python.starlark" 395 | }, 396 | "2": { 397 | "name": "keyword.control.flow.python.starlark" 398 | }, 399 | "3": { 400 | "name": "keyword.operator.arithmetic.python.starlark" 401 | }, 402 | "4": { 403 | "name": "keyword.operator.comparison.python.starlark" 404 | } 405 | } 406 | }, 407 | "literal": { 408 | "patterns": [ 409 | { 410 | "name": "constant.language.python.starlark", 411 | "match": "\\b(True|False|None)\\b" 412 | }, 413 | { 414 | "include": "#number" 415 | } 416 | ] 417 | }, 418 | "number": { 419 | "patterns": [ 420 | { 421 | "include": "#number-decimal" 422 | }, 423 | { 424 | "include": "#number-hexadecimal" 425 | }, 426 | { 427 | "include": "#number-octal" 428 | }, 429 | { 430 | "name": "invalid.illegal.name.python.starlark", 431 | "match": "\\b[0-9]+\\w+" 432 | } 433 | ] 434 | }, 435 | "number-decimal": { 436 | "name": "constant.numeric.decimal.python.starlark", 437 | "match": "(? modules = Lists.newArrayList("module1", "module2"); 24 | 25 | Iterable result = Iterables.transform(modules, greeter::greet); 26 | 27 | result.forEach(System.out::println); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/projects/small/module2/BUILD: -------------------------------------------------------------------------------- 1 | load("@rules_java//java:defs.bzl", "java_library") 2 | 3 | java_library ( 4 | name = "module2", 5 | srcs = glob(["java/src/**/*.java"]), 6 | visibility = ["//module1:__pkg__"], 7 | deps = ["//module3"] 8 | ) 9 | 10 | java_test( 11 | name = "module2-test", 12 | srcs = ["java/test/library/GreetingTest.java"], 13 | test_class = "library.GreetingTest", 14 | deps = [ 15 | "module2", 16 | "@junit//jar", 17 | ], 18 | ) 19 | -------------------------------------------------------------------------------- /test/projects/small/module2/java/src/library/Greeting.java: -------------------------------------------------------------------------------- 1 | package library; 2 | 3 | import log.Logger; 4 | 5 | public class Greeting { 6 | 7 | public String greet(String name) { 8 | Logger.logDebug("Greeting.greet"); 9 | return "Hello ".concat(name); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /test/projects/small/module2/java/test/library/GreetingTest.java: -------------------------------------------------------------------------------- 1 | package library; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | public class GreetingTest { 7 | @Test 8 | public void testGreet() { 9 | Assert.assertEquals("Hello JUnit", new Greeting().greet("JUnit")); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/projects/small/module3/BUILD: -------------------------------------------------------------------------------- 1 | load("@rules_java//java:defs.bzl", "java_library") 2 | 3 | java_library ( 4 | name = "module3", 5 | srcs = glob(["java/src/**/*.java"]), 6 | visibility = ["//module1:__pkg__", "//module2:__pkg__"] 7 | ) 8 | -------------------------------------------------------------------------------- /test/projects/small/module3/java/src/log/Logger.java: -------------------------------------------------------------------------------- 1 | package log; 2 | 3 | public final class Logger { 4 | 5 | private Logger() { 6 | throw new RuntimeException(); 7 | } 8 | 9 | public static void logDebug(String message) { 10 | String output = String.format("[DEBUG] %s", message); 11 | System.out.println(output); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /test/projects/small/third_party/maven/BUILD: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/bazel-vscode-java/a73ed6f4ef6e472fd5ec7746dd0b357643e3e371/test/projects/small/third_party/maven/BUILD -------------------------------------------------------------------------------- /test/projects/small/third_party/maven/dependencies.bzl: -------------------------------------------------------------------------------- 1 | load("@bazel_tools//tools/build_defs/repo:jvm.bzl", "jvm_maven_import_external") 2 | 3 | def maven_dependencies( 4 | maven_servers = ["https://repo1.maven.org/maven2/"]): 5 | 6 | jvm_maven_import_external( 7 | name = "com_google_guava", 8 | artifact = "com.google.guava:guava:28.2-jre", 9 | artifact_sha256 = "fc3aa363ad87223d1fbea584eee015a862150f6d34c71f24dc74088a635f08ef", 10 | licenses = ["notice"], 11 | server_urls = maven_servers, 12 | ) 13 | 14 | jvm_maven_import_external( 15 | name = "junit", 16 | artifact = "junit:junit:4.13", 17 | artifact_sha256 = "4b8532f63bdc0e0661507f947eb324a954d1dbac631ad19c8aa9a00feed1d863", 18 | licenses = ["notice"], 19 | server_urls = maven_servers, 20 | ) 21 | -------------------------------------------------------------------------------- /test/projects/small/tools/intellij/runConfigurations/Custom_Bazel_Target.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | //... 4 | -c=$PROJECT_DIR$/.. 5 | 6 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as cp from 'child_process'; 2 | import * as fs from 'fs'; 3 | import { glob } from 'glob'; 4 | import * as os from 'os'; 5 | import * as path from 'path'; 6 | import { env } from 'process'; 7 | 8 | import { 9 | downloadAndUnzipVSCode, 10 | resolveCliArgsFromVSCodeExecutablePath, 11 | runTests, 12 | } from '@vscode/test-electron'; 13 | 14 | async function main() { 15 | // folder containing the Extension Manifest package.json 16 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 17 | 18 | // path to the extension test runner script 19 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 20 | 21 | // path to the sampel project for testing 22 | const testProjectPath: string = path.resolve( 23 | extensionDevelopmentPath, 24 | './test/projects/small/' 25 | ); 26 | 27 | // temp area for test user data 28 | const testUserDataPath: string = path.resolve( 29 | os.tmpdir(), 30 | './suite-projects-small/' 31 | ); 32 | 33 | try { 34 | // prepare VS Code (use latest stable) 35 | const vscodeExecutablePath = await downloadAndUnzipVSCode('stable'); 36 | const [cliPath, ...args] = 37 | resolveCliArgsFromVSCodeExecutablePath(vscodeExecutablePath); 38 | 39 | // span vs code process for custom setup 40 | cp.spawnSync(cliPath, [...args, '--install-extension', 'redhat.java'], { 41 | encoding: 'utf-8', 42 | stdio: 'inherit', 43 | }); 44 | 45 | // ensrue the user data area is empty 46 | if (fs.existsSync(testUserDataPath)) { 47 | fs.rmdirSync(testUserDataPath, { recursive: true }); 48 | } 49 | 50 | // run the integration test with spawned VS Code 51 | console.log( 52 | `Running test suite with\n\tproject: ${testProjectPath}\n\tuser-data-dir: ${testUserDataPath}\n...` 53 | ); 54 | await runTests({ 55 | vscodeExecutablePath, 56 | extensionDevelopmentPath, 57 | extensionTestsPath, 58 | launchArgs: [ 59 | '--disable-workspace-trust', 60 | '--user-data-dir', 61 | testUserDataPath, 62 | testProjectPath, 63 | ], 64 | }); 65 | } catch (err) { 66 | console.log('\n\n\n'); 67 | console.log( 68 | '********************************************************************************************' 69 | ); 70 | console.error(`Failed to run tests: ${err}`); 71 | console.log( 72 | '********************************************************************************************' 73 | ); 74 | console.log('\n\n\n'); 75 | 76 | // try to locate Java LS log 77 | const lsLogs = await glob( 78 | ['**/*Java.log', '**/redhat.java/jdt_ws/.metadata/.log'], 79 | { cwd: testUserDataPath, withFileTypes: true, dot: true } 80 | ); 81 | if (lsLogs.length > 0) { 82 | if (env['PRINT_JDTLS_LOGS'] === 'true') { 83 | lsLogs.forEach((log) => { 84 | console.log(`> cat ${log.fullpath()}`); 85 | try { 86 | const data = fs.readFileSync(log.fullpath(), 'utf8'); 87 | console.log(data); 88 | } catch (err) { 89 | console.error(err); 90 | } 91 | console.log('\n\n\n'); 92 | }); 93 | } else { 94 | console.log( 95 | 'Set PRINT_JDTLS_LOGS=true to show the following JDTLS log automatically:' 96 | ); 97 | lsLogs.forEach((log) => { 98 | console.log(`\tcat ${log.fullpath()}`); 99 | }); 100 | } 101 | } else { 102 | console.warn('No logs from JDTLS found!'); 103 | } 104 | 105 | process.exit(1); 106 | } 107 | } 108 | 109 | main(); 110 | -------------------------------------------------------------------------------- /test/suite/Jdtls.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. 3 | // copied from https://github.com/jdneo/vscode-java-dependency/blob/86e7b60e8dd17a7bcaf0c39fe9dbcd5e8cf236d9/src/java/jdtls.ts 4 | 5 | import { Commands, executeJavaExtensionCommand, executeJavaLanguageServerCommand } from "../../src/commands"; 6 | import { UpdateClasspathResponse } from "../../src/types"; 7 | 8 | export namespace Jdtls { 9 | export function getSourcePaths(): Thenable { 10 | return executeJavaLanguageServerCommand(Commands.JAVA_LS_LIST_SOURCEPATHS); 11 | } 12 | 13 | export function buildWorkspace(): Thenable { 14 | return executeJavaExtensionCommand(Commands.JAVA_BUILD_WORKSPACE, false); 15 | } 16 | 17 | export enum CompileWorkspaceStatus { 18 | Failed = 0, 19 | Succeed = 1, 20 | Witherror = 2, 21 | Cancelled = 3, 22 | } 23 | } -------------------------------------------------------------------------------- /test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { setTimeout } from 'node:timers/promises'; 3 | import { env } from 'process'; 4 | import * as vscode from 'vscode'; 5 | import { extensions } from 'vscode'; 6 | import { Commands } from '../../src/commands'; 7 | import { BazelVscodeExtensionAPI } from '../../src/extension.api'; 8 | import { Jdtls } from './Jdtls'; 9 | 10 | suite('Java Language Extension - Standard', () => { 11 | suiteSetup(async function () { 12 | await extensions.getExtension('sfdc.bazel-vscode-java')?.activate(); 13 | }); 14 | 15 | test('version should be correct', async function () { 16 | const api: BazelVscodeExtensionAPI = extensions.getExtension( 17 | 'sfdc.bazel-vscode-java' 18 | )?.exports; 19 | 20 | assert.ok(api.parseProjectFile !== null); 21 | }); 22 | 23 | test('RedHat Java Extension should be present', () => { 24 | assert.ok(vscode.extensions.getExtension('redhat.java')); 25 | }); 26 | 27 | test('Bazel Java Extension should be present', () => { 28 | assert.ok(vscode.extensions.getExtension('sfdc.bazel-vscode-java')); 29 | }); 30 | 31 | test('RedHat Java Extension should activate', async function () { 32 | this.timeout(60000 * 2); 33 | const ext = vscode.extensions.getExtension('redhat.java'); 34 | while (true) { 35 | await setTimeout(5000); 36 | if (ext!.isActive) { 37 | break; 38 | } 39 | } 40 | }); 41 | 42 | test('Bazel Java Extension should activate', async function () { 43 | this.timeout(60000 * 2); 44 | const ext = vscode.extensions.getExtension('sfdc.bazel-vscode-java'); 45 | while (true) { 46 | await setTimeout(5000); 47 | if (ext!.isActive) { 48 | break; 49 | } 50 | } 51 | }); 52 | 53 | test('should register all java.bazel commands', async function () { 54 | this.timeout(60000 * 2); 55 | if (env['SKIP_COMMANDS_TEST'] === 'true') { 56 | console.log('Skipping "should register all java commands"'); 57 | return; 58 | } 59 | 60 | let api = vscode.extensions.getExtension('bazel-vscode-java')?.exports; 61 | if (!api) { 62 | api = await vscode.extensions 63 | .getExtension('bazel-vscode-java') 64 | ?.activate(); 65 | } 66 | 67 | // we have a few 'hidden' cmds that should not be checked for in this test. 68 | const COMMAND_EXCLUSIONS = [ 69 | Commands.SYNC_PROJECTS, 70 | Commands.UPDATE_CLASSPATHS, 71 | Commands.REGISTER_BAZEL_TCP_SERVER_PORT, 72 | ]; 73 | 74 | const commands = await vscode.commands.getCommands(true); 75 | const JAVA_COMMANDS = [ 76 | Commands.SYNC_PROJECTS_CMD, 77 | Commands.SYNC_DIRECTORIES_ONLY, 78 | Commands.UPDATE_CLASSPATHS_CMD, 79 | Commands.DEBUG_LS_CMD, 80 | Commands.OPEN_BAZEL_BUILD_STATUS_CMD, 81 | Commands.OPEN_BAZEL_PROJECT_FILE, 82 | Commands.CONVERT_PROJECT_WORKSPACE, 83 | ].sort(); 84 | 85 | const foundBazelJavaCommands = commands 86 | .filter( 87 | (value) => value.startsWith('java.bazel.') || value.startsWith('bazel.') 88 | ) 89 | .filter((value) => !COMMAND_EXCLUSIONS.includes(value)) 90 | .sort(); 91 | 92 | assert.deepStrictEqual( 93 | foundBazelJavaCommands, 94 | JAVA_COMMANDS, 95 | `Some Bazel Java commands are not registered properly or a new command 96 | is not added to the test.\nActual: ${foundBazelJavaCommands}\nExpected: ${JAVA_COMMANDS}` 97 | ); 98 | }); 99 | 100 | test('should have working JDTLS', async function () { 101 | let api = vscode.extensions.getExtension('redhat.java')?.exports; 102 | if (!api) { 103 | api = await vscode.extensions.getExtension('redhat.java')!.activate(); 104 | } 105 | assert.ok(!!api); 106 | 107 | // https://github.com/redhat-developer/vscode-java/blob/master/src/extension.api.ts#L67 108 | assert.ok(['Starting', 'Started'].includes(api.status)); 109 | }); 110 | 111 | // this is currently broken for the `small` test project. 112 | test('should build workspace without problems within reasonable time', function () { 113 | this.timeout(60000 * 5); 114 | return Jdtls.buildWorkspace().then((result) => { 115 | assert.strictEqual(result, Jdtls.CompileWorkspaceStatus.Succeed); 116 | 117 | return Jdtls.getSourcePaths().then((resp) => { 118 | const projects = new Set(resp.data.map((p) => p.projectName)); 119 | assert.ok(projects.size > 0); 120 | }); 121 | }); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import { glob } from 'glob'; 2 | import Mocha from 'mocha'; 3 | import * as path from 'path'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | reporter: 'json', 10 | reporterOptions: { output: './test/result/extension.test.json' }, 11 | }); 12 | 13 | const testsRoot = path.resolve(__dirname, '..'); 14 | 15 | return new Promise((c, e) => { 16 | glob('**/**.test.js', { cwd: testsRoot }).then( 17 | (files) => { 18 | // Add files to the test suite 19 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 20 | 21 | try { 22 | // Run the mocha test 23 | mocha.run((failures) => { 24 | if (failures > 0) { 25 | e(new Error(`${failures} tests failed.`)); 26 | } else { 27 | c(); 28 | } 29 | }); 30 | } catch (err) { 31 | console.error(err); 32 | e(err); 33 | } 34 | }, 35 | (err) => { 36 | return e(err); 37 | } 38 | ); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../out/test" 5 | }, 6 | "include": ["**/*.ts"], 7 | "references": [ 8 | { 9 | "path": "../src/tsconfig.json" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "sourceMap": true, 5 | 6 | /* Basic Options */ 7 | "target": "ES2022", 8 | "module": "commonjs", 9 | 10 | /* Strict Type-Checking Options */ 11 | "strict": true, 12 | 13 | /* Additional Checks */ 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | 17 | /* Module Resolution Options */ 18 | "moduleResolution": "node", 19 | "esModuleInterop": true, 20 | "resolveJsonModule": true, 21 | 22 | /* Experimental Options */ 23 | "experimentalDecorators": true, 24 | "emitDecoratorMetadata": true, 25 | 26 | /* Advanced Options */ 27 | "forceConsistentCasingInFileNames": true, 28 | "skipLibCheck": true 29 | }, 30 | "exclude": [ 31 | "node_modules", 32 | ".vscode-test" 33 | ] 34 | } 35 | --------------------------------------------------------------------------------