diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 28c9ed4a..36f9fc75 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -9,6 +9,9 @@ jobs: - macos-latest - ubuntu-latest - windows-latest + buf-bin-setup: + - buf-on-path + - buf-not-on-path runs-on: ${{ matrix.os }} steps: - name: checkout @@ -19,6 +22,7 @@ jobs: node-version-file: ".nvmrc" cache: npm - uses: bufbuild/buf-action@v1 + if: matrix.buf-bin-setup == 'buf-on-path' with: setup_only: true - name: install-deps @@ -32,24 +36,32 @@ jobs: - name: format run: npm run format - name: integration-tests-with-xvfb + env: + BUF_INSTALLED: ${{ matrix.buf-bin-setup }} run: xvfb-run -a npm run test:integration if: runner.os == 'Linux' - name: integration-tests shell: bash + env: + BUF_INSTALLED: ${{ matrix.buf-bin-setup }} run: npm run test:integration if: runner.os != 'Linux' - name: playwright-tests run: npm run test:playwright # Limiting playwright tests to macOS for now due to issues with xvfb on Linux and # timeouts on windows - if: runner.os == 'macOS' + # + # NOTE: We disable playwright tests when buf is not installed on the system $PATH + # for now so we don't have the extension attempting to resolve the installation. + # We'll need to find a way to intercept the call from playwright's VS Code extension. + if: runner.os == 'macOS' && matrix.buf-bin-setup == 'buf-on-path' - name: check diff run: node scripts/gh-diffcheck.mjs - name: upload-playwright-test-results uses: actions/upload-artifact@v4 if: always() with: - name: test-results + name: test-results-${{ matrix.buf-bin-setup }} path: test-results/ retention-days: 5 if-no-files-found: ignore diff --git a/README.md b/README.md index 7bf3854b..4502ecb2 100644 --- a/README.md +++ b/README.md @@ -1,122 +1,89 @@ # Buf for Visual Studio Code The [VS Code Buf extension][vs-code-marketplace] helps you work with [Protocol Buffers][protobuf] -files in a much more intuitive way, adding smart syntax highlighting, navigation, formatting, +files in a much more intuitive way, adding semantic syntax highlighting, navigation, formatting, documentation and diagnostic hovers, and integrations with [Buf][buf] commands. ## Features -- **Code navigation** - Go-to definition and documentation insets for `.proto` symbols. -- **Syntax highlighting** - Protobuf specific color and styling of code. -- **Code editing** - Formatting via `buf format` and annotations and hovers based on `buf lint`. -- **Documentation hovers** - Documentation for definitions when hovering a reference. -- **Buf command support** - Execution of `buf` CLI commands via the [Command Palette][command-palette]. +- Code navigation: Go-to definition and go-to references for `.proto` symbols. +- Autocompletion: Completion results for `.proto` symbols using [IntelliSense][intellisense]. +- Syntax highlighting: Protobuf specific color and styling for code. +- Documentation hovers: Documentation for definitions when hovering a reference. +- Formatting: Formats `.proto` files on-save. +- Diagnostics: Annotations and highlights for build and lint errors. ![Preview features](./preview.gif) -## Getting Started - -[Install the latest version via the VS Code marketplace][vs-code-marketplace]. - -By default, the extension will use your locally-installed version of `buf` on your system -`$PATH`. However, you don't have to install `buf` - the extension can manage and install it -for you based on the [buf.commandLine.path](#buf.commandline.path) and [buf.commandLine.version](#buf.commandline.version) -configurations: - -| |
buf.commandLine.path
|
buf.commandLine.version
| -| --- | --- | --- | -| Default: Use `buf` from the system `$PATH`. | {empty} | {empty} | -| Use the latest released version of `buf` and check for updates on extension activation. | {empty} | `latest` | -| Use `buf` at specified path. | User specified path | {empty} | -| Install and use the specified version of `buf`. | {empty} | User specified semver version | -| Use `buf` at specified path and display an error message. | User specified path | User specified semver version | - -## Extension Settings - -This extension contributes the following configuration settings. - -### buf.commandLine.path +In addition to integrated editing features, the extension provides commands through the +`buf` CLI. These commands are accessible by opening the [Command Palette][command-palette], +`Ctrl/Cmd+Shift+P`. See the [full list of commands](#commands) provided by this extension. -Default: `null` -The path to a specific install of Buf to use. Relative paths are supported and are relative to the VS Code workspace root. +## Requirements -### buf.commandLine.version +- Visual Studio Code 1.95 or newer (or editors compatible with VS Code 1.90+ APIs) -Default: `null` -Specific version (e.g. 'v1.53.0') of Buf release to download and install. - -### buf.restartAfterCrash - -Default: `true` -Automatically restart the Buf Language Server (up to 4 times) if it crashes. - -### buf.enableHover - -Default: `true` -Enable hover features provided by the language server. - -### buf.enable +## Getting Started -Default: `true` -Enable Buf Language Server features. +[Install the latest version via the VS Code marketplace][vs-code-marketplace]. -### buf.debug +You do not need to install the Buf CLI to use this extension. By default, the extension uses +the Buf CLI from your system `$PATH`. If `buf` isn't found on your `$PATH`, the extension +automatically downloads and installs the latest version to its own storage directory. -Default: `false` -Enable debug logs in output channels. +## Community and Support -### buf.log-format +Feedback is welcome and appreciated! For feature requests, bugs, or questions, please +[file an issue][issue]. -Default: `text` -Buf Language Server log format. +If you're looking for help and/or discussion around Protobuf, best practices, etc., join us +on [Slack][slack]. ## Commands -This extension contributes the following commands to the [Command Palette][command-palette]. - -### Setup - -- Install CLI: installs the `buf` CLI based on `buf.commandLine.path` and `buf.commandLine.version` - configurations and then attempts to start the language server. -- Update CLI: updates the `buf` CLI based on `buf.commandLine.path` and `buf.commandLine.version` - configurations and then attempts to start the language server. - -### Language Server +A full list of [Command Palette][command-palette] commands provided by this extension: - Start Buf Language Server: starts the Buf Language Server. If the Buf Language Server is already running, it will stop and then start it. + - Stop Buf Language Server: stops the Buf Language Server. If the Buf Language Server is not currently running, then it is a no-op. -### Buf +- Build: runs `buf build` with optional user input for the build output file. If the build + output is specified by the user, it will be created at the root of each VS Code workspace. -- Build: runs `buf build` with an optional user input for the build output. - Init: runs `buf config init` at the root of each VS Code workspace. This creates a `buf.yaml` file to help users get started with Buf modules and workspaces. -- List available breaking change detection rules: lists the breaking change detection rules - that are available. -- List available lint rules: lists the lint rules that are available. -- Prune module dependencies: prunes unused dependencies from the `buf.lock` at the root of - each VS Code workspace. -- Update module dependencies: updates the dependencies in `buf.lock` at the root of each - VS Code workspace. -- Generate: runs `buf generate` at the root of each VS Code workspace. -- List module files: lists the Protobuf definition files for the Buf module/workspace at the - root of each VS Code workspace. -- Price of BSR paid plans: provides the pricing information for Buf Schema Registry (BSR) - for each VS Code workspace. -- Module stats: provides Buf module/workspace stats at the root of each VS Code workspace. - -### Extension - -- Show Buf Output: shows the extension output channel + +- List available breaking change detection rules: runs `buf config ls-breaking-rules` at the + root of each VS Code workspace and provides a list of available [breaking change detection rules][breaking-rules] + in a VS Code editor window. + +- List available lint rules: runs `buf config ls-lint-rules` at the root of each VS Code workspace + and provides a list of available [lint rules][lint-rules] in a VS Code editor window. + +- Prune module dependencies: runs `buf dep prune` at the root of each VS Code workspace and + prunes unused dependencies from the `buf.lock` file(s). + +- Update module dependencies: runs `buf dep update` at the root of each VS Code workspace and + updates the dependencies in the `buf.lock` file(s). + +- Generate: runs `buf generate` at the root of each VS Code workspace and generates code based + on the `buf.gen.yaml` file(s). + +- Show Buf Output: shows the extension output channel. ## Legal Offered under the [Apache 2 license][license]. -[command-palette]: https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette -[vs-code-marketplace]: https://marketplace.visualstudio.com/items?itemName=bufbuild.vscode-buf -[protobuf]: https://protobuf.dev/ [buf]: https://buf.build/ +[breaking-rules]: https://buf.build/docs/breaking/rules +[command-palette]: https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette +[issue]: https://github.com/bufbuild/vscode-buf/issues/new/choose +[intellisense]: https://code.visualstudio.com/docs/editing/intellisense [license]: https://github.com/bufbuild/vscode-buf/blob/main/LICENSE +[lint-rules]: https://buf.build/docs/lint/rules +[protobuf]: https://protobuf.dev/ +[slack]: https://buf.build/links/slack +[vs-code-marketplace]: https://marketplace.visualstudio.com/items?itemName=bufbuild.vscode-buf diff --git a/package.json b/package.json index 3e5f87a8..e579187d 100644 --- a/package.json +++ b/package.json @@ -75,12 +75,6 @@ "icon": "$(list-unordered)", "title": "List available lint rules." }, - { - "command": "buf.configlsmodules", - "category": "Buf", - "icon": "$(folder-library)", - "title": "List modules in workspace." - }, { "command": "buf.depprune", "category": "Buf", @@ -102,34 +96,6 @@ "title": "Generate", "description": "Run `buf generate` across VS Code workspace(s)." }, - { - "command": "buf.lsfiles", - "category": "Buf", - "icon": "$(files)", - "title": "List module files.", - "description": "List module files across VS Code workspace(s)." - }, - { - "command": "buf.price", - "category": "Buf", - "icon": "$(briefcase)", - "title": "Price for BSR paid plans.", - "description": "Check the price of BSR paid plans across VS Code workspace(s)." - }, - { - "command": "buf.stats", - "category": "Buf", - "icon": "$(graph-line)", - "title": "Module stats", - "description": "Get stats for Buf modules across VS Code workspace(s)." - }, - { - "command": "buf.install", - "category": "Buf", - "icon": "$(cloud-download)", - "title": "Install CLI", - "description": "Install the Buf CLI from GitHub releases." - }, { "command": "buf.showOutput", "category": "Buf", @@ -147,74 +113,15 @@ "category": "Buf", "icon": "$(debug-stop)", "title": "Stop Buf Language Server" - }, - { - "command": "buf.update", - "category": "Buf", - "icon": "$(sync)", - "title": "Update CLI", - "description": "Check for updates and install the latest version of the Buf CLI." } ], "configuration": { "title": "Buf", "properties": { - "buf.commandLine.path": { - "type": "string", - "description": "The path to a specific install of Buf to use. Relative paths are supported and are relative to the workspace root." - }, - "buf.commandLine.version": { - "type": "string", - "description": "Specific version (git tag e.g. 'v1.53.0') of Buf release to download and install." - }, - "buf.restartAfterCrash": { - "type": "boolean", - "default": true, - "description": "Automatically restart Buf (up to 4 times) if it crashes." - }, - "buf.enableHover": { - "type": "boolean", - "default": true, - "description": "Enable hover features provided by the language server." - }, - "buf.enable": { - "type": "boolean", - "default": true, - "description": "Enable Buf language server features." - }, - "buf.debug": { + "buf.debugLogs": { "type": "boolean", "default": false, - "description": "Enable debug mode." - }, - "buf.log-format": { - "type": [ - "string", - "null" - ], - "enum": [ - "text", - "color", - "json" - ], - "default": "text", - "description": "Buf language server log format." - }, - "buf.checks.breaking.againstStrategy": { - "type": "string", - "enum": [ - "disk", - "git" - ], - "default": "git", - "description": "The strategy to use when checking breaking changes against a specific reference.", - "deprecationMessage": "Deprecated: breaking change detection is no longer supported in the LSP." - }, - "buf.checks.breaking.againstGitRef": { - "type": "string", - "default": "refs/remotes/origin/HEAD", - "description": "The Git reference to check breaking changes against.", - "deprecationMessage": "Deprecated: breaking change detection is no longer supported in the LSP." + "description": "Enable debug logs for the Buf language server." } } }, diff --git a/playwright.config.ts b/playwright.config.ts index c286e57c..c0e505c5 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -13,8 +13,9 @@ export default defineConfig({ reporter: process.env.CI ? "html" : "list", timeout: 120_000_000, expect: { - timeout: 40_000, + timeout: 60_000, }, + globalSetup: "./test/playwright/global-setup", projects: [ { name: "VS Code insiders", diff --git a/src/commands/buf-config-ls-modules.ts b/src/commands/buf-config-ls-modules.ts deleted file mode 100644 index 34e6cc51..00000000 --- a/src/commands/buf-config-ls-modules.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as vscode from "vscode"; -import { log } from "../log"; -import { bufState } from "../state"; -import { Command } from "./command"; - -/** - * Minimum Buf version required for `buf config ls-modules` command. - */ -const minBufVersion = "v1.34.0"; - -/** - * bufConfigLsModules shows the output channel and runs `buf config ls-modules` at the root - * of each VS Code workspace folder. If there are no workspace folders, then bufConfigLsModules - * displays a warning and is a no-op. - */ -export const bufConfigLsModules = new Command( - "buf.configlsmodules", - "COMMAND_TYPE_BUF", - async () => { - log.show(); - if (!vscode.workspace.workspaceFolders) { - log.warn(`No workspace found, unable to run "buf config ls-modules"`); - return; - } - const bufVersion = bufState.getBufBinaryVersion(); - if (bufVersion?.compare(minBufVersion) === -1) { - log.warn( - `Current Buf Version ${bufVersion} does not meet minimum required version ${minBufVersion}, unable to run "buf config ls-modules".` - ); - return; - } - for (const workspaceFolder of vscode.workspace.workspaceFolders) { - bufState.execBufCommand( - ["config", "ls-modules"], - workspaceFolder.uri.path - ); - } - } -); diff --git a/src/commands/buf-ls-files.ts b/src/commands/buf-ls-files.ts deleted file mode 100644 index 8283f4eb..00000000 --- a/src/commands/buf-ls-files.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as vscode from "vscode"; -import { log } from "../log"; -import { bufState } from "../state"; -import { Command } from "./command"; - -/** - * bufLsFiles shows the output channel and runs `buf ls-files` at the root of each VS Code - * workspace folder. If there are no workspace folders, then bufLsFiles displays a warning - * and is a no-op. - */ -export const bufLsFiles = new Command( - "buf.lsfiles", - "COMMAND_TYPE_BUF", - async () => { - log.show(); - if (!vscode.workspace.workspaceFolders) { - log.warn(`No workspace found, unable to run "buf ls-files"`); - return; - } - for (const workspaceFolder of vscode.workspace.workspaceFolders) { - bufState.execBufCommand(["ls-files"], workspaceFolder.uri.path); - } - } -); diff --git a/src/commands/buf-price.ts b/src/commands/buf-price.ts deleted file mode 100644 index 73128fa2..00000000 --- a/src/commands/buf-price.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as vscode from "vscode"; -import { log } from "../log"; -import { bufState } from "../state"; -import { Command } from "./command"; - -/** - * Buf version with the most recent price update. - */ -const lastPriceUpdate = "v1.28.0"; - -/** - * Minimum Buf version required for `buf beta price` command. - */ -const minBetaVersion = "v1.16.0"; - -/** - * bufPrice shows the output channel and runs `buf beta price` at the root of each VS Code - * workspace folder. If there are no workspace folders, then bufPrice displays a warning - * and is a no-op. - */ -export const bufPrice = new Command( - "buf.price", - "COMMAND_TYPE_BUF", - async () => { - log.show(); - if (!vscode.workspace.workspaceFolders) { - log.warn(`No workspace found, unable to run "buf beta price"`); - return; - } - const bufVersion = bufState.getBufBinaryVersion(); - if (bufVersion?.compare(minBetaVersion) === -1) { - log.warn( - `Current Buf Version ${bufVersion} does not meet minimum required version of price command ${minBetaVersion}, unable to run "buf beta price".` - ); - return; - } - if (bufVersion?.compare(lastPriceUpdate) === -1) { - log.warn( - `Current Buf Version ${bufVersion} has outdated price data, latest price update available for version ${lastPriceUpdate} and onwards.` - ); - } - for (const workspaceFolder of vscode.workspace.workspaceFolders) { - bufState.execBufCommand(["beta", "price"], workspaceFolder.uri.path); - } - } -); diff --git a/src/commands/buf-stats.ts b/src/commands/buf-stats.ts deleted file mode 100644 index e48a0cf0..00000000 --- a/src/commands/buf-stats.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as vscode from "vscode"; -import { log } from "../log"; -import { bufState } from "../state"; -import { Command } from "./command"; - -/** - * Minimum Buf version required for `buf stats` command. - */ -const minBufVersion = "v1.55.0"; - -/** - * Minimum Buf version required for `buf beta stats` command. - */ -const minBetaVersion = "v1.17.0"; - -/** - * bufStats shows the output channel and runs `buf stats` at the root of each VS Code - * workspace folder. If there are no workspace folders, then bufStats displays a warning - * and is a no-op. - */ -export const bufStats = new Command( - "buf.stats", - "COMMAND_TYPE_BUF", - async () => { - log.show(); - if (!vscode.workspace.workspaceFolders) { - log.warn(`No workspace found, unable to run "buf stats"`); - return; - } - const bufVersion = bufState.getBufBinaryVersion(); - let args = ["stats"]; - if (bufVersion?.compare(minBufVersion) === -1) { - args = ["beta", "stats"]; - if (bufVersion?.compare(minBetaVersion) === -1) { - log.warn( - `Current Buf Version ${bufVersion} does not meet minimum required version of stats command ${minBetaVersion}, unable to run "buf stats".` - ); - return; - } - } - for (const workspaceFolder of vscode.workspace.workspaceFolders) { - bufState.execBufCommand(args, workspaceFolder.uri.path); - } - } -); diff --git a/src/commands/install-buf.ts b/src/commands/install-buf.ts deleted file mode 100644 index 9f65c374..00000000 --- a/src/commands/install-buf.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { bufState } from "../state"; -import { Command } from "./command"; - -/** - * installBuf installs the Buf CLI binary and attempts to start the language server after. - */ -export const installBuf = new Command( - "buf.install", - "COMMAND_TYPE_SETUP", - async (ctx) => { - await bufState.installBufBinary(ctx.globalStorageUri.fsPath); - await bufState.startLanguageServer(ctx); - } -); diff --git a/src/commands/register-all-commands.ts b/src/commands/register-all-commands.ts index 8e54acc6..70fa5383 100644 --- a/src/commands/register-all-commands.ts +++ b/src/commands/register-all-commands.ts @@ -4,19 +4,13 @@ import { bufBuild } from "./buf-build"; import { bufConfigInit } from "./buf-config-init"; import { bufConfigLsBreakingRules } from "./buf-config-ls-breaking-rules"; import { bufConfigLsLintRules } from "./buf-config-ls-lint-rules"; -import { bufConfigLsModules } from "./buf-config-ls-modules"; import { bufDepPrune } from "./buf-dep-prune"; import { bufDepUpdate } from "./buf-dep-update"; import { bufGenerate } from "./buf-generate"; -import { bufLsFiles } from "./buf-ls-files"; -import { bufPrice } from "./buf-price"; -import { bufStats } from "./buf-stats"; -import { installBuf } from "./install-buf"; import { showCommands } from "./show-commands"; import { showOutput } from "./show-output"; import { startLanguageServer } from "./start-lsp"; import { stopLanguageServer } from "./stop-lsp"; -import { updateBuf } from "./update-buf"; /** * @file Provides a convenience function for registering all commands in the extension. @@ -28,19 +22,13 @@ const commands = [ bufConfigInit, bufConfigLsBreakingRules, bufConfigLsLintRules, - bufConfigLsModules, bufDepPrune, bufDepUpdate, bufGenerate, - bufLsFiles, - bufPrice, - bufStats, - installBuf, showCommands, showOutput, startLanguageServer, stopLanguageServer, - updateBuf, ]; /** diff --git a/src/commands/update-buf.ts b/src/commands/update-buf.ts deleted file mode 100644 index fdb7a373..00000000 --- a/src/commands/update-buf.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { bufState } from "../state"; -import { Command } from "./command"; - -/** - * updateBuf updates the Buf CLI binary and attempts to start the language server after. - */ -export const updateBuf = new Command( - "buf.update", - "COMMAND_TYPE_SETUP", - async (ctx) => { - await bufState.updateBufBinary(ctx.globalStorageUri.fsPath); - await bufState.startLanguageServer(ctx); - } -); diff --git a/src/extension.ts b/src/extension.ts index e735dd8a..2d4b2743 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,9 +1,9 @@ import * as vscode from "vscode"; -import { installBuf } from "./commands/install-buf"; import { registerAllCommands } from "./commands/register-all-commands"; import { startLanguageServer } from "./commands/start-lsp"; import { stopLanguageServer } from "./commands/stop-lsp"; import { log } from "./log"; +import { bufState } from "./state"; import { activateStatusBar, deactivateStatusBar } from "./status-bar"; /** @@ -15,7 +15,7 @@ export async function activate(ctx: vscode.ExtensionContext) { ctx.subscriptions.push( vscode.workspace.onDidChangeConfiguration(handleOnDidConfigChange) ); - await installBuf.execute(); + await bufState.init(ctx); } /** @@ -35,13 +35,9 @@ const handleOnDidConfigChange = async (e: vscode.ConfigurationChangeEvent) => { if (!e.affectsConfiguration("buf")) { return; } - if ( - e.affectsConfiguration("buf.commandLine.path") || - e.affectsConfiguration("buf.commandLine.version") - ) { - await installBuf.execute(); - } - if (e.affectsConfiguration("buf.enable")) { + if (e.affectsConfiguration("buf.debugLogs")) { + // When debug logs configuration changes, restart the language server. + await stopLanguageServer.execute(); await startLanguageServer.execute(); } }; diff --git a/src/github.ts b/src/github.ts index 7782327d..3f4f9aae 100644 --- a/src/github.ts +++ b/src/github.ts @@ -13,11 +13,8 @@ import { progress } from "./progress"; /** * The GitHub release URL for the Buf CLI. - * - * Exported for tests. */ -export const githubReleaseURL = - "https://api.github.com/repos/bufbuild/buf/releases/"; +const githubReleaseURL = "https://api.github.com/repos/bufbuild/buf/releases/"; /** * Release is a GitHub release for the Buf CLI. diff --git a/src/state.ts b/src/state.ts index d328faa7..bf5ad7f4 100644 --- a/src/state.ts +++ b/src/state.ts @@ -48,6 +48,8 @@ const minBufVersion = "v1.59.0"; * - The `buf` CLI binary used by the extension * - The LSP server (e.g. starting and stopping) * + * @method init initializes the state of the Buf extension by setting up the buf CLI binary. + * This is expected to be called when the extension is activated. * @method handleExtensionStatus sets the extension status on the state to the provided * status, and once the work is complete, sets the extension status back to idle. * @method getExtensionStatus gets the current extension status. @@ -55,10 +57,6 @@ const minBufVersion = "v1.59.0"; * @method getBufBinaryVersion gets the current Buf binary version. * @method getBufBinaryPath gets the current Buf binary path. * @method execBufCommand execs the Buf binary with the specified arguments and working directory. - * @method installBufBinary installs the Buf CLI for the extension based on the extension - * configuration. - * @method updateBufBinary updates the Buf CLI for the extension based on the extension - * configuration. * @method startLanguageServer starts the LSP server and client. * @method stopLanguageServer stops the language server and client. * @@ -77,10 +75,6 @@ class BufState { public constructor() { effect(() => { switch (this._languageServerStatus.value) { - case "LANGUAGE_SERVER_NOT_INSTALLED": - this.bufBinary = undefined; - this.lspClient = undefined; - break; case "LANGUAGE_SERVER_DISABLED": this.lspClient = undefined; break; @@ -183,156 +177,50 @@ class BufState { } /** - * installBufBinary installs the Buf CLI for the extension based on the extension configuration. - * - * There are two configuration fields for managing the Buf CLI binary: - * buf.commandLine.path: a path to a local binary, e.g. /usr/local/bin/buf - * buf.commandLine.version: the expected version of the Buf binary, e.g. v1.53.0. This can - * also be set to "latest", in which case, the extension will check for and expect the latest - * released version of the Buf CLI. + * init initializes the state of the Buf extension by setting up the buf CLI binary and + * starting up the LSP. * - * When checking for the Buf CLI binary, the resolution logic uses the following order of - * precendence: - * 1. The path set on buf.commandLine.path. - * 2. The version set to buf.commandLine.version. If "latest" is set, check for and use the - * latest released version of the Buf CLI. - * 3. If neither buf.commandLine.path or buf.commandLine.version is set, look for the Buf - * CLI on the OS path. + * We check the user's system $PATH for buf. If the user has buf installed locally, then + * we use their locally installed version of buf. * - * If the Buf CLI for the configured path is already installed, then installBufBinary logs - * this and is a no-op. + * If the user does not have the buf binary installed locally, then the extension will + * attempt to install the latest version of buf to the VS Code global storage and use that. */ - public async installBufBinary(storagePath: string) { - let configPath = config.get("commandLine.path"); - const configVersion = config.get("commandLine.version"); - - if (configPath) { - if (!path.isAbsolute(configPath)) { - try { - configPath = getBinaryPathForRelConfigPath(configPath); - } catch (e) { - log.error(`Error loading buf from relative config path: ${e}`); - this._languageServerStatus.value = "LANGUAGE_SERVER_NOT_INSTALLED"; - return; - } - } - if (configVersion) { - log.warn( - "Both 'buf.commandLine.path' and 'buf.commandLine.version' are set. Using 'buf.commandLine.path'." - ); - } - if (this.bufBinary && this.bufBinary.path === configPath) { - log.info( - `Buf CLI for configured path '${configPath}' already installed` - ); - return; - } - try { - log.info(`Installing Buf CLI set to path '${configPath}...`); - this.bufBinary = await getBufBinaryFromPath(configPath); - log.info( - `Using '${this.bufBinary.path}', version: ${this.bufBinary.version}.` - ); - } catch (e) { - log.error(`Error loading buf from path '${configPath}': ${e}`); - this._languageServerStatus.value = "LANGUAGE_SERVER_NOT_INSTALLED"; - } - return; - } - if (configVersion) { - await this.updateBufBinary(storagePath); - return; - } + public async init(ctx: vscode.ExtensionContext) { log.info("Looking for Buf on the system $PATH..."); try { this.bufBinary = await findBufInSystemPath(); } catch (e) { - log.error(`Buf is not installed on the OS path: ${e}`); - this._languageServerStatus.value = "LANGUAGE_SERVER_NOT_INSTALLED"; - } - } - - /** - * updateBufBinary updates the Buf CLI for the extension based on the extension configuration. - * - * The version is specified by the buf.commandLine.version configuration. - * - * updateBufBinary will download and install the configured version of the Buf CLI if there - * is currently no version of the Buf CLI used by the extension or if the current version - * used does not match the configured version. - * - * If an explicit path to the Buf CLI binary is specified via buf.commandLine.path, then - * updateBufBinary displays a warning and is a no-op. - * - * If no version is set, then updateBuf displays a warning and is a no-op. - * - * If the version set is not valid semver, then updateBufBinary displays a warning and is - * a no-op. - * - * If the version set cannot be resolved, then updateBufBinary will provide the user with - * a pop-up with the error message and a link to the installation docs. - */ - public async updateBufBinary(storagePath: string) { - if (config.get("commandLine.path")) { - vscode.window.showErrorMessage( - "'buf.commandLine.path' is explicitly set, no updates will be made." + log.info( + `Buf not found on the OS path: ${e}, installing from releases...` ); - return; - } - const configVersion = config.get("commandLine.version"); - if (!configVersion) { - vscode.window.showErrorMessage( - "'buf.commandLine.version' is not set, no updates will be made." - ); - return; - } - if (configVersion !== "latest") { - if (!semver.valid(configVersion)) { - log.error( - `buf.commandLine.version '${configVersion}' is not a valid semver version, no updates installed for Buf CLI...` - ); - return; - } - } - if ( - this.bufBinary && - configVersion !== "latest" && - this.bufBinary.version.compare(configVersion) === 0 - ) { - log.info(`Already installed Buf CLI version '${configVersion}',`); - return; - } - const abort = new AbortController(); - try { - log.info(`Checking github releases for '${configVersion}' release...`); - const release = await github.getRelease(configVersion); - const asset = await github.findAsset(release); - this.bufBinary = await installReleaseAsset( - storagePath, - release, - asset, - abort - ); - vscode.window.showInformationMessage( - `Buf ${release.name} is now installed.` - ); - } catch (e) { - if (!abort.signal.aborted) { - log.info(`Failed to install buf: ${e}`); - this._languageServerStatus.value = "LANGUAGE_SERVER_NOT_INSTALLED"; - showPopup( - `Failed to install Buf CLI. You may want to install it manually.`, - "https://buf.build/docs/cli/installation/" + const abort = new AbortController(); + try { + const release = await github.getRelease("latest"); + const asset = await github.findAsset(release); + this.bufBinary = await installReleaseAsset( + ctx.globalStorageUri.fsPath, + release, + asset, + abort ); + } catch (e) { + if (!abort.signal.aborted) { + log.info(`Failed to install buf: ${e}`); + this._languageServerStatus.value = "LANGUAGE_SERVER_DISABLED"; + showPopup( + `Failed to install Buf CLI. You may want to install it manually.`, + "https://buf.build/docs/cli/installation/" + ); + } } } + this.startLanguageServer(ctx); } /** * startLanguageServer starts the LSP server and client. * - * If the LSP is disabled through configuration, then startLanguageServer will display - * a warning, set the appropriate status, and be a no-op. * If the LSP server is already running (or already starting), then startLanguageServer * will log a warning and be a no-op. * If the LSP server is stopped or in an errored state, startLanguageServer will attempt @@ -349,14 +237,11 @@ class BufState { serverOutputChannel = createConsoleOutputChannel("Buf (server)"); ctx.subscriptions.push(serverOutputChannel); } - if (!config.get("enable")) { - await this.stopLanguageServer(); - this._languageServerStatus.value = "LANGUAGE_SERVER_DISABLED"; - log.warn("Buf is disabled. Enable it by setting 'buf.enable' to true."); - return; - } if (this.lspClient) { - if (this._languageServerStatus.value === "LANGUAGE_SERVER_STARTING") { + if ( + this._languageServerStatus.value === "LANGUAGE_SERVER_STARTING" || + this._languageServerStatus.value === "LANGUAGE_SERVER_RUNNING" + ) { log.warn("Buf Language Server already starting, no new actions taken."); return; } @@ -368,18 +253,12 @@ class BufState { this._languageServerStatus.value = "LANGUAGE_SERVER_STARTING"; return; } - if (this._languageServerStatus.value === "LANGUAGE_SERVER_RUNNING") { - log.warn("Buf Language Server already running, restarting."); - await this.stopLanguageServer(); - this._languageServerStatus.value = "LANGUAGE_SERVER_STARTING"; - return; - } } if (!this.bufBinary) { log.error( "No installed version of Buf found, cannot start Buf Language Server." ); - this._languageServerStatus.value = "LANGUAGE_SERVER_NOT_INSTALLED"; + this._languageServerStatus.value = "LANGUAGE_SERVER_STOPPED"; return; } const args = getBufArgs(); @@ -398,13 +277,10 @@ class BufState { documentSelector: protoDocumentSelector, diagnosticCollectionName: "bufc", outputChannel: serverOutputChannel, - // TODO: we can consider making this configurable through our settings. revealOutputChannelOn: lsp.RevealOutputChannelOn.Never, middleware: { + // Always configure a hover provider on the client. provideHover: async (document, position, token, next) => { - if (!config.get("enableHover")) { - return null; - } return next(document, position, token); }, }, @@ -414,9 +290,8 @@ class BufState { serverOptions, clientOptions ); - const errorHandler = this.lspClient.createDefaultErrorHandler( - config.get("restartAfterCrash") ? 4 : 0 - ); + // Always restart buf LSP if it crashes, up to 4 times. + const errorHandler = this.lspClient.createDefaultErrorHandler(4); this.lspClient.clientOptions.errorHandler = { error: (error, message, count) => { return errorHandler.error(error, message, count); @@ -456,23 +331,6 @@ class BufState { */ export const bufState = new BufState(); -/** - * A helper function for getting the binary path based on a relative path config. We check - * each workspace folder and return the first relative binary path that exists, otherwise - * return undefined. - */ -function getBinaryPathForRelConfigPath(configPath: string): string { - if (vscode.workspace.workspaceFolders) { - for (const workspaceFolder of vscode.workspace.workspaceFolders) { - const joinedPath = path.join(workspaceFolder.uri.path, configPath); - if (fs.existsSync(joinedPath)) { - return joinedPath; - } - } - } - throw new Error(`Unable to use relative Buf binary path ${configPath}`); -} - /** * BufBinary contains the Buf CLI binary information used by the extension. * @@ -532,16 +390,18 @@ async function installReleaseAsset( await fs.promises.access(downloadBin); // We await for the bufBinary to be set before returning so we can catch any errors. const bufBinary = await getBufBinaryFromPath(downloadBin); + log.info(`Using buf version v${bufBinary.version} from extension cache.`); return bufBinary; } catch (e) { // In the case of an error, we log, and then move on to attempt a download. - log.error(`Error accessing buf binary, downloading... ${e}`); + log.info(`No buf binary available locally, downloading... ${e}`); } log.info(`Downloading ${asset.name} to ${downloadBin}...`); await github.download(asset, downloadBin, abort); await fs.promises.chmod(downloadBin, 0o755); // We await for the bufBinary to be set before returning and mutating the extension state. const bufBinary = await getBufBinaryFromPath(downloadBin); + vscode.window.showInformationMessage(`Buf ${release.name} is now installed.`); return bufBinary; } @@ -560,14 +420,10 @@ async function showPopup(message: string, url: string) { * Returns an error if bufVersion is too low to run the LSP server. */ function getBufArgs() { - const bufArgs = []; - if (config.get("debug")) { + const bufArgs = ["--log-format", "text"]; + if (config.get("debugLogs")) { bufArgs.push("--debug"); } - const logFormat = config.get("log-format"); - if (logFormat) { - bufArgs.push("--log-format", logFormat); - } const bufVersion = bufState.getBufBinaryVersion(); let args = ["lsp", "serve"]; if (bufVersion?.compare(minBufVersion) === -1) { diff --git a/src/status-bar.ts b/src/status-bar.ts index a685cfe5..31a11eaa 100644 --- a/src/status-bar.ts +++ b/src/status-bar.ts @@ -1,6 +1,5 @@ import { effect } from "@preact/signals-core"; import * as vscode from "vscode"; -import { installBuf } from "./commands/install-buf"; import { showCommands } from "./commands/show-commands"; import { showOutput } from "./commands/show-output"; import { startLanguageServer } from "./commands/start-lsp"; @@ -54,12 +53,6 @@ const languageServerStatusConfig: Record< command: startLanguageServer.name, tooltip: "$(debug-restart) Restart language server", }, - LANGUAGE_SERVER_NOT_INSTALLED: { - icon: "$(circle-slash)", - colour: new vscode.ThemeColor("statusBarItem.errorBackground"), - command: installBuf.name, - tooltip: "$(circle-slash) Buf not installed", - }, }; const busyStatusConfig: StatusBarConfig = { diff --git a/src/status.ts b/src/status.ts index 1a929e69..d6520da7 100644 --- a/src/status.ts +++ b/src/status.ts @@ -12,7 +12,6 @@ const _languageServerStatus = [ "LANGUAGE_SERVER_RUNNING", "LANGUAGE_SERVER_STOPPED", "LANGUAGE_SERVER_ERRORED", - "LANGUAGE_SERVER_NOT_INSTALLED", ] as const; /** diff --git a/test/integration/buf-binary.test.ts b/test/integration/buf-binary.test.ts index 59189722..2bf1382f 100644 --- a/test/integration/buf-binary.test.ts +++ b/test/integration/buf-binary.test.ts @@ -1,95 +1,14 @@ import assert from "node:assert"; import * as cp from "node:child_process"; -import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; -import * as vscode from "vscode"; import { promisify } from "node:util"; import { effect } from "@preact/signals-core"; -import { HttpResponse, http } from "msw"; -import { setupServer } from "msw/node"; +import * as vscode from "vscode"; import which from "which"; -import { stopLanguageServer } from "../../src/commands/stop-lsp"; -import * as config from "../../src/config"; -import { githubReleaseURL, type Release } from "../../src/github"; import { bufState } from "../../src/state"; import type { LanguageServerStatus } from "../../src/status"; - -/** - * The test asset download URL. We use a single test download URL for all assets so that - * tests are consistent across platforms. - */ -const assetDownloadURL = - "https://api.github.com/repos/bufbuild/buf/releases/assets/"; -const downloadBinPath = "test/workspaces/empty-single/node_modules/@bufbuild/"; - -/** - * msw stub handlers for GitHub releases API. - */ -const handlers = [ - http.get(`${githubReleaseURL}tags/:tag`, ({ params }) => { - if (typeof params.tag !== "string") { - return HttpResponse.json({ error: params.tag }, { status: 404 }); - } - return HttpResponse.json({ - name: params.tag, - tag_name: params.tag, - assets: [ - { - name: "buf-Darwin-arm64", - url: `${assetDownloadURL}buf-darwin-arm64`, - }, - { - name: "buf-Darwin-x86_64", - url: `${assetDownloadURL}buf-darwin-x64`, - }, - { - name: "buf-Linux-x86_64", - url: `${assetDownloadURL}buf-linux-x64`, - }, - { - name: "buf-Linux-aarch64", - url: `${assetDownloadURL}buf-linux-aarch64`, - }, - { - name: "buf-Windows-x86_64.exe", - url: `${assetDownloadURL}buf-win32-x64`, - }, - { - name: "buf-Windows-arm64.exe", - url: `${assetDownloadURL}buf-win32-arm64`, - }, - ], - } satisfies Release); - }), - http.get(`${assetDownloadURL}:platformKey`, ({ params }) => { - try { - const bin = fs.readFileSync( - os.platform() === "win32" - ? path.resolve( - __dirname, - `../../../${downloadBinPath}${params.platformKey}/bin/buf.exe` - ) - : `${downloadBinPath}${params.platformKey}/bin/buf` - ); - const stream = new ReadableStream({ - start(controller) { - controller.enqueue(bin); - controller.close(); - }, - }); - return new HttpResponse(stream, { - headers: { - "content-type": "application/octet-stream", - }, - }); - } catch (e) { - return HttpResponse.json({ error: e }, { status: 404 }); - } - }), -]; - -const server = setupServer(...handlers); +import { server } from "../shared/shared"; /** * Wraps {@link cp.exec} into an async call. @@ -103,107 +22,45 @@ suite("manage buf binary and LSP", () => { suiteTeardown(async () => { server.close(); - await config.update("commandLine.path", undefined); - await config.update("commandLine.version", undefined); }); teardown(async () => { - // Reset the state of the extension after each test case - await stopLanguageServerForTest(); - if (config.get("commandLine.path")) { - const languageServerRunning = setupLanguageServerListener( - "LANGUAGE_SERVER_RUNNING" - ); - await config.update("commandLine.path", undefined); - await languageServerRunning; - await stopLanguageServerForTest(); - } - if (config.get("commandLine.version")) { - const languageServerRunning = setupLanguageServerListener( - "LANGUAGE_SERVER_RUNNING" - ); - await config.update("commandLine.version", undefined); - await languageServerRunning; - await stopLanguageServerForTest(); - } server.resetHandlers(); }); - test("no configs, use system buf on $PATH", async () => { + test(`setup buf ${process.env.BUF_INSTALLED}`, async () => { const languageServerRunning = setupLanguageServerListener( "LANGUAGE_SERVER_RUNNING" ); - // Must activate the extension as part of the test await vscode.extensions.getExtension("bufbuild.vscode-buf")?.activate(); await languageServerRunning; - const { stdout, stderr } = await exec("buf --version"); - assert.strictEqual(stderr, ""); - const bufBinaryVersion = bufState.getBufBinaryVersion(); - assert.ok(bufBinaryVersion); - assert.strictEqual(bufBinaryVersion.compare(stdout), 0); - const bufFilename = os.platform() === "win32" ? "buf.exe" : "buf"; - const bufPath = await which(bufFilename, { nothrow: true }); - const installedBufBinaryPath = bufState.getBufBinaryPath(); - assert.ok(installedBufBinaryPath); - assert.strictEqual(bufPath, installedBufBinaryPath); - }); - - test("configure commandLine.path", async () => { - // Setup a listener for the language server status - const languageServerRunning = setupLanguageServerListener( - "LANGUAGE_SERVER_RUNNING" - ); - let configPath = "node_modules/.bin/buf"; - if (os.platform() === "win32") { - configPath = path.resolve( - __dirname, - `../../../test/workspaces/empty-single/node_modules/@bufbuild/buf-${os.platform()}-${os.arch()}/bin/buf.exe` + // This value is set in the GitHub Actions testing workflow + if (process.env.BUF_INSTALLED === "buf-on-path") { + // We expected buf to be installed on the system $PATH and for that to be used. + const { stdout, stderr } = await exec("buf --version"); + assert.strictEqual(stderr, ""); + const bufFilename = os.platform() === "win32" ? "buf.exe" : "buf"; + const bufPath = await which(bufFilename, { nothrow: true }); + const installedBufBinaryPath = bufState.getBufBinaryPath(); + assert.ok(installedBufBinaryPath); + assert.strictEqual(bufPath, installedBufBinaryPath); + const bufBinaryVersion = bufState.getBufBinaryVersion(); + assert.ok(bufBinaryVersion); + assert.strictEqual(bufBinaryVersion.compare(stdout), 0); + } else { + // We expect no buf CLI in the $PATH and the installation flow to trigger. + const bufBinaryPath = bufState.getBufBinaryPath(); + assert.ok(bufBinaryPath); + assert.ok( + path.matchesGlob( + bufBinaryPath, + `**/.vscode-test/user-data/User/globalStorage/bufbuild.vscode-buf/v1.54.0/buf*` + ) ); } - // Update the configuration to use a path for the buf binary. This will trigger a new - // install process for the buf binary, which then starts the language server after. - await config.update("commandLine.path", configPath); - await languageServerRunning; - - // Assert the binary path is the configured path - const bufBinaryPath = bufState.getBufBinaryPath(); - assert.ok(bufBinaryPath); - assert.ok(bufBinaryPath.endsWith(configPath), bufBinaryPath); - }); - - test("configure commandLine.update", async () => { - const languageServerRunning = setupLanguageServerListener( - "LANGUAGE_SERVER_RUNNING" - ); - const configuredVersion = "v1.54.0"; - await config.update("commandLine.version", configuredVersion); - await languageServerRunning; - - // Assert the binary path used is the "downloaded" binary in global storage - const bufBinaryPath = bufState.getBufBinaryPath(); - assert.ok(bufBinaryPath); - assert.ok( - path.matchesGlob( - bufBinaryPath, - `**/.vscode-test/user-data/User/globalStorage/bufbuild.vscode-buf/v1.54.0/buf*` - ), - bufBinaryPath - ); }); }); -/** - * A helper function for stopping the language server and ensuring the status is stable using - * a listener. - */ -async function stopLanguageServerForTest() { - const languageServerStopped = setupLanguageServerListener( - "LANGUAGE_SERVER_STOPPED" - ); - await stopLanguageServer.execute(); - await languageServerStopped; -} - /** * A helper function that returns a Promise listening for the language server status. Once * the language server is the status we want to listen for, the promise resolves. If the @@ -219,10 +76,7 @@ function setupLanguageServerListener( resolve(); dispose(); } - if ( - languageServerStatus === "LANGUAGE_SERVER_NOT_INSTALLED" || - languageServerStatus === "LANGUAGE_SERVER_ERRORED" - ) { + if (languageServerStatus === "LANGUAGE_SERVER_ERRORED") { reject( new Error(`language server in failed state: ${languageServerStatus}`) ); diff --git a/test/playwright/extension.test.ts b/test/playwright/extension.test.ts index 74670167..721e9b06 100644 --- a/test/playwright/extension.test.ts +++ b/test/playwright/extension.test.ts @@ -426,11 +426,6 @@ extensionTest.describe("command palette", async () => { "Prune module dependencies.", "Update module dependencies.", "Generate", - "List module files.", - "Price for BSR paid plans.", - "Module stats", - "Install CLI", - "Update CLI", "Show Buf Output", ]; for (const expectation of expectations) { diff --git a/test/playwright/global-setup.ts b/test/playwright/global-setup.ts new file mode 100644 index 00000000..36db1413 --- /dev/null +++ b/test/playwright/global-setup.ts @@ -0,0 +1,8 @@ +import { server } from "../shared/shared"; + +async function globalSetup() { + server.listen(); + return () => server.close(); +} + +export default globalSetup; diff --git a/test/shared/shared.ts b/test/shared/shared.ts new file mode 100644 index 00000000..f0a26b7b --- /dev/null +++ b/test/shared/shared.ts @@ -0,0 +1,81 @@ +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { HttpResponse, http } from "msw"; +import { setupServer } from "msw/node"; + +/** + * The test asset download URL. We use a single test download URL for all assets so that + * tests are consistent across platforms. + * + * githubReleaseURL matches the value in src/github.ts. + */ +const assetDownloadURL = + "https://api.github.com/repos/bufbuild/buf/releases/assets/"; +const downloadBinPath = "test/workspaces/empty-single/node_modules/@bufbuild/"; +const githubReleaseURL = "https://api.github.com/repos/bufbuild/buf/releases/"; + +/** + * msw stub handlers for GitHub releases API. + */ +const handlers = [ + http.get(`${githubReleaseURL}latest`, () => { + return HttpResponse.json({ + name: "v1.54.0", + tag_name: "v1.54.0", + assets: [ + { + name: "buf-Darwin-arm64", + url: `${assetDownloadURL}buf-darwin-arm64`, + }, + { + name: "buf-Darwin-x86_64", + url: `${assetDownloadURL}buf-darwin-x64`, + }, + { + name: "buf-Linux-x86_64", + url: `${assetDownloadURL}buf-linux-x64`, + }, + { + name: "buf-Linux-aarch64", + url: `${assetDownloadURL}buf-linux-aarch64`, + }, + { + name: "buf-Windows-x86_64.exe", + url: `${assetDownloadURL}buf-win32-x64`, + }, + { + name: "buf-Windows-arm64.exe", + url: `${assetDownloadURL}buf-win32-arm64`, + }, + ], + }); + }), + http.get(`${assetDownloadURL}:platformKey`, ({ params }) => { + try { + const bin = fs.readFileSync( + os.platform() === "win32" + ? path.resolve( + __dirname, + `../../../${downloadBinPath}${params.platformKey}/bin/buf.exe` + ) + : `${downloadBinPath}${params.platformKey}/bin/buf` + ); + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(bin); + controller.close(); + }, + }); + return new HttpResponse(stream, { + headers: { + "content-type": "application/octet-stream", + }, + }); + } catch (e) { + return HttpResponse.json({ error: e }, { status: 404 }); + } + }), +]; + +export const server = setupServer(...handlers);