From 712e74f798b5f4eecd03d9c37b2499567884a0fa Mon Sep 17 00:00:00 2001 From: qvalentin Date: Thu, 4 Jul 2024 18:51:24 +0200 Subject: [PATCH] feat(yamlls): add config to enable yamlls per filetype --- README.md | 56 +++++++++++++------------ internal/adapter/yamlls/completion.go | 2 +- internal/adapter/yamlls/diagnostics.go | 5 +++ internal/adapter/yamlls/documentSync.go | 17 ++++++-- internal/adapter/yamlls/hover.go | 2 +- internal/adapter/yamlls/yamlls.go | 17 ++++++++ internal/adapter/yamlls/yamlls_test.go | 37 ++++++++++++++++ internal/handler/initialization.go | 16 ++++++- internal/lsp/document.go | 1 + internal/lsp/document_store.go | 12 ++++++ internal/lsp/document_store_test.go | 17 ++++++++ internal/util/config.go | 20 +++++---- 12 files changed, 160 insertions(+), 42 deletions(-) create mode 100644 internal/adapter/yamlls/yamlls_test.go create mode 100644 internal/lsp/document_store_test.go diff --git a/README.md b/README.md index 456fc000..4d05d940 100644 --- a/README.md +++ b/README.md @@ -10,37 +10,37 @@ \/ /_/ \___|_|_| |_| |_\____/___/ -## Helm Language Server +## Helm Language Server Helm-ls is a [helm](https://github.com/helm/helm) language server protocol [LSP](https://microsoft.github.io/language-server-protocol/) implementation. -* [Demo](#demo) -* [Getting Started](#getting-started) - * [Installation with a package manager](#installation-with-a-package-manager) - * [Homebrew](#homebrew) - * [Nix](#nix) - * [Arch Linux](#arch-linux) - * [Windows](#windows) - * [mason (neovim)](#mason-neovim) - * [Manual download](#manual-download) - * [Make it executable](#make-it-executable) - * [Integration with yaml-language-server](#integration-with-yaml-language-server) -* [Configuration options](#configuration-options) - * [General](#general) - * [Values Files](#values-files) - * [yaml-language-server config](#yaml-language-server-config) - * [Default Configuration](#default-configuration) -* [Editor Config examples](#editor-config-examples) - * [Neovim](#neovim-using-nvim-lspconfig) - * [Vim Helm Plugin](#vim-helm-plugin) - * [nvim-lspconfig setup](#nvim-lspconfig-setup) - * [coc.nvim setup](#cocnvim-setup) - * [VSCode](#vscode) - * [Emacs eglot setup](#emacs-eglot-setup) -* [Contributing](#contributing) -* [License](#license) +- [Demo](#demo) +- [Getting Started](#getting-started) + - [Installation with a package manager](#installation-with-a-package-manager) + - [Homebrew](#homebrew) + - [Nix](#nix) + - [Arch Linux](#arch-linux) + - [Windows](#windows) + - [mason (neovim)](#mason-neovim) + - [Manual download](#manual-download) + - [Make it executable](#make-it-executable) + - [Integration with yaml-language-server](#integration-with-yaml-language-server) +- [Configuration options](#configuration-options) + - [General](#general) + - [Values Files](#values-files) + - [yaml-language-server config](#yaml-language-server-config) + - [Default Configuration](#default-configuration) +- [Editor Config examples](#editor-config-examples) + - [Neovim](#neovim) + - [Vim Helm Plugin](#vim-helm-plugin) + - [nvim-lspconfig setup](#nvim-lspconfig-setup) + - [coc.nvim setup](#cocnvim-setup) + - [VSCode](#vscode) + - [Emacs eglot setup](#emacs-eglot-setup) +- [Contributing](#contributing) +- [License](#license) @@ -78,7 +78,7 @@ You can install it from the [aur](https://aur.archlinux.org/packages/helm-ls/) u ```bash yay -S helm-ls -# or +# or yay -S helm-ls-bin ``` @@ -157,6 +157,7 @@ You can configure helm-ls with lsp workspace configurations. ### yaml-language-server config - **Enable yaml-language-server**: Toggle support of this feature. +- **EnabledForFilesGlob**: A glob pattern defining for which files yaml-language-server should be enabled. - **Path to yaml-language-server**: Specify the executable location. - **Diagnostics Settings**: @@ -181,6 +182,7 @@ settings = { }, yamlls = { enabled = true, + enabledForFilesGlob = "*.{yaml,yml}", diagnosticsLimit = 50, showDiagnosticsDirectly = false, path = "yaml-language-server", diff --git a/internal/adapter/yamlls/completion.go b/internal/adapter/yamlls/completion.go index b3459259..a95cf497 100644 --- a/internal/adapter/yamlls/completion.go +++ b/internal/adapter/yamlls/completion.go @@ -7,7 +7,7 @@ import ( ) func (yamllsConnector Connector) CallCompletion(ctx context.Context, params *lsp.CompletionParams) (*lsp.CompletionList, error) { - if yamllsConnector.server == nil { + if !yamllsConnector.shouldRun(params.TextDocumentPositionParams.TextDocument.URI) { return &lsp.CompletionList{}, nil } diff --git a/internal/adapter/yamlls/diagnostics.go b/internal/adapter/yamlls/diagnostics.go index b140103d..f7fa48cb 100644 --- a/internal/adapter/yamlls/diagnostics.go +++ b/internal/adapter/yamlls/diagnostics.go @@ -32,23 +32,28 @@ func (c Connector) PublishDiagnostics(ctx context.Context, params *protocol.Publ 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("Checking if diagnostic is relevant", diagnostic.Message) + switch diagnostic.Message { case "Map keys must be unique": return !lsplocal.IsInElseBranch(node) diff --git a/internal/adapter/yamlls/documentSync.go b/internal/adapter/yamlls/documentSync.go index 9c10e054..45b0e6cc 100644 --- a/internal/adapter/yamlls/documentSync.go +++ b/internal/adapter/yamlls/documentSync.go @@ -13,10 +13,17 @@ func (yamllsConnector Connector) InitiallySyncOpenDocuments(docs []*lsplocal.Doc if yamllsConnector.server == nil { return } + for _, doc := range docs { if !doc.IsOpen { continue } + + doc.IsYaml = lsplocal.IsYamlDocument(doc.URI, yamllsConnector.config) + if !yamllsConnector.isRelevantFile(doc.URI) { + continue + } + yamllsConnector.DocumentDidOpen(doc.Ast, lsp.DidOpenTextDocumentParams{ TextDocument: lsp.TextDocumentItem{ URI: doc.URI, @@ -28,7 +35,8 @@ func (yamllsConnector Connector) InitiallySyncOpenDocuments(docs []*lsplocal.Doc func (yamllsConnector Connector) DocumentDidOpen(ast *sitter.Tree, params lsp.DidOpenTextDocumentParams) { logger.Debug("YamllsConnector DocumentDidOpen", params.TextDocument.URI) - if yamllsConnector.server == nil { + + if !yamllsConnector.shouldRun(params.TextDocument.URI) { return } params.TextDocument.Text = lsplocal.TrimTemplate(ast, params.TextDocument.Text) @@ -40,9 +48,10 @@ func (yamllsConnector Connector) DocumentDidOpen(ast *sitter.Tree, params lsp.Di } func (yamllsConnector Connector) DocumentDidSave(doc *lsplocal.Document, params lsp.DidSaveTextDocumentParams) { - if yamllsConnector.server == nil { + if !yamllsConnector.shouldRun(doc.URI) { return } + params.Text = lsplocal.TrimTemplate(doc.Ast, doc.Content) err := yamllsConnector.server.DidSave(context.Background(), ¶ms) @@ -58,7 +67,7 @@ func (yamllsConnector Connector) DocumentDidSave(doc *lsplocal.Document, params } func (yamllsConnector Connector) DocumentDidChange(doc *lsplocal.Document, params lsp.DidChangeTextDocumentParams) { - if yamllsConnector.server == nil { + if !yamllsConnector.shouldRun(doc.URI) { return } trimmedText := lsplocal.TrimTemplate(doc.Ast, doc.Content) @@ -87,7 +96,7 @@ func (yamllsConnector Connector) DocumentDidChange(doc *lsplocal.Document, param } func (yamllsConnector Connector) DocumentDidChangeFullSync(doc *lsplocal.Document, params lsp.DidChangeTextDocumentParams) { - if yamllsConnector.server == nil { + if !yamllsConnector.shouldRun(doc.URI) { return } diff --git a/internal/adapter/yamlls/hover.go b/internal/adapter/yamlls/hover.go index b6f85df2..df19e200 100644 --- a/internal/adapter/yamlls/hover.go +++ b/internal/adapter/yamlls/hover.go @@ -13,7 +13,7 @@ import ( // Yamlls can not handle hover if the schema validation returns error, // thats why we fall back to calling completion func (yamllsConnector Connector) CallHover(ctx context.Context, params lsp.HoverParams, word string) (*lsp.Hover, error) { - if yamllsConnector.server == nil { + if !yamllsConnector.shouldRun(params.TextDocumentPositionParams.TextDocument.URI) { return &lsp.Hover{}, nil } diff --git a/internal/adapter/yamlls/yamlls.go b/internal/adapter/yamlls/yamlls.go index 9349f0b2..2d07a068 100644 --- a/internal/adapter/yamlls/yamlls.go +++ b/internal/adapter/yamlls/yamlls.go @@ -11,6 +11,7 @@ import ( "github.com/mrjosh/helm-ls/internal/util" "go.lsp.dev/jsonrpc2" "go.lsp.dev/protocol" + lsp "go.lsp.dev/protocol" "go.uber.org/zap" ) @@ -75,3 +76,19 @@ func NewConnector(ctx context.Context, yamllsConfiguration util.YamllsConfigurat yamllsConnector.server = server return &yamllsConnector } + +func (yamllsConnector *Connector) isRelevantFile(uri lsp.URI) bool { + doc, ok := yamllsConnector.documents.Get(uri) + if !ok { + logger.Error("Could not find document", uri) + return true + } + return doc.IsYaml +} + +func (yamllsConnector *Connector) shouldRun(uri lsp.DocumentURI) bool { + if yamllsConnector.server == nil { + return false + } + return yamllsConnector.isRelevantFile(uri) +} diff --git a/internal/adapter/yamlls/yamlls_test.go b/internal/adapter/yamlls/yamlls_test.go new file mode 100644 index 00000000..af0ae636 --- /dev/null +++ b/internal/adapter/yamlls/yamlls_test.go @@ -0,0 +1,37 @@ +package yamlls + +import ( + "testing" + + lsplocal "github.com/mrjosh/helm-ls/internal/lsp" + "github.com/mrjosh/helm-ls/internal/util" + "github.com/stretchr/testify/assert" + "go.lsp.dev/uri" +) + +func TestIsRelevantFile(t *testing.T) { + connector := Connector{ + config: util.YamllsConfiguration{ + Enabled: true, + }, + } + + connector.documents = &lsplocal.DocumentStore{} + yamlFile := uri.File("../../../testdata/example/templates/deployment.yaml") + nonYamlFile := uri.File("../../../testdata/example/templates/_helpers.tpl") + connector.documents.Store(yamlFile, util.DefaultConfig) + connector.documents.Store(nonYamlFile, util.DefaultConfig) + + assert.True(t, connector.isRelevantFile(yamlFile)) + assert.False(t, connector.isRelevantFile(nonYamlFile)) +} + +func TestShouldRun(t *testing.T) { + connector := Connector{ + config: util.YamllsConfiguration{ + Enabled: true, + }, + } + assert.False(t, connector.shouldRun(uri.File("test.yaml"))) + assert.False(t, connector.shouldRun(uri.File("_helpers.tpl"))) +} diff --git a/internal/handler/initialization.go b/internal/handler/initialization.go index 40f3923d..1828e676 100644 --- a/internal/handler/initialization.go +++ b/internal/handler/initialization.go @@ -4,6 +4,7 @@ import ( "context" "os" + "github.com/gobwas/glob" "github.com/mrjosh/helm-ls/internal/adapter/yamlls" "github.com/mrjosh/helm-ls/internal/charts" "github.com/mrjosh/helm-ls/internal/util" @@ -58,17 +59,28 @@ func (h *langHandler) Initialized(ctx context.Context, _ *lsp.InitializedParams) func (h *langHandler) initializationWithConfig(ctx context.Context) { configureLogLevel(h.helmlsConfig) h.chartStore.SetValuesFilesConfig(h.helmlsConfig.ValuesFilesConfig) - configureYamlls(ctx, h) + h.configureYamlls(ctx) } -func configureYamlls(ctx context.Context, h *langHandler) { +func (h *langHandler) configureYamlsEnabledGlob() { + globObject, err := glob.Compile(h.helmlsConfig.YamllsConfiguration.EnabledForFilesGlob) + if err != nil { + logger.Error("Error compiling glob for yamlls EnabledForFilesGlob", err) + globObject = util.DefaultConfig.YamllsConfiguration.EnabledForFilesGlobObject + } + h.helmlsConfig.YamllsConfiguration.EnabledForFilesGlobObject = globObject +} + +func (h *langHandler) configureYamlls(ctx context.Context) { config := h.helmlsConfig if config.YamllsConfiguration.Enabled { + h.configureYamlsEnabledGlob() h.yamllsConnector = yamlls.NewConnector(ctx, config.YamllsConfiguration, h.client, h.documents) err := h.yamllsConnector.CallInitialize(ctx, h.chartStore.RootURI) if err != nil { logger.Error("Error initializing yamlls", err) } + h.yamllsConnector.InitiallySyncOpenDocuments(h.documents.GetAllDocs()) } } diff --git a/internal/lsp/document.go b/internal/lsp/document.go index 70265950..f20ffbf3 100644 --- a/internal/lsp/document.go +++ b/internal/lsp/document.go @@ -21,6 +21,7 @@ type Document struct { DiagnosticsCache DiagnosticsCache IsOpen bool SymbolTable *SymbolTable + IsYaml bool } // ApplyChanges updates the content of the document from LSP textDocument/didChange events. diff --git a/internal/lsp/document_store.go b/internal/lsp/document_store.go index d7c8428d..46432e77 100644 --- a/internal/lsp/document_store.go +++ b/internal/lsp/document_store.go @@ -44,6 +44,7 @@ func (s *DocumentStore) DidOpen(params *lsp.DidOpenTextDocumentParams, helmlsCon DiagnosticsCache: NewDiagnosticsCache(helmlsConfig), IsOpen: true, SymbolTable: NewSymbolTable(ast, []byte(params.TextDocument.Text)), + IsYaml: IsYamlDocument(uri, helmlsConfig.YamllsConfiguration), } logger.Debug("Storing doc ", path) s.documents.Store(path, doc) @@ -51,6 +52,11 @@ func (s *DocumentStore) DidOpen(params *lsp.DidOpenTextDocumentParams, helmlsCon } func (s *DocumentStore) Store(uri uri.URI, helmlsConfig util.HelmlsConfiguration) error { + _, ok := s.documents.Load(uri.Filename()) + if ok { + return nil + } + content, err := os.ReadFile(uri.Filename()) if err != nil { logger.Error("Could not open file ", uri.Filename(), " ", err) @@ -67,6 +73,7 @@ func (s *DocumentStore) Store(uri uri.URI, helmlsConfig util.HelmlsConfiguration DiagnosticsCache: NewDiagnosticsCache(helmlsConfig), IsOpen: false, SymbolTable: NewSymbolTable(ast, content), + IsYaml: IsYamlDocument(uri, helmlsConfig.YamllsConfiguration), }, ) return nil @@ -75,8 +82,13 @@ func (s *DocumentStore) Store(uri uri.URI, helmlsConfig util.HelmlsConfiguration func (s *DocumentStore) Get(docuri uri.URI) (*Document, bool) { path := docuri.Filename() d, ok := s.documents.Load(path) + if !ok { return nil, false } return d.(*Document), ok } + +func IsYamlDocument(uri lsp.URI, yamllsConfiguration util.YamllsConfiguration) bool { + return yamllsConfiguration.EnabledForFilesGlobObject.Match(uri.Filename()) +} diff --git a/internal/lsp/document_store_test.go b/internal/lsp/document_store_test.go new file mode 100644 index 00000000..31ad645b --- /dev/null +++ b/internal/lsp/document_store_test.go @@ -0,0 +1,17 @@ +package lsp + +import ( + "testing" + + "github.com/mrjosh/helm-ls/internal/util" + "github.com/stretchr/testify/assert" + "go.lsp.dev/uri" +) + +func TestIsYamlDocument(t *testing.T) { + assert := assert.New(t) + assert.True(IsYamlDocument(uri.File("test.yaml"), util.DefaultConfig.YamllsConfiguration)) + assert.False(IsYamlDocument(uri.File("test.tpl"), util.DefaultConfig.YamllsConfiguration)) + assert.True(IsYamlDocument(uri.File("../../testdata/example/templates/hpa.yaml"), util.DefaultConfig.YamllsConfiguration)) + assert.False(IsYamlDocument(uri.File("../../testdata/example/templates/_helpers.tpl"), util.DefaultConfig.YamllsConfiguration)) +} diff --git a/internal/util/config.go b/internal/util/config.go index 5c65ff98..18fb0c6e 100644 --- a/internal/util/config.go +++ b/internal/util/config.go @@ -1,5 +1,7 @@ package util +import "github.com/gobwas/glob" + type HelmlsConfiguration struct { YamllsConfiguration YamllsConfiguration `json:"yamlls,omitempty"` ValuesFilesConfig ValuesFilesConfig `json:"valuesFiles,omitempty"` @@ -13,8 +15,10 @@ type ValuesFilesConfig struct { } type YamllsConfiguration struct { - Enabled bool `json:"enabled,omitempty"` - Path string `json:"path,omitempty"` + Enabled bool `json:"enabled,omitempty"` + EnabledForFilesGlob string `json:"enabledForFilesGlob,omitempty"` + EnabledForFilesGlobObject glob.Glob + Path string `json:"path,omitempty"` // max diagnostics from yamlls that are shown for a single file DiagnosticsLimit int `json:"diagnosticsLimit,omitempty"` // if set to false diagnostics will only be shown after saving the file @@ -32,11 +36,13 @@ var DefaultConfig = HelmlsConfiguration{ AdditionalValuesFilesGlobPattern: "values*.yaml", }, YamllsConfiguration: YamllsConfiguration{ - Enabled: true, - Path: "yaml-language-server", - DiagnosticsLimit: 50, - ShowDiagnosticsDirectly: false, - YamllsSettings: DefaultYamllsSettings, + Enabled: true, + EnabledForFilesGlob: "*.{yaml,yml}", + EnabledForFilesGlobObject: glob.MustCompile("*.{yaml,yml}"), + Path: "yaml-language-server", + DiagnosticsLimit: 50, + ShowDiagnosticsDirectly: false, + YamllsSettings: DefaultYamllsSettings, }, }