Skip to content

x/tools/gopls: revise gopls flag parsing #79906

Description

@hyangah

The gopls Command-Line Interface (CLI) enforces strict flag ordering constraints which adds a significant usability pain point. For example,

  1. Confusing Flag Application: Subcommands accept global flags that have no effect or are irrelevant. For example,

    • gopls -remote=auto version silently ignores -remote.
    • gopls remote debug -remote=.. or gopls remote -remote=.. debug are invalid which is unintuitive.
  2. Flag Collision & Silent Flag Ignoring: Running gopls -listen=:8000 mcp silently ignores -listen because the global flag configures the root LSP server but gets ignored, and the mcp subcommand runs over stdio because its subcommand-specific -listen flag is not set. gopls -listen=:8000 mcp -listen=:8001 will start only MCP server on 8001.

  3. Strict Ordering Constraints with Confusing Error Message: Placing a global flag (like -profile.cpu or -v) after the subcommand is invalid, but the error message is misleading. For example, gopls symbols -profile.cpu=cpu.out main.go or gopls version -v fails with flag provided but not defined: -profile.cpu or flag provided but not defined: -v, even though they are valid global flags.

As part of the gopls CLI redesign work, I propose to revise the flag parsing rule to better align with the official Go command’s flag parsing style, and make it more predictable.

  • Separating global flags from subcommand flags.
  • Aligning flag behavior with the go command style.
  • Maintaining compatibility with existing editor integrations.

Background

Initially, gopls only operated as an LSP server. Developers added subcommands like symbols, rename, mcp, and remote later. To maintain compatibility, running gopls without a subcommand must still start the server.

To support this default behavior, the root Application struct (defined in ./gopls/internal/cmd) embeds the Serve subcommand struct as an exported field. This design exposes all Serve flags globally.

Root Flag Pollution (Current Implementation)

The following table lists flags exposed at the root level in the current implementation:

Flag Type Description Intended Scope
-v, -verbose Global Verbose output Global
-vv, -veryverbose Global Very verbose output Global
-otel Global OpenTelemetry endpoint Global
-profile.cpu Global (Profiling) Write CPU profile to file Global
-profile.mem Global (Profiling) Write memory profile to file Global
-profile.trace Global (Profiling) Write execution trace to file Global
-remote Connection Forward commands to remote LSP Client Subcommands / Serve
-remote.debug Connection -debug for auto-launched daemon Client Subcommands / Serve
-remote.logfile Connection -logfile for auto-launched daemon Client Subcommands / Serve
-remote.listen.timeout Connection timeout for auto-launched daemon Client Subcommands / Serve
-listen Serve Address to listen on for LSP Serve
-listen.timeout Serve Shut down server if idle Serve
-debug Serve Serve debug info Serve
-logfile Serve Log filename Serve
-mode Serve Server mode (no effect, TODO: deprecate) Serve
-rpc.trace Serve Print RPC trace Serve
-mcp.listen Serve Listen for MCP connections Serve

Proposed Change

We refactor flag registration and introduce an argument pre-processor to handle the case where gopls is invoked without serve subcommand, or where the serve subcommand is used but the old style of flag ordering is used. This normalization runs before flag parsing.

Flag Localization

  1. Unexport serve flags: Rename the exported Serve field in the Application struct to serve. This unexports the field, which removes the serve flags from the global scope and resolves the -listen collision.
  2. Isolate connection flags: Define a RemoteFlags struct for -remote and -remote.* flags. Embed this struct only in the serve subcommand and in subcommands that connect to an LSP server.

Normalization for Compatibility

Many editor plugins (for example, VS Code Go, Neovim's nvim-lspconfig, Vim's vim-lsc, and Emacs' lsp-mode) configure gopls without any arguments. They often use -remote=auto to enable daemon mode. For debugging, users frequently append -rpc.trace and -logfile.

Because these editors start gopls without a subcommand, they rely on the default serve behavior and expect these flags to be globally available. The new design must preserve this capability to avoid breaking existing integrations.

Before passing arguments to the standard flag parser, we normalize the args/flags and handle two cases:

  • Case 1: Explicit Subcommand (Go tool style): Enforce Go-style command ordering. Global flags must precede the subcommand, and subcommand flags must follow it. NormalizeArgs validates the placement of all flags.

  • Case 2: Implicit serve (Hoisting): If the command line lacks a subcommand (for example, gopls -listen=localhost:3000 -profile.cpu=cpu.out), the pre-processor separates global flags from serve flags using reflection. It moves global flags to the front, inserts the serve subcommand, and appends subcommand flags to the end:

    • Before: gopls -listen=localhost:3000 -profile.cpu=cpu.out
    • After: gopls -profile.cpu=cpu.out serve -listen=localhost:3000

    This automatically preserves compatibility with existing editor integrations.

Helpful Error Messages

When validation fails in Case 1, NormalizeArgs rejects the command with a clear, actionable error message rather than the default Go flag error:

  • Misplaced Global Flag: Placing a global flag after a subcommand (for example, gopls symbols -profile.cpu=cpu.out) returns:

    global flag -profile.cpu=cpu.out must be placed before subcommand symbols

  • Misplaced Subcommand Flag: Placing a subcommand flag before the subcommand (for example, gopls -remote=auto symbols) returns: flag -remote=auto belongs to subcommand but is placed before it )

Alternative Considered

  • Allow flags to be specified anywhere: not great. Not go style.
  • Define all global flags in the last subcommands (using struct embedding): backwards compatible change is hard.

Metadata

Metadata

Assignees

Labels

ToolsThis label describes issues relating to any tools in the x/tools repository.goplsIssues related to the Go language server, gopls.

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions