# LSP Server

Shuck ships with a first-party Language Server Protocol server in the main CLI. Start it with `shuck server` and point your editor at that command over stdio.

That gives editor diagnostics from the current in-memory buffer instead of rerunning `shuck check` from scratch on every save.

## Current scope

Shuck advertises a capability only after its request handler is implemented. The
scope still varies by feature: some requests analyze only the active document,
while workspace symbols and call hierarchy maintain bounded indexes across
files.

| Feature | LSP methods | Scope and limitations |
| --- | --- | --- |
| Document synchronization | `textDocument/didOpen`, `didChange`, `didClose` | Incremental synchronization for open shell buffers. Dialect inference uses the buffer `languageId`, shebang, and file path. |
| Diagnostics | `textDocument/diagnostic`, `textDocument/publishDiagnostics`, `workspace/diagnostic` | Pull diagnostics when the client supports them, with push diagnostics as a fallback. Workspace pulls are enabled by default and discover shell files lazily, honor workspace and ignore boundaries, prefer open buffers, reuse stable result IDs, report partial progress, and stop at configurable file, visited-entry, and source-byte limits. Inter-file diagnostic dependencies are not supported. |
| Code actions | `textDocument/codeAction`, `codeAction/resolve` | Quick fixes, disable-this-line actions, and `source.fixAll.shuck` for the active document. |
| Completion | `textDocument/completion`, `completionItem/resolve` | Variables, declaration names, runtime names, builtins, shell keywords, and functions visible in the active document or through unconditional static `source`/`.` edges. Literal paths, valid source hints, configured source paths, and unsaved open buffers participate; dynamic or unresolved sources do not. Command-specific option completion is not supported. |
| Hover | `textDocument/hover` | Semantic symbol information plus help for suppression codes in `# shuck:` and `# shellcheck` directives. Function calls can resolve through the same exact static source graph as go-to-definition, including source hints, configured source paths, and open buffers. Dynamic or ambiguous sources are not guessed, and Shuck does not fetch command manuals. |
| Go to definition | `textDocument/definition` | Variables resolve within the active document. Function calls also resolve through literal sources, valid `# shuck: source=` hints, and configured source paths, with open buffers and shell source order taking precedence. Dynamic sources are not guessed. |
| Document links | `textDocument/documentLink` | Resolvable literal `source`/`.` operands and valid source hints link to their target files. Configured source paths and unsaved open buffers participate; dynamic, missing, and out-of-workspace targets are omitted. |
| Find references | `textDocument/references` | Variables remain document-local. Function references follow exact binding identity across the statically resolved source graph, including source hints, configured source paths, cycles, and open buffers. Dynamic or ambiguous calls are omitted, and `includeDeclaration` is honored. |
| Document highlights | `textDocument/documentHighlight` | Read and write occurrences within the active document. |
| Document symbols | `textDocument/documentSymbol` | Functions, declarations, and assignments from the active document. |
| Workspace symbols | `workspace/symbol` | Fuzzy search over open buffers and discovered shell files in workspace folders, subject to a configurable file limit. |
| Rename | `textDocument/prepareRename`, `textDocument/rename` | Variables remain document-local. Function rename spans the exact statically resolved source graph by default when the client supports `documentChanges`; set `server.rename.allowCrossFile` to `false` to keep edits in the active document. Open buffers use versioned edits, while incomplete, ambiguous, stale, overlapping, or out-of-workspace edit sets are rejected atomically. |
| Formatting | `textDocument/formatting`, `textDocument/rangeFormatting` | Whole-document formatting or the smallest complete statement containing the selected range. Incomplete syntactic fragments are left unchanged. |
| Folding ranges | `textDocument/foldingRange` | Functions, conditionals, loops, case statements, subshells, brace groups, heredocs, own-line comment blocks, and active continuation regions. Closing delimiters remain visible, malformed or crossing ranges are omitted, and client range limits are honored. |
| Call hierarchy | `textDocument/prepareCallHierarchy`, `callHierarchy/incomingCalls`, `callHierarchy/outgoingCalls` | Preparation, incoming calls, and outgoing calls work across the statically resolvable source graph. Preparing on a sourced function call returns the exact target definition, including when a file redefines the same function name. Literal sources and valid `# shuck: source=` hints participate; runtime-only dispatch and unresolved dynamic sources do not. |
| Selection ranges | `textDocument/selectionRange` | Parser-backed expansion from words and nested expressions through commands, compound constructs, and the complete file. Comments, whitespace, heredocs, incomplete input, multiple positions, and UTF-8/16/32 position encodings are handled predictably. |

