feat: initial blink-cmp-ssh implementation
Problem: the existing blink-cmp-sshconfig plugin uses a synchronous, build-time Python scraping approach that requires uv and make to generate a static Lua file. Solution: implement a runtime, async blink.cmp source that parses ssh_config keywords from man ssh_config and enum values from ssh -Q queries, matching the architecture of blink-cmp-tmux and blink-cmp-ghostty.
This commit is contained in:
parent
01d8b4eb5e
commit
ad6c683052
28 changed files with 1158 additions and 0 deletions
9
.busted
Normal file
9
.busted
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
return {
|
||||||
|
_all = {
|
||||||
|
lua = 'nvim -l',
|
||||||
|
ROOT = { './spec/' },
|
||||||
|
},
|
||||||
|
default = {
|
||||||
|
verbose = true,
|
||||||
|
},
|
||||||
|
}
|
||||||
9
.editorconfig
Normal file
9
.editorconfig
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
insert_final_newline = true
|
||||||
|
charset = utf-8
|
||||||
|
|
||||||
|
[*.lua]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
17
.github/DISCUSSION_TEMPLATE/q-a.yaml
vendored
Normal file
17
.github/DISCUSSION_TEMPLATE/q-a.yaml
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
title: 'Q&A'
|
||||||
|
labels: []
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Use this space for questions, ideas, and general discussion about blink-cmp-ssh.
|
||||||
|
For bug reports, please [open an issue](https://github.com/barrettruth/blink-cmp-ssh/issues/new/choose) instead.
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Question or topic
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Context
|
||||||
|
description: Any relevant details (Neovim version, config, screenshots)
|
||||||
85
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
85
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
name: Bug Report
|
||||||
|
description: Report a bug
|
||||||
|
title: 'bug: '
|
||||||
|
labels: [bug]
|
||||||
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Prerequisites
|
||||||
|
options:
|
||||||
|
- label:
|
||||||
|
I have searched [existing
|
||||||
|
issues](https://github.com/barrettruth/blink-cmp-ssh/issues)
|
||||||
|
required: true
|
||||||
|
- label: I have updated to the latest version
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 'Neovim version'
|
||||||
|
description: 'Output of `nvim --version`'
|
||||||
|
render: text
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: 'Operating system'
|
||||||
|
placeholder: 'e.g. Arch Linux, macOS 15, Ubuntu 24.04'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: What happened? What did you expect?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: Minimal steps to trigger the bug
|
||||||
|
value: |
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Minimal reproduction
|
||||||
|
description: |
|
||||||
|
Save the script below as `repro.lua`, edit if needed, and run:
|
||||||
|
```
|
||||||
|
nvim -u repro.lua
|
||||||
|
```
|
||||||
|
Confirm the bug reproduces with this config before submitting.
|
||||||
|
render: lua
|
||||||
|
value: |
|
||||||
|
vim.env.LAZY_STDPATH = '.repro'
|
||||||
|
load(vim.fn.system('curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua'))()
|
||||||
|
require('lazy.nvim').setup({
|
||||||
|
spec = {
|
||||||
|
{
|
||||||
|
'saghen/blink.cmp',
|
||||||
|
dependencies = {
|
||||||
|
'barrettruth/blink-cmp-ssh',
|
||||||
|
},
|
||||||
|
opts = {
|
||||||
|
sources = {
|
||||||
|
default = { 'ssh' },
|
||||||
|
providers = {
|
||||||
|
ssh = {
|
||||||
|
name = 'SSH',
|
||||||
|
module = 'blink-cmp-ssh',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
5
.github/ISSUE_TEMPLATE/config.yaml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yaml
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Questions
|
||||||
|
url: https://github.com/barrettruth/blink-cmp-ssh/discussions
|
||||||
|
about: Ask questions and discuss ideas
|
||||||
30
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
30
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
name: Feature Request
|
||||||
|
description: Suggest a feature
|
||||||
|
title: 'feat: '
|
||||||
|
labels: [enhancement]
|
||||||
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Prerequisites
|
||||||
|
options:
|
||||||
|
- label:
|
||||||
|
I have searched [existing
|
||||||
|
issues](https://github.com/barrettruth/blink-cmp-ssh/issues)
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Problem
|
||||||
|
description: What problem does this solve?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Proposed solution
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Alternatives considered
|
||||||
21
.github/workflows/luarocks.yaml
vendored
Normal file
21
.github/workflows/luarocks.yaml
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
name: luarocks
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
quality:
|
||||||
|
uses: ./.github/workflows/quality.yaml
|
||||||
|
|
||||||
|
publish:
|
||||||
|
needs: quality
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: nvim-neorocks/luarocks-tag-release@v7
|
||||||
|
env:
|
||||||
|
LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }}
|
||||||
89
.github/workflows/quality.yaml
vendored
Normal file
89
.github/workflows/quality.yaml
vendored
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
name: quality
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
changes:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
lua: ${{ steps.changes.outputs.lua }}
|
||||||
|
markdown: ${{ steps.changes.outputs.markdown }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dorny/paths-filter@v3
|
||||||
|
id: changes
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
lua:
|
||||||
|
- 'lua/**'
|
||||||
|
- 'plugin/**'
|
||||||
|
- '*.lua'
|
||||||
|
- '.luarc.json'
|
||||||
|
- '*.toml'
|
||||||
|
markdown:
|
||||||
|
- '*.md'
|
||||||
|
|
||||||
|
lua-format:
|
||||||
|
name: Lua Format Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: changes
|
||||||
|
if: ${{ needs.changes.outputs.lua == 'true' }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: JohnnyMorganz/stylua-action@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
version: 2.1.0
|
||||||
|
args: --check .
|
||||||
|
|
||||||
|
lua-lint:
|
||||||
|
name: Lua Lint Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: changes
|
||||||
|
if: ${{ needs.changes.outputs.lua == 'true' }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Lint with Selene
|
||||||
|
uses: NTBBloodbath/selene-action@v1.0.0
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
args: --display-style quiet .
|
||||||
|
|
||||||
|
lua-typecheck:
|
||||||
|
name: Lua Type Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: changes
|
||||||
|
if: ${{ needs.changes.outputs.lua == 'true' }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Run Lua LS Type Check
|
||||||
|
uses: mrcjkb/lua-typecheck-action@v0
|
||||||
|
with:
|
||||||
|
checklevel: Warning
|
||||||
|
directories: lua
|
||||||
|
configpath: .luarc.json
|
||||||
|
|
||||||
|
markdown-format:
|
||||||
|
name: Markdown Format Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: changes
|
||||||
|
if: ${{ needs.changes.outputs.markdown == 'true' }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
- name: Install prettier
|
||||||
|
run: pnpm add -g prettier@3.1.0
|
||||||
|
- name: Check markdown formatting with prettier
|
||||||
|
run: prettier --check .
|
||||||
22
.github/workflows/test.yaml
vendored
Normal file
22
.github/workflows/test.yaml
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
name: test
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
nvim: [stable, nightly]
|
||||||
|
name: Test (Neovim ${{ matrix.nvim }})
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: nvim-neorocks/nvim-busted-action@v1
|
||||||
|
with:
|
||||||
|
nvim_version: ${{ matrix.nvim }}
|
||||||
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
*.log
|
||||||
|
|
||||||
|
.*cache*
|
||||||
|
CLAUDE.md
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
result
|
||||||
|
result-*
|
||||||
|
.direnv/
|
||||||
|
.envrc
|
||||||
8
.luarc.json
Normal file
8
.luarc.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"runtime.version": "Lua 5.1",
|
||||||
|
"runtime.path": ["lua/?.lua", "lua/?/init.lua"],
|
||||||
|
"diagnostics.globals": ["vim", "jit"],
|
||||||
|
"workspace.library": ["$VIMRUNTIME/lua", "${3rd}/luv/library"],
|
||||||
|
"workspace.checkThirdParty": false,
|
||||||
|
"completion.callSnippet": "Replace"
|
||||||
|
}
|
||||||
17
.pre-commit-config.yaml
Normal file
17
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
minimum_pre_commit_version: '3.5.0'
|
||||||
|
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/JohnnyMorganz/StyLua
|
||||||
|
rev: v2.3.1
|
||||||
|
hooks:
|
||||||
|
- id: stylua-github
|
||||||
|
name: stylua (Lua formatter)
|
||||||
|
files: \.lua$
|
||||||
|
pass_filenames: true
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
|
rev: v4.0.0-alpha.8
|
||||||
|
hooks:
|
||||||
|
- id: prettier
|
||||||
|
name: prettier
|
||||||
|
files: \.(md|toml|yaml|yml|sh)$
|
||||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
node_modules/
|
||||||
9
.prettierrc
Normal file
9
.prettierrc
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"proseWrap": "always",
|
||||||
|
"printWidth": 80,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Raphael
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
49
README.md
Normal file
49
README.md
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# blink-cmp-ssh
|
||||||
|
|
||||||
|
SSH configuration completion source for
|
||||||
|
[blink.cmp](https://github.com/saghen/blink.cmp).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Completes `ssh_config` keywords with man page documentation
|
||||||
|
- Provides enum values for keywords with known option sets (ciphers,
|
||||||
|
MACs, key exchange algorithms, etc.)
|
||||||
|
- Keyword and enum data fetched asynchronously at runtime via
|
||||||
|
`man ssh_config` and `ssh -Q`
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Neovim 0.10.0+
|
||||||
|
- [blink.cmp](https://github.com/saghen/blink.cmp)
|
||||||
|
- `ssh` and `man` executables
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Install via
|
||||||
|
[luarocks](https://luarocks.org/modules/barrettruth/blink-cmp-ssh):
|
||||||
|
|
||||||
|
```
|
||||||
|
luarocks install blink-cmp-ssh
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with lazy.nvim:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{
|
||||||
|
'saghen/blink.cmp',
|
||||||
|
dependencies = {
|
||||||
|
'barrettruth/blink-cmp-ssh',
|
||||||
|
},
|
||||||
|
opts = {
|
||||||
|
sources = {
|
||||||
|
default = { 'ssh' },
|
||||||
|
providers = {
|
||||||
|
ssh = {
|
||||||
|
name = 'SSH',
|
||||||
|
module = 'blink-cmp-ssh',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
30
blink-cmp-ssh-scm-1.rockspec
Normal file
30
blink-cmp-ssh-scm-1.rockspec
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
rockspec_format = '3.0'
|
||||||
|
package = 'blink-cmp-ssh'
|
||||||
|
version = 'scm-1'
|
||||||
|
|
||||||
|
source = {
|
||||||
|
url = 'git+https://github.com/barrettruth/blink-cmp-ssh.git',
|
||||||
|
}
|
||||||
|
|
||||||
|
description = {
|
||||||
|
summary = 'SSH configuration completion source for blink.cmp',
|
||||||
|
homepage = 'https://github.com/barrettruth/blink-cmp-ssh',
|
||||||
|
license = 'MIT',
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies = {
|
||||||
|
'lua >= 5.1',
|
||||||
|
}
|
||||||
|
|
||||||
|
test_dependencies = {
|
||||||
|
'nlua',
|
||||||
|
'busted >= 2.1.1',
|
||||||
|
}
|
||||||
|
|
||||||
|
test = {
|
||||||
|
type = 'busted',
|
||||||
|
}
|
||||||
|
|
||||||
|
build = {
|
||||||
|
type = 'builtin',
|
||||||
|
}
|
||||||
43
flake.lock
generated
Normal file
43
flake.lock
generated
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1771207753,
|
||||||
|
"narHash": "sha256-b9uG8yN50DRQ6A7JdZBfzq718ryYrlmGgqkRm9OOwCE=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "d1c15b7d5806069da59e819999d70e1cec0760bf",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"systems": "systems"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
35
flake.nix
Normal file
35
flake.nix
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
description = "blink-cmp-ssh — SSH configuration completion source for blink.cmp";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
|
systems.url = "github:nix-systems/default";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{
|
||||||
|
nixpkgs,
|
||||||
|
systems,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
forEachSystem = f: nixpkgs.lib.genAttrs (import systems) (system: f nixpkgs.legacyPackages.${system});
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells = forEachSystem (pkgs: {
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
packages = [
|
||||||
|
(pkgs.luajit.withPackages (
|
||||||
|
ps: with ps; [
|
||||||
|
busted
|
||||||
|
nlua
|
||||||
|
]
|
||||||
|
))
|
||||||
|
pkgs.prettier
|
||||||
|
pkgs.stylua
|
||||||
|
pkgs.selene
|
||||||
|
];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
281
lua/blink-cmp-ssh.lua
Normal file
281
lua/blink-cmp-ssh.lua
Normal file
|
|
@ -0,0 +1,281 @@
|
||||||
|
---@class blink-cmp-ssh : blink.cmp.Source
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
---@type blink.cmp.CompletionItem[]?
|
||||||
|
local keywords_cache = nil
|
||||||
|
---@type table<string, string[]>?
|
||||||
|
local enums_cache = nil
|
||||||
|
local loading = false
|
||||||
|
---@type {ctx: blink.cmp.Context, callback: fun(response: blink.cmp.CompletionResponse)}[]
|
||||||
|
local pending = {}
|
||||||
|
|
||||||
|
function M.new()
|
||||||
|
return setmetatable({}, { __index = M })
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return boolean
|
||||||
|
function M.enabled()
|
||||||
|
return vim.bo.filetype == 'sshconfig'
|
||||||
|
end
|
||||||
|
|
||||||
|
---@type table<string, string[]>
|
||||||
|
local static_enums = {
|
||||||
|
AddKeysToAgent = { 'yes', 'no', 'ask', 'confirm' },
|
||||||
|
AddressFamily = { 'any', 'inet', 'inet6' },
|
||||||
|
BatchMode = { 'yes', 'no' },
|
||||||
|
CanonicalizeHostname = { 'yes', 'no', 'always' },
|
||||||
|
CanonicalizeFallbackLocal = { 'yes', 'no' },
|
||||||
|
CheckHostIP = { 'yes', 'no' },
|
||||||
|
ClearAllForwardings = { 'yes', 'no' },
|
||||||
|
Compression = { 'yes', 'no' },
|
||||||
|
ControlMaster = { 'yes', 'no', 'ask', 'auto', 'autoask' },
|
||||||
|
EnableEscapeCommandline = { 'yes', 'no' },
|
||||||
|
EnableSSHKeysign = { 'yes', 'no' },
|
||||||
|
ExitOnForwardFailure = { 'yes', 'no' },
|
||||||
|
FingerprintHash = { 'md5', 'sha256' },
|
||||||
|
ForkAfterAuthentication = { 'yes', 'no' },
|
||||||
|
ForwardAgent = { 'yes', 'no' },
|
||||||
|
ForwardX11 = { 'yes', 'no' },
|
||||||
|
ForwardX11Trusted = { 'yes', 'no' },
|
||||||
|
GatewayPorts = { 'yes', 'no' },
|
||||||
|
GSSAPIAuthentication = { 'yes', 'no' },
|
||||||
|
GSSAPIDelegateCredentials = { 'yes', 'no' },
|
||||||
|
HashKnownHosts = { 'yes', 'no' },
|
||||||
|
HostbasedAuthentication = { 'yes', 'no' },
|
||||||
|
IdentitiesOnly = { 'yes', 'no' },
|
||||||
|
KbdInteractiveAuthentication = { 'yes', 'no' },
|
||||||
|
LogLevel = {
|
||||||
|
'QUIET',
|
||||||
|
'FATAL',
|
||||||
|
'ERROR',
|
||||||
|
'INFO',
|
||||||
|
'VERBOSE',
|
||||||
|
'DEBUG',
|
||||||
|
'DEBUG1',
|
||||||
|
'DEBUG2',
|
||||||
|
'DEBUG3',
|
||||||
|
},
|
||||||
|
NoHostAuthenticationForLocalhost = { 'yes', 'no' },
|
||||||
|
PasswordAuthentication = { 'yes', 'no' },
|
||||||
|
PermitLocalCommand = { 'yes', 'no' },
|
||||||
|
PermitRemoteOpen = { 'any', 'none' },
|
||||||
|
ProxyUseFdpass = { 'yes', 'no' },
|
||||||
|
PubkeyAuthentication = { 'yes', 'no', 'unbound', 'host-bound' },
|
||||||
|
RequestTTY = { 'yes', 'no', 'force', 'auto' },
|
||||||
|
SessionType = { 'none', 'subsystem', 'default' },
|
||||||
|
StdinNull = { 'yes', 'no' },
|
||||||
|
StreamLocalBindUnlink = { 'yes', 'no' },
|
||||||
|
StrictHostKeyChecking = { 'yes', 'no', 'ask', 'accept-new', 'off' },
|
||||||
|
TCPKeepAlive = { 'yes', 'no' },
|
||||||
|
Tunnel = { 'yes', 'no', 'point-to-point', 'ethernet' },
|
||||||
|
UpdateHostKeys = { 'yes', 'no', 'ask' },
|
||||||
|
VerifyHostKeyDNS = { 'yes', 'no', 'ask' },
|
||||||
|
VisualHostKey = { 'yes', 'no' },
|
||||||
|
}
|
||||||
|
|
||||||
|
---@type table<string, string[]>
|
||||||
|
local query_to_keywords = {
|
||||||
|
cipher = { 'Ciphers' },
|
||||||
|
['cipher-auth'] = { 'Ciphers' },
|
||||||
|
mac = { 'MACs' },
|
||||||
|
kex = { 'KexAlgorithms' },
|
||||||
|
key = { 'HostKeyAlgorithms', 'PubkeyAcceptedAlgorithms' },
|
||||||
|
['key-sig'] = { 'CASignatureAlgorithms' },
|
||||||
|
}
|
||||||
|
|
||||||
|
---@param stdout string
|
||||||
|
---@return blink.cmp.CompletionItem[]
|
||||||
|
local function parse_keywords(stdout)
|
||||||
|
local Kind = require('blink.cmp.types').CompletionItemKind
|
||||||
|
local lines = {}
|
||||||
|
for line in (stdout .. '\n'):gmatch('(.-)\n') do
|
||||||
|
lines[#lines + 1] = line
|
||||||
|
end
|
||||||
|
|
||||||
|
local defs = {}
|
||||||
|
for i, line in ipairs(lines) do
|
||||||
|
local kw = line:match('^ (%u%a+)%s*$') or line:match('^ (%u%a+) ')
|
||||||
|
if kw then
|
||||||
|
defs[#defs + 1] = { line = i, keyword = kw }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local items = {}
|
||||||
|
for idx, def in ipairs(defs) do
|
||||||
|
local block_end = (defs[idx + 1] and defs[idx + 1].line or #lines) - 1
|
||||||
|
|
||||||
|
local desc_lines = {}
|
||||||
|
for k = def.line + 1, block_end do
|
||||||
|
desc_lines[#desc_lines + 1] = lines[k]
|
||||||
|
end
|
||||||
|
|
||||||
|
local paragraphs = { {} }
|
||||||
|
for _, dl in ipairs(desc_lines) do
|
||||||
|
local stripped = vim.trim(dl)
|
||||||
|
if stripped == '' then
|
||||||
|
if #paragraphs[#paragraphs] > 0 then
|
||||||
|
paragraphs[#paragraphs + 1] = {}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local para = paragraphs[#paragraphs]
|
||||||
|
para[#para + 1] = stripped
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local parts = {}
|
||||||
|
for _, para in ipairs(paragraphs) do
|
||||||
|
if #para > 0 then
|
||||||
|
parts[#parts + 1] = table.concat(para, ' ')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local desc = table.concat(parts, '\n\n')
|
||||||
|
desc = desc:gsub(string.char(0xe2, 0x80, 0x90) .. ' ', '')
|
||||||
|
desc = desc:gsub(' +', ' ')
|
||||||
|
|
||||||
|
items[#items + 1] = {
|
||||||
|
label = def.keyword,
|
||||||
|
kind = Kind.Property,
|
||||||
|
documentation = desc ~= '' and { kind = 'markdown', value = desc } or nil,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return items
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param stdout string
|
||||||
|
---@return table<string, string[]>
|
||||||
|
local function parse_enums(stdout)
|
||||||
|
local enums = {}
|
||||||
|
for k, v in pairs(static_enums) do
|
||||||
|
enums[k:lower()] = v
|
||||||
|
end
|
||||||
|
|
||||||
|
local current_query = nil
|
||||||
|
for line in (stdout .. '\n'):gmatch('(.-)\n') do
|
||||||
|
local query = line:match('^##(.+)')
|
||||||
|
if query then
|
||||||
|
current_query = query
|
||||||
|
elseif current_query and line ~= '' then
|
||||||
|
local keywords = query_to_keywords[current_query]
|
||||||
|
if keywords then
|
||||||
|
for _, kw in ipairs(keywords) do
|
||||||
|
local key = kw:lower()
|
||||||
|
if not enums[key] then
|
||||||
|
enums[key] = {}
|
||||||
|
end
|
||||||
|
local seen = {}
|
||||||
|
for _, existing in ipairs(enums[key]) do
|
||||||
|
seen[existing] = true
|
||||||
|
end
|
||||||
|
if not seen[line] then
|
||||||
|
enums[key][#enums[key] + 1] = line
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return enums
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param ctx blink.cmp.Context
|
||||||
|
---@param callback fun(response: blink.cmp.CompletionResponse)
|
||||||
|
local function respond(ctx, callback)
|
||||||
|
if not keywords_cache or not enums_cache then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local before = ctx.line:sub(1, ctx.cursor[2])
|
||||||
|
|
||||||
|
if before:match('^%s*%a*$') then
|
||||||
|
callback({
|
||||||
|
is_incomplete_forward = false,
|
||||||
|
is_incomplete_backward = false,
|
||||||
|
items = keywords_cache,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local keyword = before:match('^%s*(%S+)')
|
||||||
|
if keyword then
|
||||||
|
local vals = enums_cache[keyword:lower()]
|
||||||
|
if vals then
|
||||||
|
local Kind = require('blink.cmp.types').CompletionItemKind
|
||||||
|
local items = {}
|
||||||
|
for _, v in ipairs(vals) do
|
||||||
|
items[#items + 1] = {
|
||||||
|
label = v,
|
||||||
|
kind = Kind.EnumMember,
|
||||||
|
filterText = v,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
callback({
|
||||||
|
is_incomplete_forward = false,
|
||||||
|
is_incomplete_backward = false,
|
||||||
|
items = items,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
callback({ items = {} })
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param ctx blink.cmp.Context
|
||||||
|
---@param callback fun(response: blink.cmp.CompletionResponse)
|
||||||
|
---@return fun()
|
||||||
|
function M:get_completions(ctx, callback)
|
||||||
|
if keywords_cache then
|
||||||
|
respond(ctx, callback)
|
||||||
|
return function() end
|
||||||
|
end
|
||||||
|
|
||||||
|
pending[#pending + 1] = { ctx = ctx, callback = callback }
|
||||||
|
if not loading then
|
||||||
|
loading = true
|
||||||
|
local man_out, enums_out
|
||||||
|
local remaining = 2
|
||||||
|
|
||||||
|
local function on_all_done()
|
||||||
|
remaining = remaining - 1
|
||||||
|
if remaining > 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
vim.schedule(function()
|
||||||
|
local ok_kw, kw = pcall(parse_keywords, man_out)
|
||||||
|
if not ok_kw then
|
||||||
|
kw = {}
|
||||||
|
end
|
||||||
|
keywords_cache = kw
|
||||||
|
local ok_en, en = pcall(parse_enums, enums_out)
|
||||||
|
if not ok_en then
|
||||||
|
en = {}
|
||||||
|
end
|
||||||
|
enums_cache = en
|
||||||
|
loading = false
|
||||||
|
for _, p in ipairs(pending) do
|
||||||
|
respond(p.ctx, p.callback)
|
||||||
|
end
|
||||||
|
pending = {}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.system(
|
||||||
|
{ 'bash', '-c', 'MANWIDTH=80 man -P cat ssh_config 2>/dev/null' },
|
||||||
|
{},
|
||||||
|
function(result)
|
||||||
|
man_out = result.stdout or ''
|
||||||
|
on_all_done()
|
||||||
|
end
|
||||||
|
)
|
||||||
|
vim.system({
|
||||||
|
'bash',
|
||||||
|
'-c',
|
||||||
|
'for q in cipher cipher-auth mac kex key key-cert key-plain key-sig protocol-version compression sig; do echo "##$q"; ssh -Q "$q" 2>/dev/null; done',
|
||||||
|
}, {}, function(result)
|
||||||
|
enums_out = result.stdout or ''
|
||||||
|
on_all_done()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
return function() end
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
36
lua/blink-cmp-ssh/health.lua
Normal file
36
lua/blink-cmp-ssh/health.lua
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
function M.check()
|
||||||
|
vim.health.start('blink-cmp-ssh')
|
||||||
|
|
||||||
|
local ok = pcall(require, 'blink.cmp')
|
||||||
|
if ok then
|
||||||
|
vim.health.ok('blink.cmp is installed')
|
||||||
|
else
|
||||||
|
vim.health.error('blink.cmp is not installed')
|
||||||
|
end
|
||||||
|
|
||||||
|
local bin = vim.fn.exepath('ssh')
|
||||||
|
if bin ~= '' then
|
||||||
|
vim.health.ok('ssh executable found: ' .. bin)
|
||||||
|
else
|
||||||
|
vim.health.error('ssh executable not found')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local man_bin = vim.fn.exepath('man')
|
||||||
|
if man_bin ~= '' then
|
||||||
|
vim.health.ok('man executable found: ' .. man_bin)
|
||||||
|
else
|
||||||
|
vim.health.warn('man executable not found (keyword descriptions will be unavailable)')
|
||||||
|
end
|
||||||
|
|
||||||
|
local result = vim.system({ 'ssh', '-Q', 'cipher' }):wait()
|
||||||
|
if result.code == 0 and result.stdout and result.stdout ~= '' then
|
||||||
|
vim.health.ok('ssh -Q cipher produces output')
|
||||||
|
else
|
||||||
|
vim.health.warn('ssh -Q cipher failed (enum completions will be unavailable)')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
16
lua/blink-cmp-ssh/types.lua
Normal file
16
lua/blink-cmp-ssh/types.lua
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
---@class blink.cmp.Source
|
||||||
|
|
||||||
|
---@class blink.cmp.CompletionItem
|
||||||
|
---@field label string
|
||||||
|
---@field kind? integer
|
||||||
|
---@field documentation? {kind: string, value: string}
|
||||||
|
---@field filterText? string
|
||||||
|
|
||||||
|
---@class blink.cmp.Context
|
||||||
|
---@field line string
|
||||||
|
---@field cursor integer[]
|
||||||
|
|
||||||
|
---@class blink.cmp.CompletionResponse
|
||||||
|
---@field is_incomplete_forward? boolean
|
||||||
|
---@field is_incomplete_backward? boolean
|
||||||
|
---@field items blink.cmp.CompletionItem[]
|
||||||
1
selene.toml
Normal file
1
selene.toml
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
std = 'vim'
|
||||||
54
spec/helpers.lua
Normal file
54
spec/helpers.lua
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
local plugin_dir = vim.fn.getcwd()
|
||||||
|
vim.opt.runtimepath:prepend(plugin_dir)
|
||||||
|
|
||||||
|
if not package.loaded['blink.cmp.types'] then
|
||||||
|
package.loaded['blink.cmp.types'] = {
|
||||||
|
CompletionItemKind = {
|
||||||
|
Text = 1,
|
||||||
|
Method = 2,
|
||||||
|
Function = 3,
|
||||||
|
Constructor = 4,
|
||||||
|
Field = 5,
|
||||||
|
Variable = 6,
|
||||||
|
Class = 7,
|
||||||
|
Interface = 8,
|
||||||
|
Module = 9,
|
||||||
|
Property = 10,
|
||||||
|
Unit = 11,
|
||||||
|
Value = 12,
|
||||||
|
Enum = 13,
|
||||||
|
Keyword = 14,
|
||||||
|
Snippet = 15,
|
||||||
|
Color = 16,
|
||||||
|
File = 17,
|
||||||
|
Reference = 18,
|
||||||
|
Folder = 19,
|
||||||
|
EnumMember = 20,
|
||||||
|
Constant = 21,
|
||||||
|
Struct = 22,
|
||||||
|
Event = 23,
|
||||||
|
Operator = 24,
|
||||||
|
TypeParameter = 25,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
function M.create_buffer(lines, filetype)
|
||||||
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines or {})
|
||||||
|
if filetype then
|
||||||
|
vim.api.nvim_set_option_value('filetype', filetype, { buf = bufnr })
|
||||||
|
end
|
||||||
|
vim.api.nvim_set_current_buf(bufnr)
|
||||||
|
return bufnr
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.delete_buffer(bufnr)
|
||||||
|
if bufnr and vim.api.nvim_buf_is_valid(bufnr) then
|
||||||
|
vim.api.nvim_buf_delete(bufnr, { force = true })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
34
spec/minimal_init.lua
Normal file
34
spec/minimal_init.lua
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
vim.cmd([[set runtimepath=$VIMRUNTIME]])
|
||||||
|
vim.opt.runtimepath:append('.')
|
||||||
|
vim.opt.packpath = {}
|
||||||
|
vim.opt.loadplugins = false
|
||||||
|
|
||||||
|
package.loaded['blink.cmp.types'] = {
|
||||||
|
CompletionItemKind = {
|
||||||
|
Text = 1,
|
||||||
|
Method = 2,
|
||||||
|
Function = 3,
|
||||||
|
Constructor = 4,
|
||||||
|
Field = 5,
|
||||||
|
Variable = 6,
|
||||||
|
Class = 7,
|
||||||
|
Interface = 8,
|
||||||
|
Module = 9,
|
||||||
|
Property = 10,
|
||||||
|
Unit = 11,
|
||||||
|
Value = 12,
|
||||||
|
Enum = 13,
|
||||||
|
Keyword = 14,
|
||||||
|
Snippet = 15,
|
||||||
|
Color = 16,
|
||||||
|
File = 17,
|
||||||
|
Reference = 18,
|
||||||
|
Folder = 19,
|
||||||
|
EnumMember = 20,
|
||||||
|
Constant = 21,
|
||||||
|
Struct = 22,
|
||||||
|
Event = 23,
|
||||||
|
Operator = 24,
|
||||||
|
TypeParameter = 25,
|
||||||
|
},
|
||||||
|
}
|
||||||
183
spec/ssh_spec.lua
Normal file
183
spec/ssh_spec.lua
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
local helpers = require('spec.helpers')
|
||||||
|
|
||||||
|
local MAN_PAGE = table.concat({
|
||||||
|
'SSH_CONFIG(5) File Formats Manual SSH_CONFIG(5)',
|
||||||
|
'',
|
||||||
|
' The possible keywords and their meanings are as follows:',
|
||||||
|
'',
|
||||||
|
' Host Restricts the following declarations (up to the next Host or',
|
||||||
|
' Match keyword) to be only for those hosts that match one of the',
|
||||||
|
' patterns given after the keyword.',
|
||||||
|
'',
|
||||||
|
' StrictHostKeyChecking',
|
||||||
|
' If this flag is set to yes, ssh(1) will never automatically add',
|
||||||
|
' host keys to the ~/.ssh/known_hosts file, and refuses to connect',
|
||||||
|
' to hosts whose host key has changed.',
|
||||||
|
'',
|
||||||
|
' Hostname',
|
||||||
|
' Specifies the real host name to log into.',
|
||||||
|
'',
|
||||||
|
}, '\n')
|
||||||
|
|
||||||
|
local SSH_Q_OUTPUT = table.concat({
|
||||||
|
'##cipher',
|
||||||
|
'aes128-ctr',
|
||||||
|
'aes256-ctr',
|
||||||
|
'chacha20-poly1305@openssh.com',
|
||||||
|
'##cipher-auth',
|
||||||
|
'chacha20-poly1305@openssh.com',
|
||||||
|
'##mac',
|
||||||
|
'hmac-sha2-256',
|
||||||
|
'##kex',
|
||||||
|
'curve25519-sha256',
|
||||||
|
'##key',
|
||||||
|
'ssh-ed25519',
|
||||||
|
'##key-cert',
|
||||||
|
'##key-plain',
|
||||||
|
'##key-sig',
|
||||||
|
'ssh-ed25519',
|
||||||
|
'##protocol-version',
|
||||||
|
'2',
|
||||||
|
'##compression',
|
||||||
|
'none',
|
||||||
|
'zlib@openssh.com',
|
||||||
|
'##sig',
|
||||||
|
}, '\n')
|
||||||
|
|
||||||
|
local function mock_system()
|
||||||
|
local original_system = vim.system
|
||||||
|
local original_schedule = vim.schedule
|
||||||
|
---@diagnostic disable-next-line: duplicate-set-field
|
||||||
|
vim.system = function(cmd, _, on_exit)
|
||||||
|
local stdout = ''
|
||||||
|
if cmd[1] == 'bash' and cmd[3] and cmd[3]:find('man %-P cat ssh_config') then
|
||||||
|
stdout = MAN_PAGE
|
||||||
|
elseif cmd[1] == 'bash' and cmd[3] and cmd[3]:find('ssh %-Q') then
|
||||||
|
stdout = SSH_Q_OUTPUT
|
||||||
|
end
|
||||||
|
local result = { stdout = stdout, code = 0 }
|
||||||
|
if on_exit then
|
||||||
|
on_exit(result)
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
return {
|
||||||
|
wait = function()
|
||||||
|
return result
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
vim.schedule = function(fn)
|
||||||
|
fn()
|
||||||
|
end
|
||||||
|
return function()
|
||||||
|
vim.system = original_system
|
||||||
|
vim.schedule = original_schedule
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe('blink-cmp-ssh', function()
|
||||||
|
local restores = {}
|
||||||
|
|
||||||
|
before_each(function()
|
||||||
|
package.loaded['blink-cmp-ssh'] = nil
|
||||||
|
end)
|
||||||
|
|
||||||
|
after_each(function()
|
||||||
|
for _, fn in ipairs(restores) do
|
||||||
|
fn()
|
||||||
|
end
|
||||||
|
restores = {}
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('enabled', function()
|
||||||
|
it('returns true for sshconfig filetype', function()
|
||||||
|
local bufnr = helpers.create_buffer({}, 'sshconfig')
|
||||||
|
local source = require('blink-cmp-ssh')
|
||||||
|
assert.is_true(source.enabled())
|
||||||
|
helpers.delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('returns false for other filetypes', function()
|
||||||
|
local bufnr = helpers.create_buffer({}, 'lua')
|
||||||
|
local source = require('blink-cmp-ssh')
|
||||||
|
assert.is_false(source.enabled())
|
||||||
|
helpers.delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('get_completions', function()
|
||||||
|
it('returns keyword items with Property kind on empty line', function()
|
||||||
|
restores[#restores + 1] = mock_system()
|
||||||
|
local source = require('blink-cmp-ssh').new()
|
||||||
|
local items
|
||||||
|
source:get_completions({ line = '', cursor = { 1, 0 } }, function(response)
|
||||||
|
items = response.items
|
||||||
|
end)
|
||||||
|
assert.is_not_nil(items)
|
||||||
|
assert.equals(3, #items)
|
||||||
|
for _, item in ipairs(items) do
|
||||||
|
assert.equals(10, item.kind)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('returns keyword items on partial keyword', function()
|
||||||
|
restores[#restores + 1] = mock_system()
|
||||||
|
local source = require('blink-cmp-ssh').new()
|
||||||
|
local items
|
||||||
|
source:get_completions({ line = 'Str', cursor = { 1, 3 } }, function(response)
|
||||||
|
items = response.items
|
||||||
|
end)
|
||||||
|
assert.is_not_nil(items)
|
||||||
|
assert.equals(3, #items)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('includes man page documentation in items', function()
|
||||||
|
restores[#restores + 1] = mock_system()
|
||||||
|
local source = require('blink-cmp-ssh').new()
|
||||||
|
local items
|
||||||
|
source:get_completions({ line = '', cursor = { 1, 0 } }, function(response)
|
||||||
|
items = response.items
|
||||||
|
end)
|
||||||
|
local strict = vim.iter(items):find(function(item)
|
||||||
|
return item.label == 'StrictHostKeyChecking'
|
||||||
|
end)
|
||||||
|
assert.is_not_nil(strict)
|
||||||
|
assert.is_not_nil(strict.documentation)
|
||||||
|
assert.is_truthy(strict.documentation.value:find('known_hosts'))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('returns enum values after a known keyword', function()
|
||||||
|
restores[#restores + 1] = mock_system()
|
||||||
|
local source = require('blink-cmp-ssh').new()
|
||||||
|
local items
|
||||||
|
source:get_completions(
|
||||||
|
{ line = 'StrictHostKeyChecking ', cursor = { 1, 22 } },
|
||||||
|
function(response)
|
||||||
|
items = response.items
|
||||||
|
end
|
||||||
|
)
|
||||||
|
assert.is_not_nil(items)
|
||||||
|
assert.is_true(#items > 0)
|
||||||
|
for _, item in ipairs(items) do
|
||||||
|
assert.equals(20, item.kind)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('returns empty after a non-enum keyword', function()
|
||||||
|
restores[#restores + 1] = mock_system()
|
||||||
|
local source = require('blink-cmp-ssh').new()
|
||||||
|
local items
|
||||||
|
source:get_completions({ line = 'Hostname ', cursor = { 1, 9 } }, function(response)
|
||||||
|
items = response.items
|
||||||
|
end)
|
||||||
|
assert.equals(0, #items)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('returns a cancel function', function()
|
||||||
|
restores[#restores + 1] = mock_system()
|
||||||
|
local source = require('blink-cmp-ssh').new()
|
||||||
|
local cancel = source:get_completions({ line = '', cursor = { 1, 0 } }, function() end)
|
||||||
|
assert.is_function(cancel)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
8
stylua.toml
Normal file
8
stylua.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
column_width = 100
|
||||||
|
line_endings = "Unix"
|
||||||
|
indent_type = "Spaces"
|
||||||
|
indent_width = 2
|
||||||
|
quote_style = "AutoPreferSingle"
|
||||||
|
call_parentheses = "Always"
|
||||||
|
[sort_requires]
|
||||||
|
enabled = true
|
||||||
33
vim.toml
Normal file
33
vim.toml
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
[selene]
|
||||||
|
base = "lua51"
|
||||||
|
name = "vim"
|
||||||
|
|
||||||
|
[vim]
|
||||||
|
any = true
|
||||||
|
|
||||||
|
[jit]
|
||||||
|
any = true
|
||||||
|
|
||||||
|
[bit]
|
||||||
|
any = true
|
||||||
|
|
||||||
|
[assert]
|
||||||
|
any = true
|
||||||
|
|
||||||
|
[describe]
|
||||||
|
any = true
|
||||||
|
|
||||||
|
[it]
|
||||||
|
any = true
|
||||||
|
|
||||||
|
[before_each]
|
||||||
|
any = true
|
||||||
|
|
||||||
|
[after_each]
|
||||||
|
any = true
|
||||||
|
|
||||||
|
[spy]
|
||||||
|
any = true
|
||||||
|
|
||||||
|
[stub]
|
||||||
|
any = true
|
||||||
Loading…
Add table
Add a link
Reference in a new issue