diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..83e55fdc --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "testdata/charts"] + path = testdata/charts + url = https://github.com/qvalentin/charts.git diff --git a/Makefile b/Makefile index 2f3f03fa..2d537ee5 100644 --- a/Makefile +++ b/Makefile @@ -55,7 +55,16 @@ install-metalinter: @$(GO) get -v github.com/golangci/golangci-lint/cmd/golangci-lint@v1.53.2 @$(GO) install -v github.com/golangci/golangci-lint/cmd/golangci-lint@v1.53.2 +install-yamlls: + npm install --global yaml-language-server + +integration-test-deps: + @YAMLLS_BIN=$$(command -v yaml-language-server) || { echo "yaml-language-server command not found! Installing..." && $(MAKE) install-yamlls; }; + git submodule init + git submodule update --depth 1 + test: + $(MAKE) integration-test-deps @$(GO) test ./... -v -race coverage: diff --git a/internal/adapter/yamlls/diagnostics.go b/internal/adapter/yamlls/diagnostics.go index 9e2d905f..40b32782 100644 --- a/internal/adapter/yamlls/diagnostics.go +++ b/internal/adapter/yamlls/diagnostics.go @@ -54,20 +54,29 @@ func diagnisticIsRelevant(diagnostic lsp.Diagnostic, node *sitter.Node) bool { 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 - // case "Block scalars with more-indented leading empty lines must use an explicit indentation indicator": - // return false - // // TODO: check if this is a false positive, probably requires parsing the yaml with tree-sitter injections - // // smtp-password: | - // // {{- if not .Values.existingSecret }} - // // test: dsa - // // {{- end }} - // + case "All mapping items must start at the same column": + // unknown what exactly this is, only causes one error in bitnami/charts + return false + case "Implicit map keys need to be followed by map values": + // still breaks with + // params: + // {{- range $key, $value := .params }} + // {{ $key }}: + // {{- range $value }} + // - {{ . | quote }} + // {{- end }} + // {{- end }} + return false && !lsplocal.IsInElseBranch(node) + case "Implicit keys need to be on a single line": + // beaks for same as above + return false + case "Block scalars with more-indented leading empty lines must use an explicit indentation indicator": + // TODO: check if this is a false positive, probably requires parsing the yaml with tree-sitter injections + // smtp-password: | + // {{- if not .Values.existingSecret }} + // test: dsa + // {{- end }} + return false default: return true } diff --git a/internal/adapter/yamlls/diagnostics_integration_test.go b/internal/adapter/yamlls/diagnostics_integration_test.go index b430ff09..3dae8867 100644 --- a/internal/adapter/yamlls/diagnostics_integration_test.go +++ b/internal/adapter/yamlls/diagnostics_integration_test.go @@ -2,30 +2,48 @@ package yamlls import ( "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + "regexp" + "strings" "testing" "time" lsplocal "github.com/mrjosh/helm-ls/internal/lsp" "github.com/mrjosh/helm-ls/internal/util" + "github.com/stretchr/testify/assert" "go.lsp.dev/jsonrpc2" lsp "go.lsp.dev/protocol" "go.lsp.dev/uri" ) -type readWriteCloseMock struct{} +// must be relative to this file +var TEST_DATA_DIR = "../../../testdata/charts/bitnami/" + +type jsonRpcDiagnostics struct { + Params lsp.PublishDiagnosticsParams `json:"params"` + Jsonrpc string `json:"jsonrpc"` + Method string `json:"method"` +} + +type readWriteCloseMock struct { + diagnosticsChan chan lsp.PublishDiagnosticsParams +} func (proc readWriteCloseMock) Read(p []byte) (int, error) { return 1, nil } func (proc readWriteCloseMock) Write(p []byte) (int, error) { - // TODO: remove the Content-Length header - var params lsp.PublishDiagnosticsParams - if err := json.Unmarshal(p, ¶ms); err != nil { - logger.Println("Error handling diagnostic", err) + if strings.HasPrefix(string(p), "Content-Length: ") { + return 1, nil } + var diagnostics jsonRpcDiagnostics + json.NewDecoder(strings.NewReader(string(p))).Decode(&diagnostics) - logger.Printf("Write: %s", params) + proc.diagnosticsChan <- diagnostics.Params return 1, nil } @@ -33,47 +51,110 @@ func (proc readWriteCloseMock) Close() error { return nil } +func readTestFiles(dir string, channel chan<- lsp.DidOpenTextDocumentParams, doneChan chan<- int) { + libRegEx, e := regexp.Compile(".*/templates/.*\\.ya?ml") + if e != nil { + log.Fatal(e) + return + } + if _, err := os.Stat(dir); os.IsNotExist(err) { + log.Fatal(err) + return + } + + count := 0 + filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { + if d.Type().IsRegular() && libRegEx.MatchString(path) { + contentBytes, _ := os.ReadFile(path) + count++ + channel <- lsp.DidOpenTextDocumentParams{ + TextDocument: lsp.TextDocumentItem{ + URI: uri.File(path), + LanguageID: "", + Version: 0, + Text: string(contentBytes), + }, + } + } + return nil + }) + doneChan <- count +} + +func sendTestFilesToYamlls(documents *lsplocal.DocumentStore, yamllsConnector *Connector, + doneReadingFilesChan <-chan int, + doneSendingFilesChan chan<- int, + filesChan <-chan lsp.DidOpenTextDocumentParams, +) { + ownCount := 0 + for { + select { + case d := <-filesChan: + documents.DidOpen(d, util.DefaultConfig) + tree := lsplocal.ParseAst(nil, d.TextDocument.Text) + yamllsConnector.DocumentDidOpen(tree, d) + ownCount++ + case count := <-doneReadingFilesChan: + if count != ownCount { + log.Fatal("Count mismatch: ", count, " != ", ownCount) + } + doneSendingFilesChan <- count + return + } + } +} + func TestYamllsDiagnosticsIntegration(t *testing.T) { + diagnosticsChan := make(chan lsp.PublishDiagnosticsParams) + doneReadingFilesChan := make(chan int) + doneSendingFilesChan := make(chan int) + dir := t.TempDir() documents := lsplocal.NewDocumentStore() - con := jsonrpc2.NewConn(jsonrpc2.NewStream(readWriteCloseMock{})) + con := jsonrpc2.NewConn(jsonrpc2.NewStream(readWriteCloseMock{diagnosticsChan})) config := util.DefaultConfig.YamllsConfiguration - config.Path = "yamlls-debug.sh" + + yamllsSettings := util.DefaultYamllsSettings + // disabling yamlls schema store improves performance and + // removes all schema diagnostics that are not caused by the yaml trimming + yamllsSettings.Schemas = make(map[string]string) + yamllsSettings.YamllsSchemaStoreSettings = util.YamllsSchemaStoreSettings{ + Enable: false, + } + config.YamllsSettings = yamllsSettings yamllsConnector := NewConnector(config, con, documents) yamllsConnector.CallInitialize(uri.File(dir)) - content := ` -{{- if .Values.serviceAccount.create }} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "whereabouts.serviceAccountName" . }} - namespace: {{ include "common.names.namespace" . | quote }} - labels: {{- include "common.labels.standard" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} - {{- if or .Values.serviceAccount.annotations .Values.commonAnnotations }} - {{- $annotations := include "common.tplvalues.merge" ( dict "values" ( list .Values.serviceAccount.annotations .Values.commonAnnotations ) "context" . ) }} - annotations: {{- include "common.tplvalues.render" ( dict "value" $annotations "context" $) | nindent 4 }} - {{- end }} -automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} -{{- end }} - ` - - tree := lsplocal.ParseAst(nil, content) - - didOpenParams := lsp.DidOpenTextDocumentParams{ - TextDocument: lsp.TextDocumentItem{ - URI: uri.File(dir + "/test.yaml"), - LanguageID: "", - Version: 0, - Text: content, - }, - } - documents.DidOpen(didOpenParams, util.DefaultConfig) - yamllsConnector.DocumentDidOpen(tree, didOpenParams) + didOpenChan := make(chan lsp.DidOpenTextDocumentParams) + go readTestFiles(TEST_DATA_DIR, didOpenChan, doneReadingFilesChan) + go sendTestFilesToYamlls(documents, + yamllsConnector, doneReadingFilesChan, doneSendingFilesChan, didOpenChan) - logger.Printf("Running tests in %s", dir) + sentCount, diagnosticsCount := 0, 0 + receivedDiagnostics := make(map[uri.URI]lsp.PublishDiagnosticsParams) + + afterCh := time.After(600 * time.Second) + for { + if sentCount != 0 && len(receivedDiagnostics) == sentCount { + fmt.Println("All files checked") + break + } + select { + case d := <-diagnosticsChan: + receivedDiagnostics[d.URI] = d + if len(d.Diagnostics) > 0 { + diagnosticsCount++ + fmt.Printf("Got diagnostic in %s diagnostics: %v \n", d.URI.Filename(), d.Diagnostics) + } + case <-afterCh: + t.Fatal("Timed out waiting for diagnostics") + case count := <-doneSendingFilesChan: + sentCount = count + } + } - // sleep 5 seconds - time.Sleep(5 * time.Second) + fmt.Printf("Checked %d files, found %d diagnostics\n", sentCount, diagnosticsCount) + assert.Equal(t, diagnosticsCount, 22) + assert.Equal(t, 2368, sentCount, "Count of files in test data not equal to actual count") } diff --git a/internal/adapter/yamlls/documentSync.go b/internal/adapter/yamlls/documentSync.go index c32f3756..167d0f6c 100644 --- a/internal/adapter/yamlls/documentSync.go +++ b/internal/adapter/yamlls/documentSync.go @@ -24,11 +24,11 @@ func (yamllsConnector Connector) InitiallySyncOpenDocuments(docs []*lsplocal.Doc } func (yamllsConnector Connector) DocumentDidOpen(ast *sitter.Tree, params lsp.DidOpenTextDocumentParams) { - logger.Println("YamllsConnector DocumentDidOpen", params.TextDocument.URI) + logger.Debug("YamllsConnector DocumentDidOpen", params.TextDocument.URI) if yamllsConnector.Conn == nil { return } - params.TextDocument.Text = trimTemplateForYamllsFromAst(ast, params.TextDocument.Text) + params.TextDocument.Text = lsplocal.TrimTemplate(ast, params.TextDocument.Text) err := (*yamllsConnector.Conn).Notify(context.Background(), lsp.MethodTextDocumentDidOpen, params) if err != nil { @@ -40,16 +40,17 @@ func (yamllsConnector Connector) DocumentDidSave(doc *lsplocal.Document, params if yamllsConnector.Conn == nil { return } - params.Text = trimTemplateForYamllsFromAst(doc.Ast, doc.Content) + params.Text = lsplocal.TrimTemplate(doc.Ast, doc.Content) err := (*yamllsConnector.Conn).Notify(context.Background(), lsp.MethodTextDocumentDidSave, params) if err != nil { logger.Error("Error calling yamlls for didSave", err) } - yamllsConnector.DocumentDidChangeFullSync(doc, lsp.DidChangeTextDocumentParams{TextDocument: lsp.VersionedTextDocumentIdentifier{ - TextDocumentIdentifier: params.TextDocument, - }, + yamllsConnector.DocumentDidChangeFullSync(doc, lsp.DidChangeTextDocumentParams{ + TextDocument: lsp.VersionedTextDocumentIdentifier{ + TextDocumentIdentifier: params.TextDocument, + }, }) } @@ -57,7 +58,7 @@ func (yamllsConnector Connector) DocumentDidChange(doc *lsplocal.Document, param if yamllsConnector.Conn == nil { return } - var trimmedText = trimTemplateForYamllsFromAst(doc.Ast, doc.Content) + trimmedText := lsplocal.TrimTemplate(doc.Ast, doc.Content) logger.Debug("Sending DocumentDidChange previous", params) for i, change := range params.ContentChanges { @@ -88,7 +89,7 @@ func (yamllsConnector Connector) DocumentDidChangeFullSync(doc *lsplocal.Documen } logger.Println("Sending DocumentDidChange with full sync, current content:", doc.Content) - var trimmedText = trimTemplateForYamllsFromAst(doc.Ast.Copy(), doc.Content) + trimmedText := lsplocal.TrimTemplate(doc.Ast.Copy(), doc.Content) params.ContentChanges = []lsp.TextDocumentContentChangeEvent{ { diff --git a/internal/adapter/yamlls/trimTemplate.go b/internal/adapter/yamlls/trimTemplate.go deleted file mode 100644 index c5d873b5..00000000 --- a/internal/adapter/yamlls/trimTemplate.go +++ /dev/null @@ -1,123 +0,0 @@ -package yamlls - -import ( - "github.com/mrjosh/helm-ls/internal/lsp" - "github.com/mrjosh/helm-ls/internal/tree-sitter/gotemplate" - sitter "github.com/smacker/go-tree-sitter" -) - -func trimTemplateForYamllsFromAst(ast *sitter.Tree, text string) string { - return lsp.TrimTemplate(ast, text) -} - -func prettyPrintNode(node *sitter.Node, previous []byte, result []byte) { - childCount := node.ChildCount() - - switch node.Type() { - case gotemplate.NodeTypeIfAction: - trimIfAction(node, previous, result) - case gotemplate.NodeTypeBlockAction, gotemplate.NodeTypeWithAction, gotemplate.NodeTypeRangeAction: - trimAction(childCount, node, previous, result) - case gotemplate.NodeTypeDefineAction: - earaseTemplate(node, previous, result) - case gotemplate.NodeTypeFunctionCall: - trimFunctionCall(node, previous, result) - case gotemplate.NodeTypeComment, gotemplate.NodeTypeVariableDefinition, gotemplate.NodeTypeAssignment: - earaseTemplateAndSiblings(node, previous, result) - default: - for i := 0; i < int(childCount); i++ { - prettyPrintNode(node.Child(i), previous, result) - } - } -} - -func trimAction(childCount uint32, node *sitter.Node, previous []byte, result []byte) { - for i := 0; i < int(childCount); i++ { - child := node.Child(i) - switch child.Type() { - case - gotemplate.NodeTypeAssignment, - gotemplate.NodeTypeIf, - gotemplate.NodeTypeSelectorExpression, - gotemplate.NodeTypeElse, - gotemplate.NodeTypeRange, - gotemplate.NodeTypeFunctionCall, - gotemplate.NodeTypeWith, - gotemplate.NodeTypeDefine, - gotemplate.NodeTypeOpenBraces, - gotemplate.NodeTypeOpenBracesDash, - gotemplate.NodeTypeCloseBraces, - gotemplate.NodeTypeCloseBracesDash, - gotemplate.NodeTypeEnd, - gotemplate.NodeTypeInterpretedStringLiteral, - gotemplate.NodeTypeBlock, - gotemplate.NodeTypeVariableDefinition, - gotemplate.NodeTypeVariable, - gotemplate.NodeTypeRangeVariableDefinition: - earaseTemplate(child, previous, result) - default: - prettyPrintNode(child, previous, result) - } - } -} - -func trimIfAction(node *sitter.Node, previous []byte, result []byte) { - if node.StartPoint().Row == node.EndPoint().Row { - earaseTemplate(node, previous, result) - return - } - - curser := sitter.NewTreeCursor(node) - curser.GoToFirstChild() - for curser.GoToNextSibling() { - if curser.CurrentFieldName() == gotemplate.FieldNameCondition { - earaseTemplate(curser.CurrentNode(), previous, result) - earaseTemplate(curser.CurrentNode().NextSibling(), previous, result) - continue - } - switch curser.CurrentNode().Type() { - case gotemplate.NodeTypeIf, gotemplate.NodeTypeElseIf: - earaseTemplate(curser.CurrentNode(), previous, result) - earaseTemplate(curser.CurrentNode().PrevSibling(), previous, result) - case gotemplate.NodeTypeEnd, gotemplate.NodeTypeElse: - earaseTemplateAndSiblings(curser.CurrentNode(), previous, result) - default: - prettyPrintNode(curser.CurrentNode(), previous, result) - } - } - curser.Close() -} - -func trimFunctionCall(node *sitter.Node, previous []byte, result []byte) { - functionName := node.ChildByFieldName(gotemplate.FieldNameFunction) - if functionName.Content(previous) == "include" { - parent := node.Parent() - if parent != nil && parent.Type() == gotemplate.NodeTypeChainedPipeline { - earaseTemplateAndSiblings(parent, previous, result) - } - } -} - -func earaseTemplateAndSiblings(node *sitter.Node, previous []byte, result []byte) { - earaseTemplate(node, previous, result) - prevSibling, nextSibling := node.PrevSibling(), node.NextSibling() - if prevSibling != nil { - earaseTemplate(prevSibling, previous, result) - } - if nextSibling != nil { - earaseTemplate(nextSibling, previous, result) - } -} - -func earaseTemplate(node *sitter.Node, previous []byte, result []byte) { - if node == nil { - return - } - logger.Debug("Content that is erased", node.Content(previous)) - for i := range []byte(node.Content(previous)) { - index := int(node.StartByte()) + i - if result[index] != '\n' && result[index] != '\r' { - result[index] = byte(' ') - } - } -} diff --git a/internal/adapter/yamlls/trimTemplate_test.go b/internal/adapter/yamlls/trimTemplate_test.go deleted file mode 100644 index cc86f1bc..00000000 --- a/internal/adapter/yamlls/trimTemplate_test.go +++ /dev/null @@ -1,400 +0,0 @@ -package yamlls - -import ( - "fmt" - "testing" - - lsplocal "github.com/mrjosh/helm-ls/internal/lsp" - "github.com/stretchr/testify/assert" -) - -type TrimTemplateTestData struct { - documentText string - trimmedText string -} - -var testTrimTemplateTestData = []TrimTemplateTestData{ - { - documentText: ` -{{ .Values.global. }} -yaml: test - -{{block "name"}} T1 {{end}} -`, - trimmedText: ` -{{ .Values.global. }} -yaml: test - - T1 -`, - }, - { - documentText: ` -{{ .Values.global. }} -yaml: test - -{{block "name"}} T1 {{end}} -`, - - trimmedText: ` -{{ .Values.global. }} -yaml: test - - T1 -`, - }, - { - documentText: ` -{{ if eq .Values.service.myParameter "true" }} -apiVersion: keda.sh/v1alpha1 -kind: ScaledObject -metadata: - name: prometheus-scaledobject - namespace: default -spec: - scaleTargetRef: - name: hasd - triggers: - - type: prometheus - metadata: - serverAdress: http://:9090 - metricName: http_requests_total # DEPRECATED: This parameter is deprecated as of KEDA v2.10 and will be removed in version 2.12 - threshold: '100' - query: sum(rate(http_requests_total{deployment="my-deployment"}[2m])) -# yaml-language-server: $schema=https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/keda.sh/scaledobject_v1alpha1.json -{{ end }} -`, - trimmedText: ` - -apiVersion: keda.sh/v1alpha1 -kind: ScaledObject -metadata: - name: prometheus-scaledobject - namespace: default -spec: - scaleTargetRef: - name: hasd - triggers: - - type: prometheus - metadata: - serverAdress: http://:9090 - metricName: http_requests_total # DEPRECATED: This parameter is deprecated as of KEDA v2.10 and will be removed in version 2.12 - threshold: '100' - query: sum(rate(http_requests_total{deployment="my-deployment"}[2m])) -# yaml-language-server: $schema=https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/keda.sh/scaledobject_v1alpha1.json - -`, - }, - { - documentText: ` -{{ if eq .Values.service.myParameter "true" }} -{{ if eq .Values.service.second "true" }} -apiVersion: keda.sh/v1alpha1 -{{ end }} -{{ end }} -`, - trimmedText: ` - - -apiVersion: keda.sh/v1alpha1 - - -`, - }, - { - documentText: ` -{{- if .Values.ingress.enabled }} -apiVersion: apps/v1 -kind: Ingress -{{- end }} -`, - - trimmedText: ` - -apiVersion: apps/v1 -kind: Ingress - -`, - }, - { - documentText: ` -{{- if .Values.ingress.enabled }} -apiVersion: apps/v1 -{{- else }} -apiVersion: apps/v2 -{{- end }} -`, - - trimmedText: ` - -apiVersion: apps/v1 - -apiVersion: apps/v2 - -`, - }, - { - documentText: ` -apiVersion: {{ include "common.capabilities.ingress.apiVersion" . }} -kind: Ingress -metadata: - name: {{ include "common.names.fullname" . }} - namespace: {{ .Release.Namespace | quote }} - labels: {{- include "common.labels.standard" . | nindent 4 }} - {{- if .Values.commonLabels }} - {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} - {{- end }} - app.kubernetes.io/component: grafana - annotations: - {{- if .Values.ingress.certManager }} - kubernetes.io/tls-acme: "true" - {{- end }} - {{- if .Values.ingress.annotations }} - {{- include "common.tplvalues.render" (dict "value" .Values.ingress.annotations "context" $) | nindent 4 }} - {{- end }} - {{- if .Values.commonAnnotations }} - {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} - {{- end }} - `, - trimmedText: ` -apiVersion: {{ include "common.capabilities.ingress.apiVersion" . }} -kind: Ingress -metadata: - name: {{ include "common.names.fullname" . }} - namespace: {{ .Release.Namespace | quote }} - labels: - - - - app.kubernetes.io/component: grafana - annotations: - - kubernetes.io/tls-acme: "true" - - - - - - - - `, - }, - {documentText: `{{- $x := "test" -}}`, trimmedText: ` `}, - {documentText: `{{ $x := "test" }}`, trimmedText: ` `}, - {documentText: `{{ /* comment */ }}`, trimmedText: ` `}, - {documentText: `{{define "name"}} T1 {{end}}`, trimmedText: ` `}, - { - documentText: ` - {{- if .Values.controller.customStartupProbe }} - startupProbe: {} - {{- else if .Values.controller.startupProbe.enabled }} - startupProbe: - httpGet: - path: /healthz - port: {{ .Values.controller.containerPorts.controller }} - {{- end }} - `, - trimmedText: ` - - startupProbe: {} - - startupProbe: - httpGet: - path: /healthz - port: {{ .Values.controller.containerPorts.controller }} - - `, - }, - { - documentText: ` - {{ if eq .Values.replicaCout 1 }} - {{- $kube := "" -}} - apiVersion: v1 - kind: Service - bka: dsa - metadata: - name: {{ include "hello-world.fullname" . }} - labels: - {{- include "hello-world.labels" . | nindent 4 }} - spec: - type: {{ .Values.service.type }} - ports: - - port: {{ .Values.service.port }} - targetPort: http - {{ end }} - `, - trimmedText: ` - - - apiVersion: v1 - kind: Service - bka: dsa - metadata: - name: {{ include "hello-world.fullname" . }} - labels: - - spec: - type: {{ .Values.service.type }} - ports: - - port: {{ .Values.service.port }} - targetPort: http - - `, - }, - { - documentText: `{{ if }}{{- end -}}`, - trimmedText: ` `, - }, - { - // todo: Handle this case better - documentText: ` -{{ if }} - -{{- end -}}`, - trimmedText: ` - }} - - `, - }, - { - documentText: `{{- $shards := $.Values.shards | int }}`, - trimmedText: ` `, - }, - { - documentText: ` -{{- if $.Values.externalAccess.enabled }} -{{- $shards := $.Values.shards | int }} -{{- $replicas := $.Values.replicaCount | int }} -{{- $totalNodes := mul $shards $replicas }} -{{- range $shard, $e := until $shards }} -{{- range $i, $_e := until $replicas }} -{{- $targetPod := printf "%s-shard%d-%d" (include "common.names.fullname" $) $shard $i }} -{{- end }} -{{- end }} -{{- end }} - `, - trimmedText: ` - - - - - - - - - - - `, - }, - { - documentText: ` -data: - pod_template.yaml: |- - {{- if .Values.worker.podTemplate }} - {{- include "common.tplvalues.render" (dict "value" .Values.worker.podTemplate "context" $) | nindent 4 }} - {{- else }} - apiVersion: v1 - kind: Pod - {{ end }} -`, - trimmedText: ` -data: - pod_template.yaml: |- - - - - apiVersion: v1 - kind: Pod - -`, - }, - { - documentText: ` -{{- /* -Copyright Some Company, Inc. -SPDX-License-Identifier: APACHE-2.0 -*/}} -`, - trimmedText: ` - - - - -`, - }, - { - documentText: ` -{{- $namespaces := list .Release.Namespace }} -{{- $namespaces = .Values.controller.workflowNamespaces }} -`, - trimmedText: ` - - -`, - }, - { - documentText: ` -{{- range $namespaces }} -{{- end }} -`, - trimmedText: ` - - -`, - }, - { - documentText: ` -list: - - value: {{ join "," .Values.initialCluster | quote }} - - name: some -`, - trimmedText: ` -list: - - value: {{ join "," .Values.initialCluster | quote }} - - name: some -`, - }, - { - documentText: ` - - name: ELASTICSEARCH_NODE_ROLES - value: {{ join "," $roles | quote }} - - name: ELASTICSEARCH_TRANSPORT_PORT_NUMBER - value: {{ .Values.containerPorts.transport | quote }} - - name: ELASTICSEARCH_HTTP_PORT_NUMBER - value: {{ .Values.containerPorts.restAPI | quote }} -`, - trimmedText: ` - - name: ELASTICSEARCH_NODE_ROLES - value: {{ join "," $roles | quote }} - - name: ELASTICSEARCH_TRANSPORT_PORT_NUMBER - value: {{ .Values.containerPorts.transport | quote }} - - name: ELASTICSEARCH_HTTP_PORT_NUMBER - value: {{ .Values.containerPorts.restAPI | quote }} -`, - }, - { - documentText: ` -apiVersion: {{ if .Values.useStatefulSet }}{{ include "common.capabilities.statefulset.apiVersion" . }}{{- else }}{{ include "common.capabilities.deployment.apiVersion" . }}{{- end }} - `, - trimmedText: ` -apiVersion: - `, - }, -} - -func TestTrimTemplate(t *testing.T) { - for _, testData := range testTrimTemplateTestData { - testTrimTemplateWithTestData(t, testData) - } -} - -func testTrimTemplateWithTestData(t *testing.T, testData TrimTemplateTestData) { - doc := &lsplocal.Document{ - Content: testData.documentText, - Ast: lsplocal.ParseAst(nil, testData.documentText), - } - - trimmed := trimTemplateForYamllsFromAst(doc.Ast, testData.documentText) - - assert.Equal(t, testData.trimmedText, trimmed, fmt.Sprintf("AST was: %v", doc.Ast.RootNode().String())) -} diff --git a/internal/lsp/yaml_ast.go b/internal/lsp/yaml_ast.go index 7e085596..974ce90b 100644 --- a/internal/lsp/yaml_ast.go +++ b/internal/lsp/yaml_ast.go @@ -8,18 +8,22 @@ import ( "github.com/smacker/go-tree-sitter/yaml" ) +func getRangeForNode(node *sitter.Node) sitter.Range { + return sitter.Range{ + StartPoint: node.StartPoint(), + EndPoint: node.EndPoint(), + StartByte: node.StartByte(), + EndByte: node.EndByte(), + } +} + func getTextNodeRanges(gotemplateNode *sitter.Node) []sitter.Range { textNodes := []sitter.Range{} for i := 0; i < int(gotemplateNode.ChildCount()); i++ { child := gotemplateNode.Child(i) if child.Type() == gotemplate.NodeTypeText { - textNodes = append(textNodes, sitter.Range{ - StartPoint: child.StartPoint(), - EndPoint: child.EndPoint(), - StartByte: child.StartByte(), - EndByte: child.EndByte(), - }) + textNodes = append(textNodes, getRangeForNode(child)) } else { textNodes = append(textNodes, getTextNodeRanges(child)...) } @@ -36,18 +40,23 @@ func ParseYamlAst(gotemplateTree *sitter.Tree, content string) *sitter.Tree { return tree } +// TrimTemplate removes all template nodes. +// This is done by keeping only the text nodes +// which is easier then removing the template nodes +// since template nodes could contain other nodes func TrimTemplate(gotemplateTree *sitter.Tree, content string) string { ranges := getTextNodeRanges(gotemplateTree.RootNode()) result := make([]byte, len(content)) - for i := 0; i < len(result); i++ { - if content[i] == '\n' { - result[i] = byte('\n') + for i := range result { + if content[i] == '\n' || content[i] == '\r' { + result[i] = content[i] continue } result[i] = byte(' ') } for _, yamlRange := range ranges { - copy(result[yamlRange.StartByte:yamlRange.EndByte], content[yamlRange.StartByte:yamlRange.EndByte]) + copy(result[yamlRange.StartByte:yamlRange.EndByte], + content[yamlRange.StartByte:yamlRange.EndByte]) } return string(result) } diff --git a/internal/util/config.go b/internal/util/config.go index 55972196..b71bd94a 100644 --- a/internal/util/config.go +++ b/internal/util/config.go @@ -40,14 +40,22 @@ var DefaultConfig = HelmlsConfiguration{ }, } +type YamllsSchemaStoreSettings struct { + Enable bool `json:"enable"` +} + type YamllsSettings struct { - Schemas map[string]string `json:"schemas"` - Completion bool `json:"completion"` - Hover bool `json:"hover"` + Schemas map[string]string `json:"schemas"` + Completion bool `json:"completion"` + Hover bool `json:"hover"` + YamllsSchemaStoreSettings YamllsSchemaStoreSettings `json:"schemaStore"` } var DefaultYamllsSettings = YamllsSettings{ Schemas: map[string]string{"kubernetes": "templates/**"}, Completion: true, Hover: true, + YamllsSchemaStoreSettings: YamllsSchemaStoreSettings{ + Enable: true, + }, } diff --git a/testdata/charts b/testdata/charts new file mode 160000 index 00000000..3b84d7dc --- /dev/null +++ b/testdata/charts @@ -0,0 +1 @@ +Subproject commit 3b84d7dc269f76064e18ba4e21937cc78e77deda diff --git a/yamlls-debug.sh b/yamlls-debug.sh deleted file mode 100755 index a7113376..00000000 --- a/yamlls-debug.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -tee -a /tmp/yamlls-input | yaml-language-server --stdio | tee -a /tmp/yamlls-output