Skip to content

Commit

Permalink
Merge pull request #32 from qvalentin/feat-yamlls
Browse files Browse the repository at this point in the history
feat(yamlls): initial support of yamlls integration
  • Loading branch information
qvalentin authored Dec 22, 2023
2 parents 750b11e + ab71fac commit 14a9370
Show file tree
Hide file tree
Showing 39 changed files with 19,943 additions and 19,015 deletions.
142 changes: 113 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,29 @@
</pre>

## Helm Language Server Protocol
helm-ls is [helm](https://github.com/helm/helm) language server protocol [LSP](https://microsoft.github.io/language-server-protocol/).
Helm-ls is a [helm](https://github.com/helm/helm) language server protocol [LSP](https://microsoft.github.io/language-server-protocol/) implementation.


<!-- vim-markdown-toc GFM -->

* [Demo](#demo)
* [Getting Started](#getting-started)
* [Download](#download)
* [Make it executable](#make-it-executable)
* [Integration with yaml-language-server](#integration-with-yaml-language-server)
* [Configuration options](#configuration-options)
* [LSP Server](#lsp-server)
* [yaml-language-server config](#yaml-language-server-config)
* [Emacs eglot setup](#emacs-eglot-setup)
* [Contributing](#contributing)
* [License](#license)

<!-- vim-markdown-toc -->

## Demo
[![asciicast](https://asciinema.org/a/485522.svg)](https://asciinema.org/a/485522)

## Getting Started
### Vim Helm Plugin
You'll need [vim-helm](https://github.com/towolf/vim-helm) plugin installed before using helm_ls, Try to install it vim:
```lua
Plug 'towolf/vim-helm'
```

### Download
* Download the latest helm_ls executable file from [here](https://github.com/mrjosh/helm-ls/releases/latest) and move it to your binaries directory
Expand All @@ -28,38 +43,107 @@ Plug 'towolf/vim-helm'
curl -L https://github.com/mrjosh/helm-ls/releases/download/master/helm_ls_{os}_{arch} --output /usr/local/bin/helm_ls
```

If you are using neovim with [mason](https://github.com/williamboman/mason.nvim) you can also install it with mason.

### Make it executable
```bash
chmod +x /usr/local/bin/helm_ls
```

## nvim-lspconfig setup
```lua
local configs = require('lspconfig.configs')
local lspconfig = require('lspconfig')
local util = require('lspconfig.util')

if not configs.helm_ls then
configs.helm_ls = {
default_config = {
cmd = {"helm_ls", "serve"},
filetypes = {'helm'},
root_dir = function(fname)
return util.root_pattern('Chart.yaml')(fname)
end,
},
}
end
### Integration with [yaml-language-server](https://github.com/redhat-developer/yaml-language-server)
Helm-ls will use yaml-language-server to provide additional capabilities, if it is installed.
This feature is expermiental, you can disable it in the config ([see](#configuration-options)).
Having a broken template syntax (e.g. while your are stil typing) will cause diagnostics from yaml-language-server to be shown as errors.

lspconfig.helm_ls.setup {
filetypes = {"helm"},
cmd = {"helm_ls", "serve"},
}
To install it using npm run (or use your preferred package manager):
```bash
npm install --global yaml-language-server
```

[![asciicast](https://asciinema.org/a/485522.svg)](https://asciinema.org/a/485522)
The default kubernetes schema of yaml-language-server will be used for all files. You can overwrite which schema to use in the config ([see](#configuration-options)).
If you are for example using CRDs that are not included in the default schema, you can overwrite the schema using a comment
to use the schemas from the [CRDs-catalog](https://github.com/datreeio/CRDs-catalog).

```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/keda.sh/scaledobject_v1alpha1.json
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
...
```

## Configuration options

You can configure helm-ls with lsp workspace configurations.

### LSP Server

- **Log Level**: Adjust log verbosity.

### yaml-language-server config

- **Enable yaml-language-server**: Toggle support of this feature.
- **Path to yaml-language-server**: Specify the executable location.
- **Diagnostics Settings**:
- **Limit**: Number of displayed diagnostics per file.
- **Show Directly**: Show diagnostics while typing.

- **Additional Settings** (see [yaml-language-server](https://github.com/redhat-developer/yaml-language-server#language-server-settings)):
- **Schemas**: Define YAML schemas.
- **Completion**: Enable code completion.
- **Hover Information**: Enable hover details.

### Default Configuration

```lua
settings = {
['helm-ls'] = {
logLevel = "debug",
yamlls = {
enabled = true,
diagnosticsLimit = 50,
showDiagnosticsDirectly = false,
path = "yaml-language-server",
config = {
schemas = {
kubernetes = "**",
},
completion = true,
hover = true,
-- any other config: https://github.com/redhat-developer/yaml-language-server#language-server-settings
}
}
}
}
```

## Editor Config examples

### Neovim (using nvim-lspconfig)
#### Vim Helm Plugin
You'll need [vim-helm](https://github.com/towolf/vim-helm) plugin installed before using helm_ls, to install it using vim-plug (or use your preferred plugin manager):
```lua
Plug 'towolf/vim-helm'
```

#### Setup laguage server
```lua
local lspconfig = require('lspconfig')

lspconfig.helm_ls.setup {
settings = {
['helm-ls'] = {
yamlls = {
path = "yaml-language-server",
}
}
}
}
```
See [examples/nvim/init.lua](https://github.com/mrjosh/helm-ls/blob/master/examples/nvim/init.lua) for an
complete example, which also includes yaml-language-server.


## Emacs eglot setup
### Emacs eglot setup

Integrating helm-ls with [eglot](https://github.com/joaotavora/eglot) for emacs consists of two steps: wiring up Helm template files into a specific major mode and then associating that major mode with `helm_ls` via the `eglot-server-programs` variable.
The first step is necessary because without a Helm-specific major mode, using an existing major mode like `yaml-mode` for `helm_ls` in `eglot-server-programs` may invoke the language server for other, non-Helm yaml files.
Expand Down
4 changes: 4 additions & 0 deletions cmds/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ package cmds
import (
"fmt"

"github.com/mrjosh/helm-ls/internal/log"
"github.com/spf13/cobra"
)

var logger = log.GetLogger()

func newVersionCmd() *cobra.Command {
return &cobra.Command{
Use: "version",
Expand All @@ -20,6 +23,7 @@ func newVersionCmd() *cobra.Command {
fmt.Sprintf("Golang: %s", versionInfo.GoVersion),
fmt.Sprintf("Compiled by: %s", versionInfo.CompiledBy),
)
logger.Debug("Additional debug info")
},
}
}
41 changes: 41 additions & 0 deletions examples/nvim/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
-- a minimal example config for setting up neovim with helm-ls and yamlls

-- setup lazy plugin manager
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
vim.fn.system({
"git",
"clone",
"--filter=blob:none",
"https://github.com/folke/lazy.nvim.git",
"--branch=stable", -- latest stable release
lazypath,
})
end
vim.opt.rtp:prepend(lazypath)
vim.g.mapleader = " "

require("lazy").setup({
-- towolf/vim-helm provides basic syntax highlighting and filetype detection
-- ft = 'helm' is important to not start yamlls
{ 'towolf/vim-helm', ft = 'helm' },

{ "neovim/nvim-lspconfig", event = { "BufReadPre", "BufNewFile", "BufEnter" } }
})


local lspconfig = require('lspconfig')

-- setup helm-ls
lspconfig.helm_ls.setup {
settings = {
['helm-ls'] = {
yamlls = {
path = "yaml-language-server",
}
}
}
}

-- setup yamlls
lspconfig.yamlls.setup {}
25 changes: 25 additions & 0 deletions internal/adapter/yamlls/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package yamlls

import (
"context"
"reflect"

lsp "go.lsp.dev/protocol"
)

func (yamllsConnector Connector) CallCompletion(params lsp.CompletionParams) *lsp.CompletionList {
if yamllsConnector.Conn == nil {
return &lsp.CompletionList{}
}

logger.Println("Calling yamlls for completions")
var response = reflect.New(reflect.TypeOf(lsp.CompletionList{})).Interface()
_, err := (*yamllsConnector.Conn).Call(context.Background(), lsp.MethodTextDocumentCompletion, params, response)
if err != nil {
logger.Error("Error Calling yamlls for completions", err)
return &lsp.CompletionList{}
}

logger.Debug("Got completions from yamlls", response)
return response.(*lsp.CompletionList)
}
18 changes: 18 additions & 0 deletions internal/adapter/yamlls/configuration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package yamlls

import (
"encoding/json"

"go.lsp.dev/jsonrpc2"
lsp "go.lsp.dev/protocol"
)

func (yamllsConnector Connector) handleConfiguration(req jsonrpc2.Request) []interface{} {
var params lsp.ConfigurationParams
if err := json.Unmarshal(req.Params(), &params); err != nil {
logger.Error("Error parsing configuration request from yamlls", err)
}
logger.Debug("Yamlls ConfigurationParams", params)
settings := []interface{}{yamllsConnector.config.YamllsSettings}
return settings
}
68 changes: 68 additions & 0 deletions internal/adapter/yamlls/diagnostics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package yamlls

import (
"context"
"encoding/json"

lsplocal "github.com/mrjosh/helm-ls/internal/lsp"
sitter "github.com/smacker/go-tree-sitter"
"go.lsp.dev/jsonrpc2"
lsp "go.lsp.dev/protocol"
)

func (yamllsConnector *Connector) handleDiagnostics(req jsonrpc2.Request, clientConn jsonrpc2.Conn, documents *lsplocal.DocumentStore) {
var params lsp.PublishDiagnosticsParams
if err := json.Unmarshal(req.Params(), &params); err != nil {
logger.Println("Error handling diagnostic", err)
}

doc, ok := documents.Get(params.URI)
if !ok {
logger.Println("Error handling diagnostic. Could not get document: " + params.URI.Filename())
}

doc.DiagnosticsCache.SetYamlDiagnostics(filterDiagnostics(params.Diagnostics, doc.Ast, doc.Content))
if doc.DiagnosticsCache.ShouldShowDiagnosticsOnNewYamlDiagnostics() {
logger.Debug("Publishing yamlls diagnostics")
params.Diagnostics = doc.DiagnosticsCache.GetMergedDiagnostics()
err := clientConn.Notify(context.Background(), lsp.MethodTextDocumentPublishDiagnostics, &params)
if err != nil {
logger.Println("Error calling yamlls for diagnostics", err)
}
}
}

func filterDiagnostics(diagnostics []lsp.Diagnostic, ast *sitter.Tree, content string) (filtered []lsp.Diagnostic) {
filtered = []lsp.Diagnostic{}
for _, diagnostic := range diagnostics {
node := lsplocal.NodeAtPosition(ast, diagnostic.Range.Start)
childNode := lsplocal.FindRelevantChildNode(ast.RootNode(), lsplocal.GetSitterPointForLspPos(diagnostic.Range.Start))
if node.Type() == "text" && childNode.Type() == "text" {
logger.Debug("Diagnostic", diagnostic)
logger.Debug("Node", node.Content([]byte(content)))
if diagnisticIsRelevant(diagnostic, childNode) {
diagnostic.Message = "Yamlls: " + diagnostic.Message
filtered = append(filtered, diagnostic)
}
}
}
return filtered
}

func diagnisticIsRelevant(diagnostic lsp.Diagnostic, node *sitter.Node) bool {
logger.Debug("Diagnostic", diagnostic.Message)
switch diagnostic.Message {
case "Map keys must be unique":
return !lsplocal.IsInElseBranch(node)
case "All mapping items must start at the same column",
"Implicit map keys need to be followed by map values",
"Implicit keys need to be on a single line",
"A block sequence may not be used as an implicit map key":
// TODO: could add a check if is is caused by includes
return false

default:
return true
}

}
Loading

0 comments on commit 14a9370

Please sign in to comment.