Editor formatting uses the same parser-backed formatter as [`shuck format`](/docs/formatting). Once `shuck server` is attached, use your editor's normal LSP format command or format-on-save integration.

### Planned or deferred

- Language-server support for embedded shell regions, such as scripts inside
  GitHub Actions YAML, needs a separate position-remapping design.

### Not currently planned

Shuck does not currently advertise semantic tokens, inlay hints, signature
help, or notebook documents. These are not permanent
prohibitions, but there is no accepted implementation design for them today.

Workspace pulls are enabled by default. Disable them through initialization
options if an editor should only request diagnostics for open documents:

```json
{
  "shuck": {
    "server": {
      "workspaceDiagnostics": {
        "enabled": false
      }
    }
  }
}
```

Shuck does not scan a workspace at startup. Each request performs a cancellable
bounded discovery with defaults of 1,000 files, 10,000 visited entries, and 32
MiB of source text. Embedded host documents remain excluded until their
diagnostic position mapping is available. Dynamic configuration can change the
limits or disable subsequent workspace pulls.

This page is the user-facing support contract. The repository's
[design specs](https://github.com/ewhauser/shuck/tree/main/specs) record the
scope and decisions of individual implementation stages, so an older spec may
describe a feature as a non-goal that a later spec subsequently added. If an
advertised feature fails within the scope above, please file a bug. If the
requested behavior is listed as deferred or unsupported, file a feature request
instead.

## Before you start

Make sure `shuck` is installed and available on your `PATH`:

```bash
shuck --version
shuck server
```

If `shuck server` starts successfully, your editor only needs to launch that command over stdio.

Shuck also resolves `.shuck.toml` or `shuck.toml` from the workspace and layers any LSP-provided Shuck settings on top when the client supports them.

## Neovim

Neovim 0.11+ can use the built-in LSP client directly through [`vim.lsp`](https://neovim.io/doc/user/lsp/):

```lua
vim.lsp.config("shuck", {
  cmd = { "shuck", "server" },
  filetypes = { "sh", "bash", "zsh", "ksh" },
  root_markers = { ".shuck.toml", "shuck.toml", ".git" },
})

vim.lsp.enable("shuck")
```

If your setup normalizes shell buffers to `sh`, leaving only `sh` in `filetypes` is fine too.

## Vim

With [`vim-lsp`](https://github.com/prabirshrestha/vim-lsp), register Shuck in your `.vimrc`:

```vim
if executable('shuck')
    augroup shuck_lsp
        au!
        au User lsp_setup call lsp#register_server({
            \ 'name': 'shuck',
            \ 'cmd': {server_info->['shuck', 'server']},
            \ 'allowlist': ['sh'],
            \ })
    augroup END
endif
```

Most Vim setups detect shell scripts as `sh`. If yours uses dedicated `bash`, `zsh`, or `ksh` filetypes, add them to the allowlist as well.

## Emacs

[`Eglot`](https://www.gnu.org/software/emacs/manual/html_mono/eglot.html) can launch `shuck server` for shell buffers by extending `eglot-server-programs`:

```elisp
(with-eval-after-load 'eglot
  (add-to-list 'eglot-server-programs
               '(sh-mode . ("shuck" "server"))))
```

Then open a shell script and run `M-x eglot`.

If you use additional shell major modes, add them to the same entry or register a second one for those modes.

## Zed

Zed currently routes language servers through extensions rather than letting you point a language directly at an arbitrary stdio command from settings alone. The practical route today is a small dev extension that launches `shuck server` for `Shell Script` buffers.

In `extension.toml`, register a shell language server:

```toml
id = "shuck"
name = "Shuck"
version = "0.0.1"
schema_version = 1

[language_servers.shuck]
name = "Shuck"
languages = ["Shell Script"]

[language_servers.shuck.language_ids]
"Shell Script" = "shellscript"
```

Then implement `language_server_command` in the extension's Rust entrypoint, following the [Zed language extension docs](https://zed.dev/docs/extensions/languages):

```rust
fn language_server_command(
    &mut self,
    _language_server_id: &zed::LanguageServerId,
    _worktree: &zed::Worktree,
) -> Result<zed::Command> {
    Ok(zed::Command {
        command: "shuck".into(),
        args: vec!["server".into()],
        env: Default::default(),
    })
}
```

Once the extension is installed, prefer it for shell files in your Zed settings:

```json
{
  "languages": {
    "Shell Script": {
      "language_servers": ["shuck", "..."]
    }
  }
}
```

## Related docs

For project-wide lint configuration, see [Configuration](/docs/configuration) and [Settings Reference](/docs/settings).
