Skip to content

Commit

Permalink
feat(completion): tests and refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
qvalentin committed Apr 18, 2024
1 parent cfcfa30 commit 0b7d843
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 88 deletions.
127 changes: 62 additions & 65 deletions internal/documentation/godocs/gotemplate.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,70 +37,67 @@ type GoTemplateSnippet struct {
Detail string
Doc string
Snippet string
Filter string
}

var (
TextSnippets = []GoTemplateSnippet{
{
Name: "comment",
Detail: "{{- /* a comment with white space trimmed from preceding and following text */ -}}",
Doc: "A comment; discarded. May contain newlines. Comments do not nest and must start and end at the delimiters, as shown here.",
Snippet: "{{- /* $1 */ -}}",
},
{
Name: "{{ }}",
Detail: "template",
Doc: "",
Snippet: "{{- $0 }}",
},
{
Name: "if",
Detail: "{{if pipeline}} T1 {{end}}",
Doc: "If the value of the pipeline is empty, no output is generated; otherwise, T1 is executed. The empty values are false, 0, any nil pointer or interface value, and any array, slice, map, or string of length zero. Dot is unaffected.",
Snippet: "{{- if $1 }}\n $0 \n{{- end }}",
},
{
Name: "if else",
Detail: "{{if pipeline}} T1 {{else}} T0 {{end}}",
Doc: "If the value of the pipeline is empty, T0 is executed; otherwise, T1 is executed. Dot is unaffected.",
Snippet: "{{- if $1 }}\n $2 \n{{- else }}\n $0 \n{{- end }}",
},
{
Name: "if else if",
Detail: "{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}",
Doc: "To simplify the appearance of if-else chains, the else action of an if may include another if directly; the effect is exactly the same as writing {{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}",
Snippet: "{{- if $1 }}\n $2 \n{{- else if $4 }}\n $0 \n{{- end }}",
},
{
Name: "range",
Detail: "{{range pipeline}} T1 {{end}}",
Doc: "The value of the pipeline must be an array, slice, map, or channel. If the value of the pipeline has length zero, nothing is output; otherwise, dot is set to the successive elements of the array, slice, or map and T1 is executed. If the value is a map and the keys are of basic type with a defined order, the elements will be visited in sorted key order.",
Snippet: "{{- range $1 }}\n $0 \n{{- end }}",
},
{
Name: "range else",
Detail: "{{range pipeline}} T1 {{else}} T0 {{end}}",
Doc: "The value of the pipeline must be an array, slice, map, or channel. If the value of the pipeline has length zero, dot is unaffected and T0 is executed; otherwise, dot is set to the successive elements of the array, slice, or map and T1 is executed.",
Snippet: "{{- range $1 }}\n $2 {{- else }}\n $0 \n{{- end }}",
},
{
Name: "block",
Detail: "{{block \"name\" pipeline}} T1 {{end}}",
Doc: "A block is shorthand for defining a template {{define \"name\"}} T1 {{end}} and then executing it in place {{template \"name\" pipeline}} The typical use is to define a set of root templates that are then customized by redefining the block templates within.",
Snippet: "{{- block $1 }}\n $0 \n{{- end }}",
},
{
Name: "with",
Detail: "{{with pipeline}} T1 {{end}}",
Doc: "If the value of the pipeline is empty, no output is generated; otherwise, dot is set to the value of the pipeline and T1 is executed.",
Snippet: "{{- with $1 }}\n $0 \n{{- end }}",
},
{
Name: "with else",
Detail: "{{with pipeline}} T1 {{else}} T0 {{end}}",
Doc: "If the value of the pipeline is empty, dot is unaffected and T0 is executed; otherwise, dot is set to the value of the pipeline and T1 is executed",
Snippet: "{{- with $1 }}\n $2 {{- else }}\n $0 \n{{- end }}",
},
}
)
var TextSnippets = []GoTemplateSnippet{
{
Name: "comment",
Detail: "{{- /* a comment with white space trimmed from preceding and following text */ -}}",
Doc: "A comment; discarded. May contain newlines. Comments do not nest and must start and end at the delimiters, as shown here.",
Snippet: "{{- /* $1 */ -}}",
},
{
Name: "{{ }}",
Detail: "template",
Doc: "",
Snippet: "{{- $0 }}",
},
{
Name: "if",
Detail: "{{if pipeline}} T1 {{end}}",
Doc: "If the value of the pipeline is empty, no output is generated; otherwise, T1 is executed. The empty values are false, 0, any nil pointer or interface value, and any array, slice, map, or string of length zero. Dot is unaffected.",
Snippet: "{{- if $1 }}\n $0 \n{{- end }}",
},
{
Name: "if else",
Detail: "{{if pipeline}} T1 {{else}} T0 {{end}}",
Doc: "If the value of the pipeline is empty, T0 is executed; otherwise, T1 is executed. Dot is unaffected.",
Snippet: "{{- if $1 }}\n $2 \n{{- else }}\n $0 \n{{- end }}",
},
{
Name: "if else if",
Detail: "{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}",
Doc: "To simplify the appearance of if-else chains, the else action of an if may include another if directly; the effect is exactly the same as writing {{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}",
Snippet: "{{- if $1 }}\n $2 \n{{- else if $4 }}\n $0 \n{{- end }}",
},
{
Name: "range",
Detail: "{{range pipeline}} T1 {{end}}",
Doc: "The value of the pipeline must be an array, slice, map, or channel. If the value of the pipeline has length zero, nothing is output; otherwise, dot is set to the successive elements of the array, slice, or map and T1 is executed. If the value is a map and the keys are of basic type with a defined order, the elements will be visited in sorted key order.",
Snippet: "{{- range $1 }}\n $0 \n{{- end }}",
},
{
Name: "range else",
Detail: "{{range pipeline}} T1 {{else}} T0 {{end}}",
Doc: "The value of the pipeline must be an array, slice, map, or channel. If the value of the pipeline has length zero, dot is unaffected and T0 is executed; otherwise, dot is set to the successive elements of the array, slice, or map and T1 is executed.",
Snippet: "{{- range $1 }}\n $2 {{- else }}\n $0 \n{{- end }}",
},
{
Name: "block",
Detail: "{{block \"name\" pipeline}} T1 {{end}}",
Doc: "A block is shorthand for defining a template {{define \"name\"}} T1 {{end}} and then executing it in place {{template \"name\" pipeline}} The typical use is to define a set of root templates that are then customized by redefining the block templates within.",
Snippet: "{{- block $1 }}\n $0 \n{{- end }}",
},
{
Name: "with",
Detail: "{{with pipeline}} T1 {{end}}",
Doc: "If the value of the pipeline is empty, no output is generated; otherwise, dot is set to the value of the pipeline and T1 is executed.",
Snippet: "{{- with $1 }}\n $0 \n{{- end }}",
},
{
Name: "with else",
Detail: "{{with pipeline}} T1 {{else}} T0 {{end}}",
Doc: "If the value of the pipeline is empty, dot is unaffected and T0 is executed; otherwise, dot is set to the value of the pipeline and T1 is executed",
Snippet: "{{- with $1 }}\n $2 {{- else }}\n $0 \n{{- end }}",
},
}
20 changes: 11 additions & 9 deletions internal/handler/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func init() {
}

