Metadata-Version: 2.4
Name: xonsh-lsp
Version: 0.2.1
Summary: Language Server Protocol implementation for xonsh
Project-URL: Homepage, https://github.com/FoamScience/xonsh-language-server
Project-URL: Repository, https://github.com/FoamScience/xonsh-language-server
Project-URL: Issues, https://github.com/FoamScience/xonsh-language-server/issues
Author-email: Mohammed Elwardi Fadeli <elwardifadeli@gmail.com>
License-Expression: MIT
Keywords: editor,ide,language-server,lsp,python,shell,xonsh
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development
Classifier: Topic :: Text Editors :: Integrated Development Environments (IDE)
Requires-Python: >=3.10
Requires-Dist: cattrs>=23.0.0
Requires-Dist: lsprotocol>=2023.0.0
Requires-Dist: pygls>=1.3.0
Requires-Dist: tree-sitter-xonsh>=0.2.1
Requires-Dist: tree-sitter>=0.25.2
Provides-Extra: all
Requires-Dist: jedi>=0.19.0; extra == 'all'
Provides-Extra: dev
Requires-Dist: jedi>=0.19.0; extra == 'dev'
Requires-Dist: mypy>=1.0.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
Requires-Dist: pytest>=7.0.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Provides-Extra: jedi
Requires-Dist: jedi>=0.19.0; extra == 'jedi'
Description-Content-Type: text/markdown

# xonsh-lsp

