From c103c434f5e2d59f1de80910692b24659c4d2b60 Mon Sep 17 00:00:00 2001 From: Paul Tyng Date: Sat, 23 May 2020 21:40:57 -0400 Subject: [PATCH 1/2] Improve UX of completion items Move descriptions to the documentation field, update to terraform-json 0.5.0 that has additional description support, etc. --- go.mod | 2 +- go.sum | 4 +- internal/lsp/completion.go | 11 ++- internal/mdplain/mdplain.go | 72 ++++++++++++++++++ internal/mdplain/mdplain_test.go | 38 ++++++++++ internal/terraform/lang/config_block.go | 37 ++++++++- internal/terraform/lang/config_block_test.go | 76 ++++++++++++------- internal/terraform/lang/datasource_block.go | 16 +++- .../terraform/lang/datasource_block_test.go | 6 +- internal/terraform/lang/parser.go | 14 +++- internal/terraform/lang/parser_test.go | 3 + internal/terraform/lang/provider_block.go | 6 ++ .../terraform/lang/provider_block_test.go | 15 ++-- internal/terraform/lang/resource_block.go | 16 +++- .../terraform/lang/resource_block_test.go | 9 ++- internal/terraform/lang/schema.go | 17 +++-- internal/terraform/lang/types.go | 27 +++++++ internal/terraform/schema/schema_storage.go | 27 ++++--- langserver/handlers/complete_test.go | 30 ++++++-- .../hashicorp/terraform-json/schemas.go | 29 ++++++- vendor/modules.txt | 2 +- 21 files changed, 376 insertions(+), 81 deletions(-) create mode 100644 internal/mdplain/mdplain.go create mode 100644 internal/mdplain/mdplain_test.go diff --git a/go.mod b/go.mod index b08446dd2..17c064a81 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/google/go-cmp v0.4.0 github.com/hashicorp/go-version v1.2.0 github.com/hashicorp/hcl/v2 v2.3.0 - github.com/hashicorp/terraform-json v0.4.0 + github.com/hashicorp/terraform-json v0.5.0 github.com/mitchellh/cli v1.0.0 github.com/pmezard/go-difflib v1.0.0 github.com/sourcegraph/go-lsp v0.0.0-20200117082640-b19bb38222e2 diff --git a/go.sum b/go.sum index 3dfa4e112..353560b00 100644 --- a/go.sum +++ b/go.sum @@ -34,8 +34,8 @@ github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+d github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl/v2 v2.3.0 h1:iRly8YaMwTBAKhn1Ybk7VSdzbnopghktCD031P8ggUE= github.com/hashicorp/hcl/v2 v2.3.0/go.mod h1:d+FwDBbOLvpAM3Z6J7gPj/VoAGkNe/gm352ZhjJ/Zv8= -github.com/hashicorp/terraform-json v0.4.0 h1:KNh29iNxozP5adfUFBJ4/fWd0Cu3taGgjHB38JYqOF4= -github.com/hashicorp/terraform-json v0.4.0/go.mod h1:eAbqb4w0pSlRmdvl8fOyHAi/+8jnkVYN28gJkSJrLhU= +github.com/hashicorp/terraform-json v0.5.0 h1:7TV3/F3y7QVSuN4r9BEXqnWqrAyeOtON8f0wvREtyzs= +github.com/hashicorp/terraform-json v0.5.0/go.mod h1:eAbqb4w0pSlRmdvl8fOyHAi/+8jnkVYN28gJkSJrLhU= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go index 8560608c0..282a7644b 100644 --- a/internal/lsp/completion.go +++ b/internal/lsp/completion.go @@ -26,14 +26,22 @@ func CompletionList(candidates lang.CompletionCandidates, pos hcl.Pos, caps lsp. } func CompletionItem(candidate lang.CompletionCandidate, pos hcl.Pos, snippetSupport bool) lsp.CompletionItem { + // TODO: deprecated / tags? + + doc := "" + if c := candidate.Documentation(); c != nil { + // TODO: markdown handling + doc = c.Value() + } + if snippetSupport { pos, newText := candidate.Snippet(pos) - return lsp.CompletionItem{ Label: candidate.Label(), Kind: lsp.CIKField, InsertTextFormat: lsp.ITFSnippet, Detail: candidate.Detail(), + Documentation: doc, TextEdit: &lsp.TextEdit{ Range: lsp.Range{ Start: lsp.Position{Line: pos.Line - 1, Character: pos.Column - 1}, @@ -49,5 +57,6 @@ func CompletionItem(candidate lang.CompletionCandidate, pos hcl.Pos, snippetSupp Kind: lsp.CIKField, InsertTextFormat: lsp.ITFPlainText, Detail: candidate.Detail(), + Documentation: doc, } } diff --git a/internal/mdplain/mdplain.go b/internal/mdplain/mdplain.go new file mode 100644 index 000000000..29d1ff93f --- /dev/null +++ b/internal/mdplain/mdplain.go @@ -0,0 +1,72 @@ +package mdplain + +import ( + "fmt" + "regexp" +) + +type replacement struct { + re *regexp.Regexp + sub string +} + +var replacements = []replacement{ + // rules heavily inspired by: https://github.com/stiang/remove-markdown/blob/master/index.js + // back references were removed + + // Header + {regexp.MustCompile(`\n={2,}`), "\n"}, + // Fenced codeblocks + {regexp.MustCompile(`~{3}.*\n`), ""}, + // Strikethrough + {regexp.MustCompile("~~"), ""}, + // Fenced codeblocks + {regexp.MustCompile("`{3}.*\\n"), ""}, + // Remove HTML tags + {regexp.MustCompile(`<[^>]*>`), ""}, + // Remove setext-style headers + {regexp.MustCompile(`^[=\-]{2,}\s*$`), ""}, + // Remove footnotes? + {regexp.MustCompile(`\[\^.+?\](\: .*?$)?`), ""}, + {regexp.MustCompile(`\s{0,2}\[.*?\]: .*?$`), ""}, + // Remove images + {regexp.MustCompile(`\!\[(.*?)\][\[\(].*?[\]\)]`), "$1"}, + // Remove inline links + {regexp.MustCompile(`\[(.*?)\][\[\(].*?[\]\)]`), "$1"}, + // Remove blockquotes + {regexp.MustCompile(`^\s{0,3}>\s?`), ""}, + // Remove reference-style links? + {regexp.MustCompile(`^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$`), ""}, + // Remove atx-style headers + {regexp.MustCompile(`^(\n)?\s{0,}#{1,6}\s+| {0,}(\n)?\s{0,}#{0,} {0,}(\n)?\s{0,}$`), "$1$2$3"}, + // Remove emphasis (repeat the line to remove double emphasis) + {regexp.MustCompile(`([*_]{1,3})([^\t\n\f\r *_].*?[^\t\n\f\r *_]{0,1})([*_]{1,3})`), "$2"}, + {regexp.MustCompile(`([*_]{1,3})([^\t\n\f\r *_].*?[^\t\n\f\r *_]{0,1})([*_]{1,3})`), "$2"}, + // Remove code blocks + {regexp.MustCompile("(`{3,})(.*?)(`{3,})"), "$2"}, + // Remove inline code + {regexp.MustCompile("`(.+?)`"), "$1"}, + // Replace two or more newlines with exactly two? Not entirely sure this belongs here... + {regexp.MustCompile(`\n{2,}`), "\n\n"}, +} + +// Clean runs a VERY naive cleanup of markdown text to make it more palatable as plain text. +func Clean(markdown string) string { + // TODO: maybe use https://github.com/russross/blackfriday/tree/v2, write custom renderer or + // generate HTML then process that to plaintext using https://github.com/jaytaylor/html2text + + fmt.Printf("cleaning %q\n", markdown) + + result := markdown + before := markdown + + for _, r := range replacements { + result = r.re.ReplaceAllString(result, r.sub) + if before != result { + fmt.Printf("RE: %q, %q to %q\n", r.re, before, result) + } + before = result + } + + return string(result) +} diff --git a/internal/mdplain/mdplain_test.go b/internal/mdplain/mdplain_test.go new file mode 100644 index 000000000..60f334a61 --- /dev/null +++ b/internal/mdplain/mdplain_test.go @@ -0,0 +1,38 @@ +package mdplain_test + +import ( + "testing" + + "github.com/hashicorp/terraform-ls/internal/mdplain" +) + +func TestClean(t *testing.T) { + for _, c := range []struct { + markdown string + expected string + }{ + {"", ""}, + + {"_foo_", "foo"}, + {"__foo__", "foo"}, + {"foo_bar", "foo_bar"}, + + {"*foo*", "foo"}, + {"**foo**", "foo"}, + {"Desc **2**", "Desc 2"}, + {"1 * 3 = 3", "1 * 3 = 3"}, + + {"## Header", "Header"}, + {"Header\n====\n\nSome text", "Header\n\nSome text"}, + + {"* item 1\n* item 2\n\n\nSome text", "* item 1\n* item 2\n\nSome text"}, + } { + t.Run(c.expected, func(t *testing.T) { + actual := mdplain.Clean(c.markdown) + + if c.expected != actual { + t.Fatalf("expected:\n%s\n\ngot:\n%s\n", c.expected, actual) + } + }) + } +} diff --git a/internal/terraform/lang/config_block.go b/internal/terraform/lang/config_block.go index 5dd70ae00..4ae49781a 100644 --- a/internal/terraform/lang/config_block.go +++ b/internal/terraform/lang/config_block.go @@ -7,11 +7,13 @@ import ( hcl "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + tfjson "github.com/hashicorp/terraform-json" ) type configBlockFactory interface { New(*hclsyntax.Block) (ConfigBlock, error) LabelSchema() LabelSchema + Documentation() MarkupContent } type labelCandidates map[string][]CompletionCandidate @@ -129,8 +131,9 @@ func (l *completeList) IsComplete() bool { } type labelCandidate struct { - label string - detail string + label string + detail string + documentation MarkupContent } func (c *labelCandidate) Label() string { @@ -141,6 +144,10 @@ func (c *labelCandidate) Detail() string { return c.detail } +func (c *labelCandidate) Documentation() MarkupContent { + return c.documentation +} + func (c *labelCandidate) Snippet(pos hcl.Pos) (hcl.Pos, string) { return pos, c.label } @@ -156,9 +163,25 @@ func (c *attributeCandidate) Label() string { } func (c *attributeCandidate) Detail() string { + if c.Attr == nil { + return "" + } return schemaAttributeDetail(c.Attr.Schema()) } +func (c *attributeCandidate) Documentation() MarkupContent { + if c.Attr == nil { + return PlainText("") + } + if schema := c.Attr.Schema(); schema != nil { + if schema.DescriptionKind == tfjson.SchemaDescriptionKindMarkdown { + return Markdown(schema.Description) + } + return PlainText(schema.Description) + } + return PlainText("") +} + func (c *attributeCandidate) Snippet(pos hcl.Pos) (hcl.Pos, string) { return pos, fmt.Sprintf("%s = %s", c.Name, snippetForAttrType(0, c.Attr.Schema().AttributeType)) } @@ -177,6 +200,16 @@ func (c *nestedBlockCandidate) Detail() string { return schemaBlockDetail(c.BlockType) } +func (c *nestedBlockCandidate) Documentation() MarkupContent { + if c.BlockType == nil || c.BlockType.Schema() == nil || c.BlockType.Schema().Block == nil { + return PlainText("") + } + if c.BlockType.Schema().Block.DescriptionKind == tfjson.SchemaDescriptionKindMarkdown { + return Markdown(c.BlockType.Schema().Block.Description) + } + return PlainText(c.BlockType.Schema().Block.Description) +} + func (c *nestedBlockCandidate) Snippet(pos hcl.Pos) (hcl.Pos, string) { return pos, snippetForNestedBlock(c.Name) } diff --git a/internal/terraform/lang/config_block_test.go b/internal/terraform/lang/config_block_test.go index a935d224d..b8efe6d65 100644 --- a/internal/terraform/lang/config_block_test.go +++ b/internal/terraform/lang/config_block_test.go @@ -98,24 +98,27 @@ func TestCompletableBlock_CompletionCandidatesAtPos(t *testing.T) { attrOnlySchema, []renderedCandidate{ { - Label: "first_str", - Detail: "(Optional, string)", + Label: "first_str", + Detail: "Optional, string", + Documentation: "", Snippet: renderedSnippet{ Pos: hcl.Pos{Line: 2, Column: 1, Byte: 14}, Text: `first_str = "${0:value}"`, }, }, { - Label: "required_bool", - Detail: "(Required, bool) test boolean", + Label: "required_bool", + Detail: "Required, bool", + Documentation: "test boolean", Snippet: renderedSnippet{ Pos: hcl.Pos{Line: 2, Column: 1, Byte: 14}, Text: "required_bool = ${0:false}", }, }, { - Label: "second_num", - Detail: "(Optional, number) random number", + Label: "second_num", + Detail: "Optional, number", + Documentation: "random number", Snippet: renderedSnippet{ Pos: hcl.Pos{Line: 2, Column: 1, Byte: 14}, Text: "second_num = ${0:42}", @@ -133,16 +136,18 @@ func TestCompletableBlock_CompletionCandidatesAtPos(t *testing.T) { singleBlockOnlySchema, []renderedCandidate{ { - Label: "optional_single", - Detail: "(Optional, single)", + Label: "optional_single", + Detail: "Block, single", + Documentation: "", Snippet: renderedSnippet{ Pos: hcl.Pos{Line: 2, Column: 1, Byte: 14}, Text: "optional_single {\n ${0}\n}", }, }, { - Label: "required_single", - Detail: "(Required, single)", + Label: "required_single", + Detail: "Block, single, min: 1", + Documentation: "", Snippet: renderedSnippet{ Pos: hcl.Pos{Line: 2, Column: 1, Byte: 14}, Text: "required_single {\n ${0}\n}", @@ -160,24 +165,27 @@ func TestCompletableBlock_CompletionCandidatesAtPos(t *testing.T) { listBlockOnlySchema, []renderedCandidate{ { - Label: "optional_list", - Detail: "(Optional, list)", + Label: "optional_list", + Detail: "Block, list", + Documentation: "", Snippet: renderedSnippet{ Pos: hcl.Pos{Line: 2, Column: 1, Byte: 14}, Text: "optional_list {\n ${0}\n}", }, }, { - Label: "required_list", - Detail: "(Required, list)", + Label: "required_list", + Detail: "Block, list, min: 1", + Documentation: "", Snippet: renderedSnippet{ Pos: hcl.Pos{Line: 2, Column: 1, Byte: 14}, Text: "required_list {\n ${0}\n}", }, }, { - Label: "undeclared_max1_list", - Detail: "(Optional, list)", + Label: "undeclared_max1_list", + Detail: "Block, list, max: 1", + Documentation: "", Snippet: renderedSnippet{ Pos: hcl.Pos{Line: 2, Column: 1, Byte: 14}, Text: "undeclared_max1_list {\n ${0}\n}", @@ -195,8 +203,9 @@ func TestCompletableBlock_CompletionCandidatesAtPos(t *testing.T) { singleBlockOnlySchema, []renderedCandidate{ { - Label: "one", - Detail: "(Optional, string)", + Label: "one", + Detail: "Optional, string", + Documentation: "", Snippet: renderedSnippet{ Pos: hcl.Pos{Line: 2, Column: 20, Byte: 33}, Text: `one = "${0:value}"`, @@ -214,24 +223,27 @@ func TestCompletableBlock_CompletionCandidatesAtPos(t *testing.T) { attrOnlySchema, []renderedCandidate{ { - Label: "first_str", - Detail: "(Optional, string)", + Label: "first_str", + Detail: "Optional, string", + Documentation: "", Snippet: renderedSnippet{ Pos: hcl.Pos{Column: 1, Line: 2, Byte: 14}, Text: `first_str = "${0:value}"`, }, }, { - Label: "required_bool", - Detail: "(Required, bool) test boolean", + Label: "required_bool", + Detail: "Required, bool", + Documentation: "test boolean", Snippet: renderedSnippet{ Pos: hcl.Pos{Column: 1, Line: 2, Byte: 14}, Text: `required_bool = ${0:false}`, }, }, { - Label: "second_num", - Detail: "(Optional, number) random number", + Label: "second_num", + Detail: "Optional, number", + Documentation: "random number", Snippet: renderedSnippet{ Pos: hcl.Pos{Column: 1, Line: 2, Byte: 14}, Text: `second_num = ${0:42}`, @@ -282,10 +294,15 @@ func renderCandidates(list CompletionCandidates, pos hcl.Pos) []renderedCandidat rendered := make([]renderedCandidate, len(list.List())) for i, c := range list.List() { pos, text := c.Snippet(pos) + doc := "" + if c.Documentation() != nil { + doc = c.Documentation().Value() + } rendered[i] = renderedCandidate{ - Label: c.Label(), - Detail: c.Detail(), + Label: c.Label(), + Detail: c.Detail(), + Documentation: doc, Snippet: renderedSnippet{ Pos: pos, Text: text, @@ -302,9 +319,10 @@ func sortRenderedCandidates(candidates []renderedCandidate) { } type renderedCandidate struct { - Label string - Detail string - Snippet renderedSnippet + Label string + Detail string + Documentation string + Snippet renderedSnippet } type renderedSnippet struct { diff --git a/internal/terraform/lang/datasource_block.go b/internal/terraform/lang/datasource_block.go index 5bb023a33..899fc203f 100644 --- a/internal/terraform/lang/datasource_block.go +++ b/internal/terraform/lang/datasource_block.go @@ -41,6 +41,12 @@ func (f *datasourceBlockFactory) BlockType() string { return "data" } +func (f *datasourceBlockFactory) Documentation() MarkupContent { + return PlainText("A data block requests that Terraform read from a given data source and export the result " + + "under the given local name. The name is used to refer to this resource from elsewhere in the same " + + "Terraform module, but has no significance outside of the scope of a module.") +} + type datasourceBlock struct { logger *log.Logger @@ -125,9 +131,15 @@ func (r *datasourceBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCand func dataSourceCandidates(dataSources []schema.DataSource) []CompletionCandidate { candidates := []CompletionCandidate{} for _, ds := range dataSources { + var desc MarkupContent = PlainText(ds.Description) + if ds.DescriptionKind == tfjson.SchemaDescriptionKindMarkdown { + desc = Markdown(ds.Description) + } + candidates = append(candidates, &labelCandidate{ - label: ds.Name, - detail: ds.Provider, + label: ds.Name, + detail: fmt.Sprintf("Data Source (%s)", ds.Provider), + documentation: desc, }) } return candidates diff --git a/internal/terraform/lang/datasource_block_test.go b/internal/terraform/lang/datasource_block_test.go index 85c7ad190..0e123edd0 100644 --- a/internal/terraform/lang/datasource_block_test.go +++ b/internal/terraform/lang/datasource_block_test.go @@ -131,7 +131,7 @@ func TestDataSourceBlock_completionCandidatesAtPos(t *testing.T) { []renderedCandidate{ { Label: "attr_optional", - Detail: "(Optional, string)", + Detail: "Optional, string", Snippet: renderedSnippet{ Pos: hcl.Pos{Line: 2, Column: 1, Byte: 26}, Text: `attr_optional = "${0:value}"`, @@ -139,7 +139,7 @@ func TestDataSourceBlock_completionCandidatesAtPos(t *testing.T) { }, { Label: "attr_required", - Detail: "(Required, string)", + Detail: "Required, string", Snippet: renderedSnippet{ Pos: hcl.Pos{Line: 2, Column: 1, Byte: 26}, Text: `attr_required = "${0:value}"`, @@ -180,7 +180,7 @@ func TestDataSourceBlock_completionCandidatesAtPos(t *testing.T) { []renderedCandidate{ { Label: "custom_ds", - Detail: "custom", + Detail: "Data Source (custom)", Snippet: renderedSnippet{ Pos: hcl.Pos{Line: 1, Column: 5, Byte: 6}, Text: "custom_ds", diff --git a/internal/terraform/lang/parser.go b/internal/terraform/lang/parser.go index 88a13e0ba..4bdc329d4 100644 --- a/internal/terraform/lang/parser.go +++ b/internal/terraform/lang/parser.go @@ -100,8 +100,9 @@ func (p *parser) BlockTypeCandidates() CompletionCandidates { for name, t := range bTypes { list.candidates = append(list.candidates, &completableBlockType{ - TypeName: name, - LabelSchema: t.LabelSchema(), + TypeName: name, + LabelSchema: t.LabelSchema(), + documentation: t.Documentation(), }) } @@ -109,8 +110,9 @@ func (p *parser) BlockTypeCandidates() CompletionCandidates { } type completableBlockType struct { - TypeName string - LabelSchema LabelSchema + TypeName string + LabelSchema LabelSchema + documentation MarkupContent } func (bt *completableBlockType) Label() string { @@ -125,6 +127,10 @@ func (bt *completableBlockType) Detail() string { return "" } +func (bt *completableBlockType) Documentation() MarkupContent { + return bt.documentation +} + func (p *parser) ParseBlockFromHCL(block *hcl.Block) (ConfigBlock, error) { if block == nil { return nil, EmptyConfigErr diff --git a/internal/terraform/lang/parser_test.go b/internal/terraform/lang/parser_test.go index 968e0cb47..2b6144ce9 100644 --- a/internal/terraform/lang/parser_test.go +++ b/internal/terraform/lang/parser_test.go @@ -32,6 +32,9 @@ func TestParser_BlockTypeCandidates_snippet(t *testing.T) { expectedCandidate := renderedCandidate{ Label: "data", Detail: "", + Documentation: "A data block requests that Terraform read from a given data source and export the result " + + "under the given local name. The name is used to refer to this resource from elsewhere in the same " + + "Terraform module, but has no significance outside of the scope of a module.", Snippet: renderedSnippet{ Pos: hcl.InitialPos, Text: `data "${1}" "${2:name}" { diff --git a/internal/terraform/lang/provider_block.go b/internal/terraform/lang/provider_block.go index 338b04b43..ef55abcec 100644 --- a/internal/terraform/lang/provider_block.go +++ b/internal/terraform/lang/provider_block.go @@ -39,6 +39,12 @@ func (f *providerBlockFactory) BlockType() string { return "provider" } +func (f *providerBlockFactory) Documentation() MarkupContent { + return PlainText("A provider block is used to specify a provider configuration. The body of the block (between " + + "{ and }) contains configuration arguments for the provider itself. Most arguments in this section are " + + "specified by the provider itself.") +} + type providerBlock struct { logger *log.Logger diff --git a/internal/terraform/lang/provider_block_test.go b/internal/terraform/lang/provider_block_test.go index a20076c3b..9f892a1d0 100644 --- a/internal/terraform/lang/provider_block_test.go +++ b/internal/terraform/lang/provider_block_test.go @@ -121,16 +121,18 @@ func TestProviderBlock_completionCandidatesAtPos(t *testing.T) { hcl.Pos{Line: 2, Column: 1, Byte: 20}, []renderedCandidate{ { - Label: "attr_optional", - Detail: "(Optional, string)", + Label: "attr_optional", + Detail: "Optional, string", + Documentation: "", Snippet: renderedSnippet{ Pos: hcl.Pos{Line: 2, Column: 1, Byte: 20}, Text: `attr_optional = "${0:value}"`, }, }, { - Label: "attr_required", - Detail: "(Required, string)", + Label: "attr_required", + Detail: "Required, string", + Documentation: "", Snippet: renderedSnippet{ Pos: hcl.Pos{Line: 2, Column: 1, Byte: 20}, Text: `attr_required = "${0:value}"`, @@ -171,8 +173,9 @@ func TestProviderBlock_completionCandidatesAtPos(t *testing.T) { hcl.Pos{Line: 1, Column: 9, Byte: 10}, []renderedCandidate{ { - Label: "custom", - Detail: "provider", + Label: "custom", + Detail: "provider", + Documentation: "", Snippet: renderedSnippet{ Pos: hcl.Pos{Line: 1, Column: 9, Byte: 10}, Text: "custom", diff --git a/internal/terraform/lang/resource_block.go b/internal/terraform/lang/resource_block.go index 425958319..f7202c43e 100644 --- a/internal/terraform/lang/resource_block.go +++ b/internal/terraform/lang/resource_block.go @@ -41,6 +41,12 @@ func (r *resourceBlockFactory) BlockType() string { return "resource" } +func (r *resourceBlockFactory) Documentation() MarkupContent { + return PlainText("A resource block declares a resource of a given type with a given local name. The name is " + + "used to refer to this resource from elsewhere in the same Terraform module, but has no significance " + + "outside of the scope of a module.") +} + type resourceBlock struct { logger *log.Logger @@ -124,9 +130,15 @@ func (r *resourceBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCandid func resourceCandidates(resources []schema.Resource) []CompletionCandidate { candidates := []CompletionCandidate{} for _, r := range resources { + var desc MarkupContent = PlainText(r.Description) + if r.DescriptionKind == tfjson.SchemaDescriptionKindMarkdown { + desc = Markdown(r.Description) + } + candidates = append(candidates, &labelCandidate{ - label: r.Name, - detail: r.Provider, + label: r.Name, + detail: fmt.Sprintf("Resource (%s)", r.Provider), + documentation: desc, }) } return candidates diff --git a/internal/terraform/lang/resource_block_test.go b/internal/terraform/lang/resource_block_test.go index 522d98777..3d9caf184 100644 --- a/internal/terraform/lang/resource_block_test.go +++ b/internal/terraform/lang/resource_block_test.go @@ -131,7 +131,7 @@ func TestResourceBlock_completionCandidatesAtPos(t *testing.T) { []renderedCandidate{ { Label: "attr_optional", - Detail: "(Optional, string)", + Detail: "Optional, string", Snippet: renderedSnippet{ Pos: hcl.Pos{Line: 2, Column: 1, Byte: 30}, Text: `attr_optional = "${0:value}"`, @@ -139,7 +139,7 @@ func TestResourceBlock_completionCandidatesAtPos(t *testing.T) { }, { Label: "attr_required", - Detail: "(Required, string)", + Detail: "Required, string", Snippet: renderedSnippet{ Pos: hcl.Pos{Line: 2, Column: 1, Byte: 30}, Text: `attr_required = "${0:value}"`, @@ -180,8 +180,9 @@ func TestResourceBlock_completionCandidatesAtPos(t *testing.T) { hcl.Pos{Line: 1, Column: 9, Byte: 10}, []renderedCandidate{ { - Label: "custom_rs", - Detail: "custom", + Label: "custom_rs", + Detail: "Resource (custom)", + Documentation: "", Snippet: renderedSnippet{ Pos: hcl.Pos{Line: 1, Column: 9, Byte: 10}, Text: "custom_rs", diff --git a/internal/terraform/lang/schema.go b/internal/terraform/lang/schema.go index af2e442af..0d7cd8bd7 100644 --- a/internal/terraform/lang/schema.go +++ b/internal/terraform/lang/schema.go @@ -69,18 +69,21 @@ func schemaAttributeDetail(attr *tfjson.SchemaAttribute) string { requiredText = "Required" } - return strings.TrimSpace(fmt.Sprintf("(%s, %s) %s", - requiredText, attr.AttributeType.FriendlyName(), attr.Description)) + return strings.TrimSpace(fmt.Sprintf("%s, %s", + requiredText, attr.AttributeType.FriendlyName())) } func schemaBlockDetail(blockType *BlockType) string { blockS := blockType.Schema() - requiredText := "Required" - if len(blockType.BlockList) >= int(blockS.MinItems) { - requiredText = "Optional" + detail := fmt.Sprintf("Block, %s", blockS.NestingMode) + + if blockS.MinItems > 0 { + detail += fmt.Sprintf(", min: %d", blockS.MinItems) + } + if blockS.MaxItems > 0 { + detail += fmt.Sprintf(", max: %d", blockS.MaxItems) } - return strings.TrimSpace(fmt.Sprintf("(%s, %s)", - requiredText, blockS.NestingMode)) + return strings.TrimSpace(detail) } diff --git a/internal/terraform/lang/types.go b/internal/terraform/lang/types.go index b34fbc7f1..c564f2ee5 100644 --- a/internal/terraform/lang/types.go +++ b/internal/terraform/lang/types.go @@ -5,6 +5,7 @@ import ( hcl "github.com/hashicorp/hcl/v2" tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-ls/internal/mdplain" "github.com/hashicorp/terraform-ls/internal/terraform/schema" ) @@ -74,5 +75,31 @@ type CompletionCandidates interface { type CompletionCandidate interface { Label() string Detail() string + Documentation() MarkupContent Snippet(pos hcl.Pos) (hcl.Pos, string) } + +// MarkupContent reflects lsp.MarkupContent +type MarkupContent interface { + // TODO: eventually will need to propapate Kind here once the LSP + // protocol types we use support it + Value() string +} + +// PlainText represents plain text markup content for the LSP. +type PlainText string + +// Value returns the content itself for the LSP protocol. +func (m PlainText) Value() string { + return string(m) +} + +// Markdown represents markdown formatted markup content for the LSP. +type Markdown string + +// Value returns the content itself for the LSP protocol. +func (m Markdown) Value() string { + // This currently returns plaintext for Markdown, but should be changed once + // the protocol types support markdown. + return mdplain.Clean(string(m)) +} diff --git a/internal/terraform/schema/schema_storage.go b/internal/terraform/schema/schema_storage.go index 3d16a9209..d1887b750 100644 --- a/internal/terraform/schema/schema_storage.go +++ b/internal/terraform/schema/schema_storage.go @@ -30,13 +30,17 @@ type Writer interface { } type Resource struct { - Name string - Provider string + Name string + Provider string + Description string + DescriptionKind tfjson.SchemaDescriptionKind } type DataSource struct { - Name string - Provider string + Name string + Provider string + Description string + DescriptionKind tfjson.SchemaDescriptionKind } type Storage struct { @@ -186,6 +190,7 @@ func (s *Storage) Providers() ([]string, error) { } func (s *Storage) ResourceSchema(rType string) (*tfjson.Schema, error) { + // TODO: this is going to need to use provider identities, especially in 0.13 s.logger.Printf("Reading %q resource schema", rType) ps, err := s.schema() @@ -214,10 +219,11 @@ func (s *Storage) Resources() ([]Resource, error) { resources := make([]Resource, 0) for provider, schema := range ps.Schemas { - for name := range schema.ResourceSchemas { + for name, r := range schema.ResourceSchemas { resources = append(resources, Resource{ - Provider: provider, - Name: name, + Provider: provider, + Name: name, + Description: r.Block.Description, }) } } @@ -254,10 +260,11 @@ func (s *Storage) DataSources() ([]DataSource, error) { dataSources := make([]DataSource, 0) for provider, schema := range ps.Schemas { - for name := range schema.DataSourceSchemas { + for name, d := range schema.DataSourceSchemas { dataSources = append(dataSources, DataSource{ - Provider: provider, - Name: name, + Provider: provider, + Name: name, + Description: d.Block.Description, }) } } diff --git a/langserver/handlers/complete_test.go b/langserver/handlers/complete_test.go index 6d8288b29..e706bbe80 100644 --- a/langserver/handlers/complete_test.go +++ b/langserver/handlers/complete_test.go @@ -84,19 +84,22 @@ func TestCompletion_withValidData(t *testing.T) { { "label":"anonymous", "kind":5, - "detail":"(Optional, number) Desc 1", + "detail":"Optional, number", + "documentation":"Desc 1", "insertTextFormat":1 }, { "label":"base_url", "kind":5, - "detail":"(Optional, string) Desc 2", + "detail":"Optional, string", + "documentation":"Desc 2", "insertTextFormat":1 }, { "label":"individual", "kind":5, - "detail":"(Optional, bool) Desc 3", + "detail":"Optional, bool", + "documentation":"Desc 3", "insertTextFormat":1 } ] @@ -115,21 +118,38 @@ var testSchemaOutput = `{ "anonymous": { "type": "number", "description": "Desc 1", + "description_kind": "plaintext", "optional": true }, "base_url": { "type": "string", - "description": "Desc 2", + "description": "Desc **2**", + "description_kind": "markdown", "optional": true }, "individual": { "type": "bool", - "description": "Desc 3", + "description": "Desc _3_", + "description_kind": "markdown", "optional": true } } } } } + }, + "resource_schemas": { + "test_resource_1": { + "version": 0, + "block": { + "description": "Resource 1 description", + "description_kind": "markdown", + "attributes": { + "deprecated_attr": { + "deprecated": true + } + } + } + } } }` diff --git a/vendor/github.com/hashicorp/terraform-json/schemas.go b/vendor/github.com/hashicorp/terraform-json/schemas.go index e025fbe04..5dd430a55 100644 --- a/vendor/github.com/hashicorp/terraform-json/schemas.go +++ b/vendor/github.com/hashicorp/terraform-json/schemas.go @@ -83,6 +83,18 @@ type Schema struct { Block *SchemaBlock `json:"block,omitempty"` } +// SchemaDescriptionKind describes the format type for a particular description's field. +type SchemaDescriptionKind string + +const ( + // SchemaDescriptionKindPlain indicates a string in plain text format. + SchemaDescriptionKindPlain SchemaDescriptionKind = "plaintext" + + // SchemaDescriptionKindMarkdown indicates a Markdown string and may need to be + // processed prior to presentation. + SchemaDescriptionKindMarkdown SchemaDescriptionKind = "markdown" +) + // SchemaBlock represents a nested block within a particular schema. type SchemaBlock struct { // The attributes defined at the particular level of this block. @@ -90,6 +102,14 @@ type SchemaBlock struct { // Any nested blocks within this particular block. NestedBlocks map[string]*SchemaBlockType `json:"block_types,omitempty"` + + // The description for this block and format of the description. If + // no kind is provided, it can be assumed to be plain text. + Description string `json:"description,omitempty"` + DescriptionKind SchemaDescriptionKind `json:"description_kind,omitempty"` + + // If true, this block is deprecated. + Deprecated bool `json:"deprecated,omitempty"` } // SchemaNestingMode is the nesting mode for a particular nested @@ -145,8 +165,13 @@ type SchemaAttribute struct { // The attribute type. AttributeType cty.Type `json:"type,omitempty"` - // The description field for this attribute. - Description string `json:"description,omitempty"` + // The description field for this attribute. If no kind is + // provided, it can be assumed to be plain text. + Description string `json:"description,omitempty"` + DescriptionKind SchemaDescriptionKind `json:"description_kind,omitempty"` + + // If true, this attribute is deprecated. + Deprecated bool `json:"deprecated,omitempty"` // If true, this attribute is required - it has to be entered in // configuration. diff --git a/vendor/modules.txt b/vendor/modules.txt index 94bfcfbe1..71484c1b3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -37,7 +37,7 @@ github.com/hashicorp/hcl/v2 github.com/hashicorp/hcl/v2/ext/customdecode github.com/hashicorp/hcl/v2/hclsyntax github.com/hashicorp/hcl/v2/json -# github.com/hashicorp/terraform-json v0.4.0 +# github.com/hashicorp/terraform-json v0.5.0 github.com/hashicorp/terraform-json # github.com/mattn/go-colorable v0.0.9 github.com/mattn/go-colorable From 83fa844dc8f0a7685c7f9a20978126b11f961d6d Mon Sep 17 00:00:00 2001 From: Paul Tyng Date: Tue, 26 May 2020 10:58:25 -0400 Subject: [PATCH 2/2] Fix godoc --- internal/terraform/lang/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/terraform/lang/types.go b/internal/terraform/lang/types.go index c564f2ee5..37f178377 100644 --- a/internal/terraform/lang/types.go +++ b/internal/terraform/lang/types.go @@ -62,7 +62,7 @@ type Attribute struct { hclAttribute *hcl.Attribute } -// CompletionCandidate represents a list of candidates +// CompletionCandidates represents a list of candidates // for completion loosely reflecting lsp.CompletionList type CompletionCandidates interface { List() []CompletionCandidate