func (h *langHandler) Completion(ctx context.Context, params *lsp.CompletionParams) (result *lsp.CompletionList, err error) {
logger.Debug("Running completion with params", params)
doc, ok := h.documents.Get(params.TextDocument.URI)
if !ok {
return nil, errors.New("Could not get document: " + params.TextDocument.URI.Filename())
Expand All @@ -50,6 +51,7 @@ func (h *langHandler) Completion(ctx context.Context, params *lsp.CompletionPara
result := make([]lsp.CompletionItem, 0)
result = append(result, textCompletionsItems...)
result = append(result, yamllsCompletions(ctx, h, params)...)
logger.Debug("Sending completions ", result)
return &protocol.CompletionList{IsIncomplete: false, Items: result}, err
}

Expand All @@ -69,7 +71,6 @@ func (h *langHandler) Completion(ctx context.Context, params *lsp.CompletionPara
}

logger.Println(fmt.Sprintf("Word found for completions is < %s >", word))

items = make([]lsp.CompletionItem, 0)
for _, v := range helmdocs.BuiltInObjects {
items = append(items, lsp.CompletionItem{
Expand Down Expand Up @@ -132,19 +133,20 @@ func completionAstParsing(doc *lsplocal.Document, position lsp.Position) (string
logger.Println("currentNode", currentNode)
logger.Println("relevantChildNode", relevantChildNode)

switch relevantChildNode.Type() {
nodeType := relevantChildNode.Type()
switch nodeType {
case gotemplate.NodeTypeIdentifier:
word = relevantChildNode.Content([]byte(doc.Content))
case gotemplate.NodeTypeDot:
logger.Debug("TraverseIdentifierPathUp for dot node")
word = lsplocal.TraverseIdentifierPathUp(relevantChildNode, doc)
case gotemplate.NodeTypeDotSymbol:
case gotemplate.NodeTypeDotSymbol, gotemplate.NodeTypeFieldIdentifier:
logger.Debug("GetFieldIdentifierPath")
word = lsplocal.GetFieldIdentifierPath(relevantChildNode, doc)
case gotemplate.NodeTypeText, gotemplate.NodeTypeTemplate:
return word, true
}
logger.Println("word", word)
logger.Debug("word", word)
return word, false
}

Expand Down Expand Up @@ -285,14 +287,14 @@ func getTextCompletionItems(gotemplateSnippet []godocs.GoTemplateSnippet) (resul
func textCompletionItem(gotemplateSnippet godocs.GoTemplateSnippet) lsp.CompletionItem {
return lsp.CompletionItem{
Label: gotemplateSnippet.Name,
TextEdit: &lsp.TextEdit{
Range: lsp.Range{},
NewText: gotemplateSnippet.Snippet,
},
// TextEdit: &lsp.TextEdit{
// // Range: lsp.Range{}, // TODO: range must contain the requested range
// NewText: gotemplateSnippet.Snippet,
// },
InsertText: gotemplateSnippet.Snippet,
Detail: gotemplateSnippet.Detail,
Documentation: gotemplateSnippet.Doc,
Kind: lsp.CompletionItemKindText,
InsertTextFormat: lsp.InsertTextFormatSnippet,
FilterText: gotemplateSnippet.Filter,
}
}
100 changes: 93 additions & 7 deletions internal/handler/completion_main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/mrjosh/helm-ls/internal/adapter/yamlls"
"github.com/mrjosh/helm-ls/internal/charts"
helmdocs "github.com/mrjosh/helm-ls/internal/documentation/helm"
lsplocal "github.com/mrjosh/helm-ls/internal/lsp"
"github.com/mrjosh/helm-ls/internal/util"
"github.com/stretchr/testify/assert"
Expand All @@ -16,20 +17,101 @@ import (

func TestCompletionMain(t *testing.T) {
testCases := []struct {
desc string
position lsp.Position
expectedInsertText string
expectedError error
desc string
position lsp.Position
expectedInsertText string
notExpectedInsertTexts []string
expectedError error
}{
{
desc: "Test completion on .Values.re",
desc: "Test completion on {{ . }}",
position: lsp.Position{
Line: 6,
Character: 4,
},
expectedInsertText: "Chart",
notExpectedInsertTexts: []string{
helmdocs.HelmFuncs[0].Name,
"replicaCount",
"toYaml",
},
expectedError: nil,
},
{
desc: "Test completion on .Values.",
position: lsp.Position{
Line: 0,
Character: 11,
},
expectedInsertText: "replicaCount",
notExpectedInsertTexts: []string{
helmdocs.HelmFuncs[0].Name,
},
expectedError: nil,
},
{
desc: "Test completion on .Values.re",
position: lsp.Position{
Line: 1,
Character: 13,
},
expectedInsertText: "not $x\n\nnegate the boolean value of $x",
expectedError: nil,
expectedInsertText: "replicaCount",
notExpectedInsertTexts: []string{
helmdocs.HelmFuncs[0].Name,
},
expectedError: nil,
},
{
desc: "Test completion on {{ toY }}",
position: lsp.Position{
Line: 3,
Character: 6,
},
expectedInsertText: "toYaml",
notExpectedInsertTexts: []string{
"replicaCount",
},
expectedError: nil,
},
{
desc: "Test completion on text",
position: lsp.Position{
Line: 4,
Character: 0,
},
expectedInsertText: "{{- if $1 }}\n $2 \n{{- else }}\n $0 \n{{- end }}",
notExpectedInsertTexts: []string{
"replicaCount",
"toYaml",
},
expectedError: nil,
},
{
desc: "Test completion on .Chart.N",
position: lsp.Position{
Line: 5,
Character: 11,
},
expectedInsertText: "Name",
notExpectedInsertTexts: []string{
helmdocs.HelmFuncs[0].Name,
"replicaCount",
"toYaml",
},
expectedError: nil,
},
// {
// desc: "Test completion on {{ }}",
// position: lsp.Position{
// Line: 4,
// Character: 3,
// },
// expectedInsertText: "toYaml",
// notExpectedInsertTexts: []string{
// "replicaCount",
// },
// expectedError: nil,
// },
}
for _, tt := range testCases {
t.Run(tt.desc, func(t *testing.T) {
Expand Down Expand Up @@ -71,6 +153,10 @@ func TestCompletionMain(t *testing.T) {
insertTexts = append(insertTexts, item.InsertText)
}
assert.Contains(t, insertTexts, tt.expectedInsertText)

for _, notExpectedInsertText := range tt.notExpectedInsertTexts {
assert.NotContains(t, insertTexts, notExpectedInsertText)
}
})
}
}
6 changes: 1 addition & 5 deletions internal/language_features/generic_template_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,7 @@ type GenericTemplateContextFeature struct {
}

func (f *GenericTemplateContextFeature) getTemplateContext() (lsplocal.TemplateContext, error) {
templateContext := f.GenericDocumentUseCase.Document.SymbolTable.GetTemplateContext(lsplocal.GetRangeForNode(f.Node))
if len(templateContext) == 0 || templateContext == nil {
return lsplocal.TemplateContext{}, fmt.Errorf("no template context found")
}
return templateContext, nil
return f.GenericDocumentUseCase.Document.SymbolTable.GetTemplateContext(lsplocal.GetRangeForNode(f.Node))
}

func (f *GenericTemplateContextFeature) getReferencesFromSymbolTable(templateContext lsplocal.TemplateContext) []lsp.Location {
Expand Down
37 changes: 37 additions & 0 deletions internal/language_features/template_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

helmdocs "github.com/mrjosh/helm-ls/internal/documentation/helm"
lsplocal "github.com/mrjosh/helm-ls/internal/lsp"
"github.com/mrjosh/helm-ls/internal/protocol"
"github.com/mrjosh/helm-ls/internal/tree-sitter/gotemplate"
"github.com/mrjosh/helm-ls/internal/util"
"github.com/mrjosh/helm-ls/pkg/chart"
Expand Down Expand Up @@ -131,3 +132,39 @@ func (f *TemplateContextFeature) getTableOrValueForSelector(values chartutil.Val
}
return values.YAML()
}

func (f *TemplateContextFeature) Completion() (result *lsp.CompletionList, err error) {
templateContext, err := f.getTemplateContext()
if err != nil {
return nil, err
}

if len(templateContext) == 0 {
result := helmdocs.BuiltInObjects
return protocol.NewCompletionResults(result).ToLSP(), nil
}

if len(templateContext) == 1 {
result, ok := helmdocs.BuiltInOjectVals[templateContext[0]]

Check failure on line 148 in internal/language_features/template_context.go

View workflow job for this annotation

GitHub Actions / tests (1.21.5, ubuntu-latest)

result declared and not used

Check failure on line 148 in internal/language_features/template_context.go

View workflow job for this annotation

GitHub Actions / lint (1.21.5, ubuntu-latest)

result declared and not used
if !ok {
result := helmdocs.BuiltInObjects

Check failure on line 150 in internal/language_features/template_context.go

View workflow job for this annotation

GitHub Actions / tests (1.21.5, ubuntu-latest)

result declared and not used

Check failure on line 150 in internal/language_features/template_context.go

View workflow job for this annotation

GitHub Actions / lint (1.21.5, ubuntu-latest)

result declared and not used
}
return nil, nil
}

m := make(map[string]lsp.CompletionItem)
for _, queriedValuesFiles := range f.Chart.ResolveValueFiles(templateContext.Tail(), f.ChartStore) {
for _, valuesFile := range queriedValuesFiles.ValuesFiles.AllValuesFiles() {
for _, item := range h.getValue(valuesFile.Values, queriedValuesFiles.Selector) {

Check failure on line 158 in internal/language_features/template_context.go

View workflow job for this annotation

GitHub Actions / tests (1.21.5, ubuntu-latest)

undefined: h

Check failure on line 158 in internal/language_features/template_context.go

View workflow job for this annotation

GitHub Actions / lint (1.21.5, ubuntu-latest)

undefined: h
m[item.InsertText] = item
}
}
}

for _, item := range m {
result = append(result, item)

Check failure on line 165 in internal/language_features/template_context.go

View workflow job for this annotation

GitHub Actions / tests (1.21.5, ubuntu-latest)

first argument to append must be a slice; have result (variable of type *"go.lsp.dev/protocol".CompletionList)

Check failure on line 165 in internal/language_features/template_context.go

View workflow job for this annotation

GitHub Actions / lint (1.21.5, ubuntu-latest)

first argument to append must be a slice; have result (variable of type *"go.lsp.dev/protocol".CompletionList)
}

return result

Check failure on line 168 in internal/language_features/template_context.go

View workflow job for this annotation

GitHub Actions / tests (1.21.5, ubuntu-latest)

not enough return values

Check failure on line 168 in internal/language_features/template_context.go

View workflow job for this annotation

GitHub Actions / lint (1.21.5, ubuntu-latest)

not enough return values
return nil, nil
}
Loading

0 comments on commit 0b7d843

Please sign in to comment.