A Language Server Protocol (LSP) implementation for [xonsh](https://xon.sh/), the Python-powered shell.

## Features

- [x] **Syntax Highlighting** (via tree-sitter-xonsh integration)
  - And optional symantic tokens (contributed by @nahoj, #6)
- [x] **Code Completion**
  - [x] Environment variables (`$VAR`, `${expr}`)
  - [x] Subprocess commands from PATH
  - [x] Shell builtins (cd, echo, jobs, etc.)
  - [x] Xonsh builtins and aliases (source, xontrib, aliases, etc.)
  - [x] Python completions (via Jedi or external backend)
  - [x] Path completions with directory traversal
  - [x] Glob pattern completions (`` `...` ``)
  - [x] At-object completions (`@.env`, `@.imp`)
  - [x] Path literal completions (`p"..."`, `pf"..."`)
- [x] **Diagnostics**
  - [x] Syntax errors (via tree-sitter)
  - [x] Undefined environment variables (with quick fix)
  - [x] Unknown commands (hint severity)
  - [x] Empty subprocess warnings
  - [x] Python errors (via Jedi or external backend)
- [x] **Hover Information**
  - [x] Environment variable values
  - [x] Xonsh operator documentation (`$()`, `!()`, `$[]`, `![]`, `@()`, `@$()`)
  - [x] Xonsh builtin documentation
  - [x] Command paths and `--help` preview
  - [x] Python symbol information with signatures
  - [x] Path literal documentation
- [x] **Go to Definition**
  - [x] Python definitions (via Jedi or external backend)
  - [x] Environment variable assignments
  - [x] Alias definitions
  - [x] Function definitions
- [x] **Find References**
  - [x] Python references (via Jedi or external backend)
  - [x] Environment variable references
  - [x] Symbol references
- [x] **Signature Help**
  - [x] Python function signatures (via Jedi or external backend)
  - [x] Parameter tracking
- [x] **Document Symbols**
  - [x] Functions, classes, variables, modules
- [x] **Code Actions**
  - [x] Quick fix for undefined environment variables
- [x] **Inlay Hints**
  - [x] Type annotations after xonsh expressions (`$()`, `!()`, `p"…"`, `$VAR`, …)
  - [x] Env-var values after `$VAR` (opt-in via `inlayHints.envVarValues`)
  - [x] Python inlay hints forwarded from the backend (Pyright/ty)
- [x] **Typed `__xonsh_env__` stub for the Python backend**
  - [x] Generated TypedDict from xonsh's env-var registry — `$XONSH_DEBUG` types as `int`, `$AUTO_CD` as `bool`, etc. (Pyright/ty backends only)
  - [x] User-defined env vars (`$X = …`) added per-document as `Any`
- [x] **Multi-Backend Support**
  - [x] Built-in Jedi backend (default, no external dependencies)
  - [x] LSP proxy to Pyright, basedpyright, pylsp, ty, or any LSP server
  - [x] Transparent settings passthrough (editor → child backend)
  - [x] Fallback `backendSettings` via `initializationOptions`

## Installation

```bash
pip install xonsh-lsp
# or just run
uvx xonsh-lsp
```

To install with the built-in Jedi backend (recommended for standalone use):

```bash
pip install "xonsh-lsp[jedi]"
```

Jedi is optional — if you configure an external backend (Pyright, ty, etc.), you don't need Jedi installed.

## Usage

### Command Line

```bash
# Start with stdio (default, for editor integration)
xonsh-lsp

# Start with TCP
xonsh-lsp --tcp --host 127.0.0.1 --port 2087

# Debug mode
xonsh-lsp --log-level DEBUG

# Use Pyright as the Python backend
xonsh-lsp --python-backend pyright

# Use ty as the Python backend
xonsh-lsp --python-backend ty

# Use a custom LSP server command
xonsh-lsp --python-backend lsp-proxy --backend-command my-lsp-server --stdio

# Enable LSP semantic-tokens syntax highlighting (off by default)
xonsh-lsp --semantic-tokens
```

### Neovim Integration

> xonsh-lsp is not yet in the canonical nvim-lspconfig registry. For now, configure it
> manually using Neovim's native LSP API (0.11+) or the nvim-lspconfig custom-server pattern.

#### 1. Register the xonsh filetype

Place this somewhere early in your config (e.g. an `autocmds.lua` file):

```lua
vim.filetype.add({
  extension = { xsh = 'xonsh', xonshrc = 'xonsh' },
  filename  = { ['.xonshrc'] = 'xonsh', ['xonshrc'] = 'xonsh' },
})
```

#### 2. Configure the language server

**Neovim 0.11+ (native `vim.lsp`)**

```lua
vim.lsp.config('xonsh_lsp', {
  cmd = { 'xonsh-lsp' },             -- or { 'uvx', '-n', 'xonsh-lsp' }
  filetypes = { 'xonsh' },
  root_markers = { '.xonshrc', 'xonshrc', '.git' },
})
vim.lsp.enable('xonsh_lsp')
```

**nvim-lspconfig (manual server registration)**

```lua
local lspconfig = require('lspconfig')
local configs = require('lspconfig.configs')

if not configs.xonsh_lsp then
  configs.xonsh_lsp = {
    default_config = {
      cmd = { 'xonsh-lsp' },
      filetypes = { 'xonsh' },
      root_dir = function(fname)
        return lspconfig.util.find_git_ancestor(fname) or vim.fn.getcwd()
      end,
      settings = {},
    },
  }
end

lspconfig.xonsh_lsp.setup({})
```

#### 3. Choose a Python backend

xonsh-lsp delegates Python analysis to an external backend. Set it via `init_options`:

**ty (recommended — extremely fast, written in Rust)**

```lua
vim.lsp.config('xonsh_lsp', {
  cmd = { 'uvx', '-n', 'xonsh-lsp' },
  filetypes = { 'xonsh' },
  root_markers = { '.xonshrc', 'xonshrc', '.git' },
  init_options = {
    pythonBackend = 'ty',
    -- Optional: pin the ty binary (e.g. from Mason)
    -- pythonBackendCommand = { '/path/to/ty', 'server' },
  },
  settings = {
    environment = {
      python = vim.fn.exepath('python3'),
    },
  },
})
vim.lsp.enable('xonsh_lsp')
```

**Pyright**

```lua
vim.lsp.config('xonsh_lsp', {
  cmd = { 'uvx', '-n', 'xonsh-lsp' },
  filetypes = { 'xonsh' },
  root_markers = { '.xonshrc', 'xonshrc', '.git' },
  init_options = {
    pythonBackend = 'pyright',
  },
  settings = {
    python = {
      pythonPath = vim.fn.exepath('python3'),
      analysis = {
        autoSearchPaths = true,
        useLibraryCodeForTypes = true,
      },
    },
  },
})
vim.lsp.enable('xonsh_lsp')
```

Your Python backend settings (e.g. `settings.python.*`, `settings.environment.*`) are
forwarded transparently to the child LSP — xonsh-lsp does not detect or override paths itself.

#### 4. (Optional) Tree-sitter highlighting

For syntax highlighting, install the [tree-sitter-xonsh](https://github.com/FoamScience/tree-sitter-xonsh) parser:

```lua
-- Register the parser so :TSInstall xonsh works
vim.api.nvim_create_autocmd('User', {
  pattern = 'TSUpdate',
  callback = function()
    require('nvim-treesitter.parsers').xonsh = {
      install_info = {
        url = 'https://github.com/FoamScience/tree-sitter-xonsh',
        queries = 'queries/',
      },
    }
  end,
})

-- Start treesitter highlighting for xonsh buffers
vim.api.nvim_create_autocmd('FileType', {
  pattern = 'xonsh',
  callback = function(args)
    if not require('nvim-treesitter.parsers').xonsh then
      vim.treesitter.start(args.buf, 'xonsh')
    end
  end,
})
```

#### 5. (Optional) UV project Python detection

If you use [uv](https://docs.astral.sh/uv/) for project management, you can
auto-detect the correct Python interpreter and notify the backend via
`workspace/didChangeConfiguration` in your `on_attach`:

```lua
vim.lsp.config('xonsh_lsp', {
  cmd = { 'uvx', '-n', 'xonsh-lsp' },
  filetypes = { 'xonsh' },
  root_markers = { '.xonshrc', 'xonshrc', '.git' },
  init_options = { pythonBackend = 'ty' },
  settings = {
    environment = { python = vim.fn.exepath('python3') },
  },
  on_attach = function(client, bufnr)
    local filepath = vim.api.nvim_buf_get_name(bufnr)
    local dir = vim.fn.fnamemodify(filepath, ':h')
    vim.system(
      { 'uv', 'python', 'find' },
      { text = true, timeout = 1000, cwd = dir },
      vim.schedule_wrap(function(result)
        if not result or not result.stdout then return end
        local py = result.stdout:match('^%s*(.-)%s*$')
        if py == '' or vim.fn.executable(py) ~= 1 then return end
        client.settings = vim.tbl_deep_extend('force', client.settings or {}, {
          environment = { python = py },
        })
        client:notify('workspace/didChangeConfiguration', {
          settings = client.settings,
        })
      end)
    )
  end,
})
vim.lsp.enable('xonsh_lsp')
```

## Configuration

### Python Backend

xonsh-lsp supports multiple Python analysis backends. The backend handles completions, hover,
go-to-definition, references, signature help, and diagnostics for Python code within xonsh files.

| Backend | Command | Description |
|---------|---------|-------------|
| `jedi` (default) | built-in | Uses [Jedi](https://github.com/davidhalter/jedi) for static analysis. Works out of the box, no external process. |
| `pyright` | `pyright-langserver --stdio` | Microsoft's [Pyright](https://github.com/microsoft/pyright) type checker. Respects `pyrightconfig.json`. |
| `basedpyright` | `basedpyright-langserver --stdio` | Community fork [basedpyright](https://github.com/DetachHead/basedpyright) with additional features. |
| `pylsp` | `pylsp` | The [Python LSP Server](https://github.com/python-lsp/python-lsp-server) with plugin ecosystem. |
| `ty` | `ty server` | Astral's [ty](https://github.com/astral-sh/ty) type checker — extremely fast, written in Rust. |
| `lsp-proxy` | custom | Any LSP server via `--backend-command`. |

The backend can be set via CLI args or editor `initializationOptions`:

**CLI:**
```bash
xonsh-lsp --python-backend pyright
```

**Editor initializationOptions:**
```jsonc
{
  "pythonBackend": "pyright"
}
```

### Initialization Options

The full set of `initializationOptions` accepted by xonsh-lsp:

```jsonc
{
  // Python analysis backend: "jedi", "pyright", "basedpyright", "pylsp", "ty", or "lsp-proxy"
  "pythonBackend": "jedi",

  // Custom command for the "lsp-proxy" backend (not needed for named backends)
  "pythonBackendCommand": ["my-lsp-server", "--stdio"],

  // Fallback settings sent to the backend during initialization.
  // Normally you should configure your backend via the editor's settings
  // (e.g. Neovim's `settings = { ... }`), which are forwarded transparently.
  // Use this only if your editor doesn't support workspace/configuration.
  "backendSettings": { },

  // Enable LSP semantic tokens for syntax highlighting (default: false).
  // Off by default because most editors already highlight xonsh via
  // tree-sitter or a TextMate grammar, and overlapping semantic tokens can
  // fight with those. Turn on if your editor relies on LSP for highlighting.
  "semanticTokens": false,

  // Inlay-hint preferences. The xonsh server emits native hints for
  // xonsh-specific syntax in addition to whatever the Python backend
  // returns for Python regions.
  "inlayHints": {
    // ": str" / ": CommandPipeline" / ": Path" / ": bool" (from the
    // xonsh env var registry) after $VAR, $(), !(), p"…", etc.
    "xonshTypes": true,
    // " = /home/user" after $HOME (resolved from the server's process env).
    "envVarValues": false,
    // Alias kind after unknown commands (reserved for v2).
    "aliasResolution": false
  }
}
```

### Settings Passthrough

When the child backend requests its configuration (via `workspace/configuration`),
xonsh-lsp forwards the request to your editor and returns the editor's response.
This means your existing Python backend settings — `pythonPath`, `python.analysis.*`,
etc. — work exactly as if the backend were running standalone.

If the editor doesn't respond (or `workspace/configuration` is not supported),
xonsh-lsp falls back to `backendSettings` from `initializationOptions`.

### How the LSP Proxy Works

When configured with an external backend, xonsh-lsp acts as LSP middleware:

1. **Receives** requests from the editor (completions, hover, etc.)
2. **Preprocesses** xonsh syntax to valid Python (e.g., `$HOME` → `__xonsh_env__["HOME"]`)
3. **Forwards** the preprocessed Python to the child LSP server
4. **Maps** positions in the response back to the original xonsh source
5. **Merges** results with xonsh-specific features (env vars, operators, commands)

Xonsh-specific features (environment variable completions, operator hover, subprocess
diagnostics, etc.) are always handled natively — only Python analysis is delegated to
the backend.

**Diagnostics** use a two-phase merge:
- Xonsh diagnostics (syntax errors, undefined env vars, unknown commands) are published immediately
- Python diagnostics from the backend arrive asynchronously and are merged with the cached xonsh diagnostics

## Architecture

```mermaid
flowchart RL
    subgraph xonsh-lsp
        parser[tree-sitter-xonsh parser]

        parser --> python[Python Regions]
        parser --> xonsh[Xonsh Builtins]
        parser --> subprocess[Subprocess Commands]

        python --> backend{PythonBackend}
        backend -->|default| jedi[JediBackend]
        backend -->|proxy| proxy[LspProxyBackend]
        proxy --> child[Child LSP: Pyright / ty / basedpyright / pylsp]
        xonsh --> native1[Native Handler]
        subprocess --> native2[Native Handler]
    end
```

## Contributing

Contributions are welcome! Please feel free to submit issues and pull requests; especially flagging unsupported xonsh syntax since this LSP is young.

## License

MIT

## Related Projects

- [tree-sitter-xonsh](https://github.com/FoamScience/tree-sitter-xonsh) - Tree-sitter grammar for xonsh
- [xonsh](https://xon.sh/) - The xonsh shell
- [pygls](https://github.com/openlawlibrary/pygls) - Python LSP library
- [jedi](https://github.com/davidhalter/jedi) - Python static analysis
- [ty](https://github.com/astral-sh/ty) - Extremely fast Python type checker and LSP
- [pyright](https://github.com/microsoft/pyright) - Static type checker for Python
