From 6689af08dc3b144c2ac09724114f851141a5e570 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Fri, 24 Jan 2025 16:45:59 -0300 Subject: [PATCH 1/8] parse doc comments --- server/internal/lsp/cst/parser.go | 3 +- server/pkg/parser/parse_structs_test.go | 61 +++++++++-- server/pkg/parser/parser.go | 75 ++++++++++++- server/pkg/parser/parser_functions_test.go | 31 +++++- server/pkg/parser/parser_test.go | 117 +++++++++++++++------ server/pkg/parser/parser_variables_test.go | 29 +++-- server/pkg/symbols/indexable.go | 11 ++ 7 files changed, 268 insertions(+), 59 deletions(-) diff --git a/server/internal/lsp/cst/parser.go b/server/internal/lsp/cst/parser.go index 5067d4cc..ea32ddbb 100644 --- a/server/internal/lsp/cst/parser.go +++ b/server/internal/lsp/cst/parser.go @@ -4,8 +4,9 @@ package cst //TSLanguage *tree_sitter_c3(); import "C" import ( - sitter "github.com/smacker/go-tree-sitter" "unsafe" + + sitter "github.com/smacker/go-tree-sitter" ) func NewSitterParser() *sitter.Parser { diff --git a/server/pkg/parser/parse_structs_test.go b/server/pkg/parser/parse_structs_test.go index f328342b..64148c3a 100644 --- a/server/pkg/parser/parse_structs_test.go +++ b/server/pkg/parser/parse_structs_test.go @@ -13,6 +13,8 @@ import ( func TestFindsGlobalStructs(t *testing.T) { source := ` module x; + +<* docs *> struct MyStruct (MyInterface, MySecondInterface) { int data; char key; @@ -37,8 +39,9 @@ fn void MyStruct.init(&self) assert.Same(t, symbols.Get("x").Children()[0], found) assert.Equal(t, "MyStruct", found.GetName()) assert.False(t, found.IsUnion()) - assert.Equal(t, idx.NewRange(2, 0, 6, 1), found.GetDocumentRange()) - assert.Equal(t, idx.NewRange(2, 7, 2, 15), found.GetIdRange()) + assert.Equal(t, idx.NewRange(4, 0, 8, 1), found.GetDocumentRange()) + assert.Equal(t, idx.NewRange(4, 7, 4, 15), found.GetIdRange()) + assert.Equal(t, "docs", found.GetDocComment()) }) t.Run("finds struct members", func(t *testing.T) { @@ -48,7 +51,7 @@ fn void MyStruct.init(&self) member := found.GetMembers()[0] assert.Equal(t, "data", member.GetName()) assert.Equal(t, "int", member.GetType().GetName()) - assert.Equal(t, idx.NewRange(3, 5, 3, 9), member.GetIdRange()) + assert.Equal(t, idx.NewRange(5, 5, 5, 9), member.GetIdRange()) assert.Equal(t, "docId", member.GetDocumentURI()) assert.Equal(t, "x", member.GetModuleString()) assert.Same(t, found.Children()[0], member) @@ -56,7 +59,7 @@ fn void MyStruct.init(&self) member = found.GetMembers()[1] assert.Equal(t, "key", member.GetName()) assert.Equal(t, "char", member.GetType().GetName()) - assert.Equal(t, idx.NewRange(4, 6, 4, 9), member.GetIdRange()) + assert.Equal(t, idx.NewRange(6, 6, 6, 9), member.GetIdRange()) assert.Equal(t, "docId", member.GetDocumentURI()) assert.Equal(t, "x", member.GetModuleString()) assert.Same(t, found.Children()[1], member) @@ -67,7 +70,7 @@ fn void MyStruct.init(&self) assert.Equal(t, "Camera", member.GetType().GetName()) assert.Equal(t, "raylib::Camera", member.GetType().GetFullQualifiedName()) - assert.Equal(t, idx.NewRange(5, 16, 5, 22), member.GetIdRange()) + assert.Equal(t, idx.NewRange(7, 16, 7, 22), member.GetIdRange()) assert.Equal(t, "docId", member.GetDocumentURI()) assert.Equal(t, "x", member.GetModuleString()) assert.Same(t, found.Children()[2], member) @@ -126,6 +129,40 @@ struct Foo { assert.Same(t, substruct.Children()[1], member) } +func TestParses_substructs_with_invalid_docs(t *testing.T) { + source := ` +module x; +struct Foo { + <* this doc comment does not compile, but parses *> + struct data { + int print; + char name; + } +} +` + docId := "docId" + doc := document.NewDocument(docId, source) + parser := createParser() + + symbols, _ := parser.ParseSymbols(&doc) + + found := symbols.Get("x").Structs["Foo"] + assert.Same(t, symbols.Get("x").Children()[0], found) + assert.Equal(t, "Foo", found.GetName()) + assert.False(t, found.IsUnion()) + assert.Equal(t, idx.NewRange(2, 0, 8, 1), found.GetDocumentRange()) + assert.Equal(t, idx.NewRange(2, 7, 2, 10), found.GetIdRange()) + + member := found.GetMembers()[0] + assert.Equal(t, "data", member.GetName()) + assert.Equal(t, true, member.IsStruct()) + assert.Equal(t, idx.NewRange(4, 9, 4, 13), member.GetIdRange()) + assert.Same(t, found.Children()[0], member) + + // Docs here are invalid + assert.Equal(t, "", member.GetDocComment()) +} + func TestParses_anonymous_substructs(t *testing.T) { source := ` module x; @@ -266,6 +303,7 @@ func TestParse_struct_subtyping_members_should_be_flagged(t *testing.T) { func TestParse_Unions(t *testing.T) { source := `module x; + <* docs *> union MyUnion{ short as_short; int as_int; @@ -280,14 +318,16 @@ func TestParse_Unions(t *testing.T) { found := module.Structs["MyUnion"] assert.Equal(t, "MyUnion", found.GetName()) assert.True(t, found.IsUnion()) - assert.Equal(t, idx.NewRange(1, 1, 4, 2), found.GetDocumentRange()) - assert.Equal(t, idx.NewRange(1, 7, 1, 14), found.GetIdRange()) + assert.Equal(t, idx.NewRange(2, 1, 5, 2), found.GetDocumentRange()) + assert.Equal(t, idx.NewRange(2, 7, 2, 14), found.GetIdRange()) + assert.Equal(t, "docs", found.GetDocComment()) assert.Same(t, module.Children()[0], found) }) } func TestParse_bitstructs(t *testing.T) { source := `module x; + <* docs *> bitstruct Test : uint { ushort a : 0..15; @@ -305,6 +345,7 @@ func TestParse_bitstructs(t *testing.T) { assert.Same(t, symbols.Get("x").Children()[0], found) assert.Equal(t, "Test", found.GetName()) assert.Equal(t, "uint", found.Type().GetName()) + assert.Equal(t, "docs", found.GetDocComment()) members := found.Members() assert.Equal(t, 3, len(members)) @@ -314,21 +355,21 @@ func TestParse_bitstructs(t *testing.T) { assert.Equal(t, "a", member.GetName()) assert.Equal(t, "ushort", members[0].GetType().GetName()) assert.Equal(t, [2]uint{0, 15}, members[0].GetBitRange()) - assert.Equal(t, idx.NewRange(3, 9, 3, 10), members[0].GetIdRange()) + assert.Equal(t, idx.NewRange(4, 9, 4, 10), members[0].GetIdRange()) assert.Same(t, found.Children()[0], member) // Check field b assert.Equal(t, "b", members[1].GetName()) assert.Equal(t, "ushort", members[1].GetType().GetName()) assert.Equal(t, [2]uint{16, 31}, members[1].GetBitRange()) - assert.Equal(t, idx.NewRange(4, 9, 4, 10), members[1].GetIdRange()) + assert.Equal(t, idx.NewRange(5, 9, 5, 10), members[1].GetIdRange()) assert.Same(t, found.Children()[1], members[1]) // Check field c assert.Equal(t, "c", members[2].GetName()) assert.Equal(t, "bool", members[2].GetType().GetName()) assert.Equal(t, [2]uint{7}, members[2].GetBitRange()) - assert.Equal(t, idx.NewRange(5, 7, 5, 8), members[2].GetIdRange()) + assert.Equal(t, idx.NewRange(6, 7, 6, 8), members[2].GetIdRange()) assert.Same(t, found.Children()[2], members[2]) }) } diff --git a/server/pkg/parser/parser.go b/server/pkg/parser/parser.go index dde26c80..60fdf475 100644 --- a/server/pkg/parser/parser.go +++ b/server/pkg/parser/parser.go @@ -9,6 +9,7 @@ import ( "github.com/tliron/commonlog" ) +const DocCommentQuery = `(doc_comment) @doc_comment` const VarDeclarationQuery = `(var_declaration name: (identifier) @variable_name )` @@ -53,6 +54,7 @@ func (p *Parser) ParseSymbols(doc *document.Document) (symbols_table.UnitModules //fmt.Println(doc.URI, doc.ContextSyntaxTree.RootNode()) query := `[ +(source_file ` + DocCommentQuery + `) (source_file ` + ModuleDeclaration + `) (source_file ` + ImportDeclaration + `) (source_file ` + GlobalVarDeclaration + `) @@ -85,6 +87,7 @@ func (p *Parser) ParseSymbols(doc *document.Document) (symbols_table.UnitModules var moduleSymbol *idx.Module anonymousModuleName := true lastModuleName := "" + lastDocComment := "" //subtyptingToResolve := []StructWithSubtyping{} for { @@ -96,7 +99,7 @@ func (p *Parser) ParseSymbols(doc *document.Document) (symbols_table.UnitModules for _, c := range m.Captures { nodeType := c.Node.Type() nodeEndPoint := idx.NewPositionFromTreeSitterPoint(c.Node.EndPoint()) - if nodeType != "module" { + if nodeType != "module" && nodeType != "doc_comment" { moduleSymbol = parsedModules.GetOrInitModule( lastModuleName, &doc.URI, @@ -106,6 +109,23 @@ func (p *Parser) ParseSymbols(doc *document.Document) (symbols_table.UnitModules } switch nodeType { + case "doc_comment": + lastDocComment = "" + doc_text := c.Node.Child(1) + if doc_text.Type() == "doc_comment_text" { + lastDocComment = doc_text.Content(sourceCode) + } + if c.Node.ChildCount() >= 4 { + // TODO: Save the contract nodes as structured data for richer information + // For now, just append and display their sources + for i := 2; i <= int(c.Node.ChildCount())-2; i++ { + contract_node := c.Node.Child(i) + if contract_node.Type() == "doc_comment_contract" { + lastDocComment += "\n" + contract_node.Content(sourceCode) + } + } + } + case "module": anonymousModuleName = false module, _, _ := p.nodeToModule(doc, c.Node, sourceCode) @@ -122,6 +142,10 @@ func (p *Parser) ParseSymbols(doc *document.Document) (symbols_table.UnitModules moduleSymbol.SetStartPosition(idx.NewPositionFromTreeSitterPoint(start)) moduleSymbol.ChangeModule(lastModuleName) + if lastDocComment != "" { + moduleSymbol.SetDocComment(lastDocComment) + } + case "import_declaration": imports := p.nodeToImport(doc, c.Node, sourceCode) moduleSymbol.AddImports(imports) @@ -131,17 +155,31 @@ func (p *Parser) ParseSymbols(doc *document.Document) (symbols_table.UnitModules moduleSymbol.AddVariables(variables) pendingToResolve.AddVariableType(variables, moduleSymbol) + if lastDocComment != "" { + for _, v := range variables { + v.SetDocComment(lastDocComment) + } + } + case "func_definition", "func_declaration": function, err := p.nodeToFunction(c.Node, moduleSymbol, &doc.URI, sourceCode) if err == nil { moduleSymbol.AddFunction(&function) pendingToResolve.AddFunctionTypes(&function, moduleSymbol) + + if lastDocComment != "" { + function.SetDocComment(lastDocComment) + } } case "enum_declaration": enum := p.nodeToEnum(c.Node, moduleSymbol, &doc.URI, sourceCode) moduleSymbol.AddEnum(&enum) + if lastDocComment != "" { + enum.SetDocComment(lastDocComment) + } + case "struct_declaration": strukt, membersNeedingSubtypingResolve := p.nodeToStruct(c.Node, moduleSymbol, &doc.URI, sourceCode) moduleSymbol.AddStruct(&strukt) @@ -151,30 +189,58 @@ func (p *Parser) ParseSymbols(doc *document.Document) (symbols_table.UnitModules pendingToResolve.AddStructMemberTypes(&strukt, moduleSymbol) + if lastDocComment != "" { + strukt.SetDocComment(lastDocComment) + } + case "bitstruct_declaration": bitstruct := p.nodeToBitStruct(c.Node, moduleSymbol, &doc.URI, sourceCode) moduleSymbol.AddBitstruct(&bitstruct) + if lastDocComment != "" { + bitstruct.SetDocComment(lastDocComment) + } + case "define_declaration": def := p.nodeToDef(c.Node, moduleSymbol, &doc.URI, sourceCode) moduleSymbol.AddDef(&def) pendingToResolve.AddDefType(&def, moduleSymbol) + if lastDocComment != "" { + def.SetDocComment(lastDocComment) + } + case "const_declaration": _const := p.nodeToConstant(c.Node, moduleSymbol, &doc.URI, sourceCode) moduleSymbol.AddVariable(&_const) + if lastDocComment != "" { + _const.SetDocComment(lastDocComment) + } + case "fault_declaration": fault := p.nodeToFault(c.Node, moduleSymbol, &doc.URI, sourceCode) moduleSymbol.AddFault(&fault) + if lastDocComment != "" { + fault.SetDocComment(lastDocComment) + } + case "interface_declaration": interf := p.nodeToInterface(c.Node, moduleSymbol, &doc.URI, sourceCode) moduleSymbol.AddInterface(&interf) + if lastDocComment != "" { + interf.SetDocComment(lastDocComment) + } + case "macro_declaration": macro := p.nodeToMacro(c.Node, moduleSymbol, &doc.URI, sourceCode) moduleSymbol.AddFunction(¯o) + + if lastDocComment != "" { + macro.SetDocComment(lastDocComment) + } default: // TODO test that module ends up with wrong endPosition // when this source code: @@ -183,10 +249,15 @@ func (p *Parser) ParseSymbols(doc *document.Document) (symbols_table.UnitModules // int value = 4; // v // } + lastDocComment = "" continue } - moduleSymbol.SetEndPosition(nodeEndPoint) + if nodeType != "doc_comment" { + // Ensure the next node won't receive the same doc comment + lastDocComment = "" + moduleSymbol.SetEndPosition(nodeEndPoint) + } } } diff --git a/server/pkg/parser/parser_functions_test.go b/server/pkg/parser/parser_functions_test.go index dcbf59ab..98ba2cdd 100644 --- a/server/pkg/parser/parser_functions_test.go +++ b/server/pkg/parser/parser_functions_test.go @@ -61,6 +61,7 @@ func TestExtractSymbols_Functions_Declaration(t *testing.T) { assert.True(t, fn.IsSome(), "Function was not found") assert.Equal(t, "init_window", fn.Get().GetName(), "Function name") assert.Equal(t, "void", fn.Get().GetReturnType().GetName(), "Return type") + assert.Equal(t, "", fn.Get().GetDocComment()) assert.Equal(t, idx.NewRange(0, 8, 0, 19), fn.Get().GetIdRange()) assert.Equal(t, idx.NewRange(0, 0, 0, 78), fn.Get().GetDocumentRange()) }) @@ -81,6 +82,7 @@ func TestExtractSymbols_Functions_Declaration(t *testing.T) { assert.Equal(t, "void", fn.Get().GetReturnType().GetName(), "Return type") assert.Equal(t, idx.NewRange(3, 10, 3, 21), fn.Get().GetIdRange()) assert.Equal(t, idx.NewRange(3, 2, 3, 80), fn.Get().GetDocumentRange()) + assert.Equal(t, "abc", fn.Get().GetDocComment()) }) t.Run("Resolves function with unnamed parameters correctly", func(t *testing.T) { @@ -151,9 +153,31 @@ func TestExtractSymbols_FunctionsWithArguments(t *testing.T) { assert.Equal(t, "void", fn.Get().GetReturnType().GetName(), "Return type") assert.Equal(t, idx.NewRange(0, 8, 0, 12), fn.Get().GetIdRange()) assert.Equal(t, idx.NewRange(0, 0, 2, 2), fn.Get().GetDocumentRange()) + assert.Equal(t, "", fn.Get().GetDocComment()) }) - t.Run("Finds function with doc comment", func(t *testing.T) { + t.Run("Finds function with simple doc comment", func(t *testing.T) { + source := `<* + abc + *> + fn void test(int number, char ch, int* pointer) { + return 1; + }` + docId := "docId" + doc := document.NewDocument(docId, source) + parser := createParser() + symbols, _ := parser.ParseSymbols(&doc) + + fn := symbols.Get("docid").GetChildrenFunctionByName("test") + assert.True(t, fn.IsSome(), "Function was not found") + assert.Equal(t, "test", fn.Get().GetName(), "Function name") + assert.Equal(t, "void", fn.Get().GetReturnType().GetName(), "Return type") + assert.Equal(t, idx.NewRange(3, 10, 3, 14), fn.Get().GetIdRange()) + assert.Equal(t, idx.NewRange(3, 2, 5, 3), fn.Get().GetDocumentRange()) + assert.Equal(t, "abc", fn.Get().GetDocComment()) + }) + + t.Run("Finds function with doc comment with contracts", func(t *testing.T) { source := `<* abc @pure @@ -175,6 +199,11 @@ func TestExtractSymbols_FunctionsWithArguments(t *testing.T) { assert.Equal(t, "void", fn.Get().GetReturnType().GetName(), "Return type") assert.Equal(t, idx.NewRange(7, 10, 7, 14), fn.Get().GetIdRange()) assert.Equal(t, idx.NewRange(7, 2, 9, 3), fn.Get().GetDocumentRange()) + assert.Equal(t, `abc +@pure +@param [in] pointer +@require number > 0, number < 1000 : "invalid number" +@ensure return == 1`, fn.Get().GetDocComment()) }) t.Run("Finds function arguments", func(t *testing.T) { diff --git a/server/pkg/parser/parser_test.go b/server/pkg/parser/parser_test.go index 6d31f422..d17588e1 100644 --- a/server/pkg/parser/parser_test.go +++ b/server/pkg/parser/parser_test.go @@ -27,8 +27,9 @@ func TestParses_empty_document(t *testing.T) { func TestParses_TypedEnums(t *testing.T) { docId := "doc" source := ` + <* abc *> enum Colors:int { RED, BLUE, GREEN } - fn bool Colors.hasRed(Colors color) + fn bool Colors.hasRed(Colors color) {} ` doc := document.NewDocument(docId, source) @@ -51,8 +52,17 @@ func TestParses_TypedEnums(t *testing.T) { scope := symbols.Get("doc") enum := scope.Enums["Colors"] - assert.Equal(t, idx.NewRange(1, 1, 1, 37), enum.GetDocumentRange(), "Wrong document rage") - assert.Equal(t, idx.NewRange(1, 6, 1, 12), enum.GetIdRange(), "Wrong identifier range") + assert.Equal(t, idx.NewRange(2, 1, 2, 37), enum.GetDocumentRange(), "Wrong document rage") + assert.Equal(t, idx.NewRange(2, 6, 2, 12), enum.GetIdRange(), "Wrong identifier range") + }) + + t.Run("finds doc comment", func(t *testing.T) { + symbols, _ := parser.ParseSymbols(&doc) + + scope := symbols.Get("doc") + enum := scope.Enums["Colors"] + + assert.Equal(t, "abc", enum.GetDocComment()) }) t.Run("finds defined enumerators", func(t *testing.T) { @@ -61,17 +71,17 @@ func TestParses_TypedEnums(t *testing.T) { enum := symbols.Get("doc").Enums["Colors"] e := enum.GetEnumerator("RED") assert.Equal(t, "RED", e.GetName()) - assert.Equal(t, idx.NewRange(1, 19, 1, 22), e.GetIdRange()) + assert.Equal(t, idx.NewRange(2, 19, 2, 22), e.GetIdRange()) assert.Same(t, enum.Children()[0], e) e = enum.GetEnumerator("BLUE") assert.Equal(t, "BLUE", e.GetName()) - assert.Equal(t, idx.NewRange(1, 24, 1, 28), e.GetIdRange()) + assert.Equal(t, idx.NewRange(2, 24, 2, 28), e.GetIdRange()) assert.Same(t, enum.Children()[1], e) e = enum.GetEnumerator("GREEN") assert.Equal(t, "GREEN", e.GetName()) - assert.Equal(t, idx.NewRange(1, 30, 1, 35), e.GetIdRange()) + assert.Equal(t, idx.NewRange(2, 30, 2, 35), e.GetIdRange()) assert.Same(t, enum.Children()[2], e) }) @@ -103,7 +113,10 @@ func TestParses_TypedEnums(t *testing.T) { func TestParses_UnTypedEnums(t *testing.T) { docId := "doc" - source := `enum Colors { RED, BLUE, GREEN };` + source := `<* + abc + *> + enum Colors { RED, BLUE, GREEN };` doc := document.NewDocument(docId, source) parser := createParser() @@ -122,8 +135,17 @@ func TestParses_UnTypedEnums(t *testing.T) { symbols, _ := parser.ParseSymbols(&doc) enum := symbols.Get("doc").Enums["Colors"] - assert.Equal(t, idx.NewRange(0, 0, 0, 32), enum.GetDocumentRange(), "Wrong document rage") - assert.Equal(t, idx.NewRange(0, 5, 0, 11), enum.GetIdRange(), "Wrong identifier range") + assert.Equal(t, idx.NewRange(3, 1, 3, 33), enum.GetDocumentRange(), "Wrong document rage") + assert.Equal(t, idx.NewRange(3, 6, 3, 12), enum.GetIdRange(), "Wrong identifier range") + }) + + t.Run("finds doc comment", func(t *testing.T) { + symbols, _ := parser.ParseSymbols(&doc) + + scope := symbols.Get("doc") + enum := scope.Enums["Colors"] + + assert.Equal(t, "abc", enum.GetDocComment()) }) t.Run("finds defined enumerators", func(t *testing.T) { @@ -132,21 +154,22 @@ func TestParses_UnTypedEnums(t *testing.T) { enum := symbols.Get("doc").Enums["Colors"] e := enum.GetEnumerator("RED") assert.Equal(t, "RED", e.GetName()) - assert.Equal(t, idx.NewRange(0, 14, 0, 17), e.GetIdRange()) + assert.Equal(t, idx.NewRange(3, 15, 3, 18), e.GetIdRange()) e = enum.GetEnumerator("BLUE") assert.Equal(t, "BLUE", e.GetName()) - assert.Equal(t, idx.NewRange(0, 19, 0, 23), e.GetIdRange()) + assert.Equal(t, idx.NewRange(3, 20, 3, 24), e.GetIdRange()) e = enum.GetEnumerator("GREEN") assert.Equal(t, "GREEN", e.GetName()) - assert.Equal(t, idx.NewRange(0, 25, 0, 30), e.GetIdRange()) + assert.Equal(t, idx.NewRange(3, 26, 3, 31), e.GetIdRange()) }) } func TestParse_fault(t *testing.T) { docId := "doc" - source := `fault IOResult + source := `<* docs *> + fault IOResult { IO_ERROR, PARSE_ERROR @@ -169,8 +192,16 @@ func TestParse_fault(t *testing.T) { symbols, _ := parser.ParseSymbols(&doc) found := symbols.Get("doc").Faults["IOResult"] - assert.Equal(t, idx.NewRange(0, 0, 4, 2), found.GetDocumentRange(), "Wrong document rage") - assert.Equal(t, idx.NewRange(0, 6, 0, 14), found.GetIdRange(), "Wrong identifier range") + assert.Equal(t, idx.NewRange(1, 1, 5, 2), found.GetDocumentRange(), "Wrong document rage") + assert.Equal(t, idx.NewRange(1, 7, 1, 15), found.GetIdRange(), "Wrong identifier range") + }) + + t.Run("finds doc comment", func(t *testing.T) { + symbols, _ := parser.ParseSymbols(&doc) + + fault := symbols.Get("doc").Faults["IOResult"] + + assert.Equal(t, "docs", fault.GetDocComment()) }) t.Run("finds defined fault constants", func(t *testing.T) { @@ -179,12 +210,12 @@ func TestParse_fault(t *testing.T) { fault := symbols.Get("doc").Faults["IOResult"] e := fault.GetConstant("IO_ERROR") assert.Equal(t, "IO_ERROR", e.GetName()) - assert.Equal(t, idx.NewRange(2, 3, 2, 11), e.GetIdRange()) + assert.Equal(t, idx.NewRange(3, 3, 3, 11), e.GetIdRange()) assert.Same(t, fault.Children()[0], e) e = fault.GetConstant("PARSE_ERROR") assert.Equal(t, "PARSE_ERROR", e.GetName()) - assert.Equal(t, idx.NewRange(3, 3, 3, 14), e.GetIdRange()) + assert.Equal(t, idx.NewRange(4, 3, 4, 14), e.GetIdRange()) assert.Same(t, fault.Children()[1], e) }) } @@ -192,7 +223,8 @@ func TestParse_fault(t *testing.T) { func TestParse_interface(t *testing.T) { module := "x" docId := "doc" - source := `interface MyName + source := `<* docs *> + interface MyName { fn String method(); };` @@ -218,8 +250,15 @@ func TestParse_interface(t *testing.T) { symbols, _ := parser.ParseSymbols(&doc) found := symbols.Get("doc").Interfaces["MyName"] - assert.Equal(t, idx.NewRange(0, 0, 3, 2), found.GetDocumentRange(), "Wrong document rage") - assert.Equal(t, idx.NewRange(0, 10, 0, 16), found.GetIdRange(), "Wrong identifier range") + assert.Equal(t, idx.NewRange(1, 1, 4, 2), found.GetDocumentRange(), "Wrong document rage") + assert.Equal(t, idx.NewRange(1, 11, 1, 17), found.GetIdRange(), "Wrong identifier range") + }) + + t.Run("finds doc comment", func(t *testing.T) { + symbols, _ := parser.ParseSymbols(&doc) + + found := symbols.Get("doc").Interfaces["MyName"] + assert.Equal(t, "docs", found.GetDocComment()) }) t.Run("finds defined methods in interface", func(t *testing.T) { @@ -230,13 +269,14 @@ func TestParse_interface(t *testing.T) { m := _interface.GetMethod("method") assert.Equal(t, "method", m.GetName()) assert.Equal(t, "String", m.GetReturnType().GetName()) - assert.Equal(t, idx.NewRange(2, 12, 2, 18), m.GetIdRange()) + assert.Equal(t, idx.NewRange(3, 12, 3, 18), m.GetIdRange()) assert.Equal(t, module.Children()[0], _interface) }) } func TestExtractSymbols_finds_definition(t *testing.T) { source := `module mod; + <* docs *> def Kilo = int; def KiloPtr = Kilo*; def MyFunction = fn void (Allocator*, JSONRPCRequest*, JSONRPCResponse*); @@ -255,9 +295,10 @@ func TestExtractSymbols_finds_definition(t *testing.T) { WithResolvesToType( idx.NewType(true, "int", 0, false, false, option.None[int](), "mod"), ). - WithIdentifierRange(1, 5, 1, 9). - WithDocumentRange(1, 1, 1, 16). + WithIdentifierRange(2, 5, 2, 9). + WithDocumentRange(2, 1, 2, 16). Build() + expectedDefKilo.SetDocComment("docs") assert.Equal(t, expectedDefKilo, module.Defs["Kilo"]) assert.Same(t, module.Children()[0], module.Defs["Kilo"]) @@ -265,16 +306,16 @@ func TestExtractSymbols_finds_definition(t *testing.T) { WithResolvesToType( idx.NewType(false, "Kilo", 1, false, false, option.None[int](), "mod"), ). - WithIdentifierRange(2, 5, 2, 12). - WithDocumentRange(2, 1, 2, 21). + WithIdentifierRange(3, 5, 3, 12). + WithDocumentRange(3, 1, 3, 21). Build() assert.Equal(t, expectedDefKiloPtr, module.Defs["KiloPtr"]) assert.Same(t, module.Children()[1], module.Defs["KiloPtr"]) expectedDefFunction := idx.NewDefBuilder("MyFunction", mod, doc.URI). WithResolvesTo("fn void (Allocator*, JSONRPCRequest*, JSONRPCResponse*)"). - WithIdentifierRange(3, 5, 3, 15). - WithDocumentRange(3, 1, 3, 74). + WithIdentifierRange(4, 5, 4, 15). + WithDocumentRange(4, 1, 4, 74). Build() assert.Equal(t, expectedDefFunction, module.Defs["MyFunction"]) @@ -292,8 +333,8 @@ func TestExtractSymbols_finds_definition(t *testing.T) { idx.NewType(false, "Feature", 0, false, false, option.None[int](), "mod"), }, "mod"), ). - WithIdentifierRange(4, 5, 4, 10). - WithDocumentRange(4, 1, 4, 40). + WithIdentifierRange(5, 5, 5, 10). + WithDocumentRange(5, 1, 5, 40). Build() assert.Equal(t, expectedDefTypeWithGenerics, module.Defs["MyMap"]) @@ -303,8 +344,8 @@ func TestExtractSymbols_finds_definition(t *testing.T) { WithResolvesToType( idx.NewType(false, "Camera", 0, false, false, option.None[int](), "raylib"), ). - WithIdentifierRange(5, 5, 5, 11). - WithDocumentRange(5, 1, 5, 29). + WithIdentifierRange(6, 5, 6, 11). + WithDocumentRange(6, 1, 6, 29). Build() assert.Equal(t, expectedDefTypeWithModulePath, module.Defs["Camera"]) @@ -321,6 +362,7 @@ func TestExtractSymbols_find_macro(t *testing.T) { } }`*/ source := ` + <* docs *> macro m(x) { return x + 2; }` @@ -335,6 +377,7 @@ func TestExtractSymbols_find_macro(t *testing.T) { assert.Equal(t, "m", fn.Get().GetName()) assert.Equal(t, "x", fn.Get().Variables["x"].GetName()) assert.Equal(t, "", fn.Get().Variables["x"].GetType().String()) + assert.Equal(t, "docs", fn.Get().GetDocComment()) assert.Same(t, module.NestedScopes()[0], fn.Get()) } @@ -352,6 +395,7 @@ func TestExtractSymbols_find_module(t *testing.T) { t.Run("finds single module in single file", func(t *testing.T) { source := ` + <* docs *> module foo; int value = 1; ` @@ -362,13 +406,16 @@ func TestExtractSymbols_find_module(t *testing.T) { module := symbols.Get("foo") assert.Equal(t, "foo", module.GetModuleString(), "module name is wrong") + assert.Equal(t, "docs", module.GetDocComment(), "module doc comment is wrong") }) t.Run("finds different modules defined in single file", func(t *testing.T) { source := ` + <* docs foo *> module foo; int value = 1; + <* docs foo2 *> module foo2; int value = 2;` @@ -379,12 +426,14 @@ func TestExtractSymbols_find_module(t *testing.T) { module := symbols.Get("foo") assert.Equal(t, "foo", module.GetModuleString(), "module name is wrong") assert.Equal(t, "foo", module.GetName(), "module name is wrong") - assert.Equal(t, idx.NewRange(1, 1, 2, 15), module.GetDocumentRange(), "Wrong range for foo module") + assert.Equal(t, "docs foo", module.GetDocComment(), "module doc comment is wrong") + assert.Equal(t, idx.NewRange(2, 1, 3, 15), module.GetDocumentRange(), "Wrong range for foo module") module = symbols.Get("foo2") assert.Equal(t, "foo2", module.GetModuleString(), "module name is wrong") assert.Equal(t, "foo2", module.GetName(), "module name is wrong") - assert.Equal(t, idx.NewRange(4, 1, 5, 15), module.GetDocumentRange(), "Wrong range for foo2 module") + assert.Equal(t, "docs foo2", module.GetDocComment(), "module doc comment is wrong") + assert.Equal(t, idx.NewRange(6, 1, 7, 15), module.GetDocumentRange(), "Wrong range for foo2 module") }) t.Run("finds named module with attributes", func(t *testing.T) { @@ -428,7 +477,7 @@ func TestExtractSymbols_module_with_generics(t *testing.T) { { return foo.a + b; } - + module foo::another::deep(); int bar = 0;` diff --git a/server/pkg/parser/parser_variables_test.go b/server/pkg/parser/parser_variables_test.go index ea6db555..4f4d7362 100644 --- a/server/pkg/parser/parser_variables_test.go +++ b/server/pkg/parser/parser_variables_test.go @@ -15,8 +15,10 @@ func assertVariableFound(t *testing.T, name string, symbols idx.Function) { func TestExtractSymbols_find_variables(t *testing.T) { source := ` + <* docs *> int value = 1; char* character; + <* multidocs *> int foo, foo2; char[] message; char[4] message2; @@ -34,13 +36,14 @@ func TestExtractSymbols_find_variables(t *testing.T) { assert.Equal(t, "value", found.GetName(), "Variable name") assert.Equal(t, "int", found.GetType().String(), "Variable type") assert.Equal(t, true, found.GetType().IsBaseTypeLanguage(), "Variable Type should be base type") - assert.Equal(t, idx.NewRange(1, 1, 1, 15), found.GetDocumentRange()) - assert.Equal(t, idx.NewRange(1, 5, 1, 10), found.GetIdRange()) + assert.Equal(t, idx.NewRange(2, 1, 2, 15), found.GetDocumentRange()) + assert.Equal(t, idx.NewRange(2, 5, 2, 10), found.GetIdRange()) + assert.Equal(t, "docs", found.GetDocComment(), "Variable docs") assert.Equal(t, 0, len(pendingToResolve.GetTypesByModule(docId)), "Basic types should not be registered as pending to resolve.") }) t.Run("finds global pointer variable declarations", func(t *testing.T) { - line := uint(2) + line := uint(3) symbols, _ := parser.ParseSymbols(&doc) found := symbols.Get("x").Variables["character"] @@ -52,7 +55,7 @@ func TestExtractSymbols_find_variables(t *testing.T) { }) t.Run("finds global variable collection declarations", func(t *testing.T) { - line := uint(4) + line := uint(6) symbols, _ := parser.ParseSymbols(&doc) found := symbols.Get("x").Variables["message"] @@ -64,7 +67,7 @@ func TestExtractSymbols_find_variables(t *testing.T) { }) t.Run("finds global variable static collection declarations", func(t *testing.T) { - line := uint(5) + line := uint(7) symbols, _ := parser.ParseSymbols(&doc) found := symbols.Get("x").Variables["message2"] @@ -76,7 +79,7 @@ func TestExtractSymbols_find_variables(t *testing.T) { }) t.Run("finds multiple global variables declared in single sentence", func(t *testing.T) { - line := uint(3) + line := uint(5) symbols, _ := parser.ParseSymbols(&doc) found := symbols.Get("x").Variables["foo"] @@ -85,6 +88,7 @@ func TestExtractSymbols_find_variables(t *testing.T) { assert.Equal(t, true, found.GetType().IsBaseTypeLanguage(), "Variable Type should be base type") assert.Equal(t, idx.NewRange(line, 5, line, 8), found.GetIdRange(), "First variable identifier range") assert.Equal(t, idx.NewRange(line, 1, line, 15), found.GetDocumentRange(), "First variable declaration range") + assert.Equal(t, "multidocs", found.GetDocComment()) found = symbols.Get("x").Variables["foo2"] assert.Equal(t, "foo2", found.GetName(), "Second variable name") @@ -92,10 +96,11 @@ func TestExtractSymbols_find_variables(t *testing.T) { assert.Equal(t, true, found.GetType().IsBaseTypeLanguage(), "Variable Type should be base type") assert.Equal(t, idx.NewRange(line, 10, line, 14), found.GetIdRange(), "Second variable identifier range") assert.Equal(t, idx.NewRange(line, 1, line, 15), found.GetDocumentRange(), "Second variable declaration range") + assert.Equal(t, "multidocs", found.GetDocComment()) }) t.Run("finds variables declared inside function", func(t *testing.T) { - line := uint(6) + line := uint(8) symbols, _ := parser.ParseSymbols(&doc) function := symbols.Get("x").GetChildrenFunctionByName("test") @@ -110,7 +115,7 @@ func TestExtractSymbols_find_variables(t *testing.T) { }) t.Run("finds multiple local variables declared in single sentence", func(t *testing.T) { - line := uint(7) + line := uint(9) symbols, _ := parser.ParseSymbols(&doc) function := symbols.Get("x").GetChildrenFunctionByName("test2") @@ -133,7 +138,8 @@ func TestExtractSymbols_find_variables(t *testing.T) { func TestExtractSymbols_find_constants(t *testing.T) { - source := `const int A_VALUE = 12;` + source := `<* docs *> + const int A_VALUE = 12;` doc := document.NewDocument("docId", source) parser := createParser() @@ -144,8 +150,9 @@ func TestExtractSymbols_find_constants(t *testing.T) { assert.Equal(t, "A_VALUE", found.GetName(), "Variable name") assert.Equal(t, "int", found.GetType().String(), "Variable type") assert.True(t, found.IsConstant()) - assert.Equal(t, idx.NewRange(0, 0, 0, 23), found.GetDocumentRange()) - assert.Equal(t, idx.NewRange(0, 10, 0, 17), found.GetIdRange()) + assert.Equal(t, idx.NewRange(1, 1, 1, 24), found.GetDocumentRange()) + assert.Equal(t, idx.NewRange(1, 11, 1, 18), found.GetIdRange()) + assert.Equal(t, "docs", found.GetDocComment(), "Variable doc comment") } func TestExtractSymbols_find_variables_flag_pending_to_resolve(t *testing.T) { diff --git a/server/pkg/symbols/indexable.go b/server/pkg/symbols/indexable.go index f3a50393..d8f0e5d4 100644 --- a/server/pkg/symbols/indexable.go +++ b/server/pkg/symbols/indexable.go @@ -33,6 +33,7 @@ type Indexable interface { GetModule() ModulePath IsSubModuleOf(parentModule ModulePath) bool + GetDocComment() string GetHoverInfo() string HasSourceCode() bool // This will return false for that code that is not accesible either because it belongs to the stdlib, or inside a .c3lib library. This results in disabling "Go to definition" / "Go to declaration" on these symbols @@ -59,6 +60,7 @@ type BaseIndexable struct { idRange Range docRange Range Kind protocol.CompletionItemKind + docComment string attributes []string children []Indexable @@ -123,6 +125,10 @@ func (b *BaseIndexable) SetDocumentURI(docId string) { b.documentURI = docId } +func (b *BaseIndexable) GetDocComment() string { + return b.docComment +} + func (b *BaseIndexable) GetAttributes() []string { return b.attributes } @@ -156,6 +162,10 @@ func (b *BaseIndexable) InsertNestedScope(symbol Indexable) { b.nestedScopes = append(b.nestedScopes, symbol) } +func (b *BaseIndexable) SetDocComment(docComment string) { + b.docComment = docComment +} + func (b *BaseIndexable) formatSource(source string) string { return fmt.Sprintf("```c3\n%s```", source) } @@ -170,6 +180,7 @@ func NewBaseIndexable(name string, module string, docId protocol.DocumentUri, id docRange: docRange, Kind: kind, hasSourceCode: true, + docComment: "", attributes: []string{}, } } From 251626812602b5a1e68d4e23af2cfc28835b974b Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Fri, 24 Jan 2025 19:20:55 -0300 Subject: [PATCH 2/8] initial attempt at hover docs --- server/internal/lsp/server/TextDocumentHover.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/server/internal/lsp/server/TextDocumentHover.go b/server/internal/lsp/server/TextDocumentHover.go index a117ad8b..e60108c2 100644 --- a/server/internal/lsp/server/TextDocumentHover.go +++ b/server/internal/lsp/server/TextDocumentHover.go @@ -22,9 +22,14 @@ func (h *Server) TextDocumentHover(context *glsp.Context, params *protocol.Hover // expected behaviour: // hovering on variables: display variable type + any description - // hovering on functions: display function signature + // hovering on functions: display function signature + docs // hovering on members: same as variable + docComment := foundSymbol.GetDocComment() + if docComment != "" { + docComment = "\n\n" + docComment + } + extraLine := "" _, isModule := foundSymbol.(*symbols.Module) @@ -45,7 +50,8 @@ func (h *Server) TextDocumentHover(context *glsp.Context, params *protocol.Hover Value: "```c3" + "\n" + sizeInfo + foundSymbol.GetHoverInfo() + "\n```" + - extraLine, + extraLine + + docComment, }, } From 51f5a2fb3175a66dad9e996fbe07d844442d750b Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Sat, 25 Jan 2025 01:43:01 -0300 Subject: [PATCH 3/8] add documentation to completion --- .../lsp/search/search_completion_list.go | 27 ++- .../lsp/search/search_completion_list_test.go | 206 ++++++++++++++++-- 2 files changed, 207 insertions(+), 26 deletions(-) diff --git a/server/internal/lsp/search/search_completion_list.go b/server/internal/lsp/search/search_completion_list.go index ad437944..b6e660e7 100644 --- a/server/internal/lsp/search/search_completion_list.go +++ b/server/internal/lsp/search/search_completion_list.go @@ -127,6 +127,17 @@ func extractExplicitModulePath(possibleModulePath string) option.Option[symbols. return option.None[symbols.ModulePath]() } +// TODO: Consider only returning the body text, not contracts here +// Returns: nil | string +func GetCompletableDocComment(s symbols.Indexable) any { + docComment := s.GetDocComment() + if docComment == "" { + return nil + } else { + return docComment + } +} + // Returns: []CompletionItem | CompletionList | nil func (s *Search) BuildCompletionList( ctx context.CursorContext, @@ -222,6 +233,9 @@ func (s *Search) BuildCompletionList( items = append(items, protocol.CompletionItem{ Label: member.GetName(), Kind: &member.Kind, + + // At this moment, struct members cannot receive documentation + Documentation: nil, }) } } @@ -252,6 +266,7 @@ func (s *Search) BuildCompletionList( NewText: fn.GetMethodName(), Range: replacementRange, }, + Documentation: GetCompletableDocComment(fn), }) } @@ -262,6 +277,9 @@ func (s *Search) BuildCompletionList( items = append(items, protocol.CompletionItem{ Label: enumerator.GetName(), Kind: &enumerator.Kind, + + // No documentation for enumerators at this time + Documentation: nil, }) } } @@ -273,6 +291,9 @@ func (s *Search) BuildCompletionList( items = append(items, protocol.CompletionItem{ Label: constant.GetName(), Kind: &constant.Kind, + + // No documentation for fault constants at this time + Documentation: nil, }) } } @@ -310,11 +331,13 @@ func (s *Search) BuildCompletionList( NewText: storedIdentifier.GetName(), Range: editRange, }, + Documentation: GetCompletableDocComment(storedIdentifier), }) } else { items = append(items, protocol.CompletionItem{ - Label: storedIdentifier.GetName(), - Kind: cast.ToPtr(storedIdentifier.GetKind()), + Label: storedIdentifier.GetName(), + Kind: cast.ToPtr(storedIdentifier.GetKind()), + Documentation: GetCompletableDocComment(storedIdentifier), }) } } diff --git a/server/internal/lsp/search/search_completion_list_test.go b/server/internal/lsp/search/search_completion_list_test.go index ecec5dc4..ccaf716b 100644 --- a/server/internal/lsp/search/search_completion_list_test.go +++ b/server/internal/lsp/search/search_completion_list_test.go @@ -214,7 +214,7 @@ func TestBuildCompletionList_should_return_nil_when_cursor_is_in_literal(t *test search := NewSearchWithoutLog() state.registerDoc( "test.c3", - `module foo; + `module foo; printf("main.");`, ) @@ -340,15 +340,22 @@ func TestBuildCompletionList(t *testing.T) { t.Run("Should suggest variable names defined in module", func(t *testing.T) { source := ` int variable = 3; - int xanadu = 10;` - expectedKind := protocol.CompletionItemKindVariable + int xanadu = 10; + <* doc *> + int documented = 50; + <* const doc *> + const int MY_CONST = 100;` + expectedVarKind := protocol.CompletionItemKindVariable + expectedConstKind := protocol.CompletionItemKindConstant cases := []struct { input string expected protocol.CompletionItem }{ - {"v", protocol.CompletionItem{Label: "variable", Kind: &expectedKind}}, - {"va", protocol.CompletionItem{Label: "variable", Kind: &expectedKind}}, - {"x", protocol.CompletionItem{Label: "xanadu", Kind: &expectedKind}}, + {"v", protocol.CompletionItem{Label: "variable", Kind: &expectedVarKind}}, + {"va", protocol.CompletionItem{Label: "variable", Kind: &expectedVarKind}}, + {"x", protocol.CompletionItem{Label: "xanadu", Kind: &expectedVarKind}}, + {"docu", protocol.CompletionItem{Label: "documented", Kind: &expectedVarKind, Documentation: "doc"}}, + {"MY_C", protocol.CompletionItem{Label: "MY_CONST", Kind: &expectedConstKind, Documentation: "const doc"}}, } for n, tt := range cases { @@ -358,7 +365,7 @@ func TestBuildCompletionList(t *testing.T) { source+"\n"+tt.input, ) - position := buildPosition(4, 1) // Cursor after `v|` + position := buildPosition(8, 1) // Cursor after `v|` completionList := search.BuildCompletionList( context.CursorContext{ @@ -430,10 +437,10 @@ func TestBuildCompletionList(t *testing.T) { expected []protocol.CompletionItem }{ {"p", []protocol.CompletionItem{ - {Label: "process", Kind: &expectedKind}, + {Label: "process", Kind: &expectedKind, Documentation: nil}, }}, {"proc", []protocol.CompletionItem{ - {Label: "process", Kind: &expectedKind}, + {Label: "process", Kind: &expectedKind, Documentation: nil}, }}, } @@ -457,6 +464,144 @@ func TestBuildCompletionList(t *testing.T) { }) } }) + + t.Run("Should suggest function names with documentation", func(t *testing.T) { + sourceStart := ` + <* abc *> + fn void process(){} + fn void main() {` + sourceEnd := ` + }` + + expectedKind := protocol.CompletionItemKindFunction + cases := []struct { + input string + expected []protocol.CompletionItem + }{ + {"p", []protocol.CompletionItem{ + {Label: "process", Kind: &expectedKind, Documentation: "abc"}, + }}, + {"proc", []protocol.CompletionItem{ + {Label: "process", Kind: &expectedKind, Documentation: "abc"}, + }}, + } + + for n, tt := range cases { + t.Run(fmt.Sprintf("Case #%d", n), func(t *testing.T) { + state.registerDoc( + "test.c3", + sourceStart+"\n"+tt.input+"\n"+sourceEnd, + ) + position := buildPosition(5, uint(len(tt.input))) // Cursor after `|` + + completionList := search.BuildCompletionList( + context.CursorContext{ + Position: position, + DocURI: "test.c3", + }, + &state.state) + + assert.Equal(t, len(tt.expected), len(completionList)) + assert.Equal(t, tt.expected, completionList) + }) + } + }) + + t.Run("Should suggest function names with contracts in documentation", func(t *testing.T) { + sourceStart := ` + <* + abc + + @param [in] a + @require a > 0, a < 1000 : "woah" + @ensure return > 1 + *> + fn int process(int a){ return 5; } + fn void main() {` + sourceEnd := ` + }` + + expectedDoc := `abc +@param [in] a +@require a > 0, a < 1000 : "woah" +@ensure return > 1` + + expectedKind := protocol.CompletionItemKindFunction + cases := []struct { + input string + expected []protocol.CompletionItem + }{ + {"p", []protocol.CompletionItem{ + {Label: "process", Kind: &expectedKind, Documentation: expectedDoc}, + }}, + {"proc", []protocol.CompletionItem{ + {Label: "process", Kind: &expectedKind, Documentation: expectedDoc}, + }}, + } + + for n, tt := range cases { + t.Run(fmt.Sprintf("Case #%d", n), func(t *testing.T) { + state.registerDoc( + "test.c3", + sourceStart+"\n"+tt.input+"\n"+sourceEnd, + ) + position := buildPosition(11, uint(len(tt.input))) // Cursor after `|` + + completionList := search.BuildCompletionList( + context.CursorContext{ + Position: position, + DocURI: "test.c3", + }, + &state.state) + + assert.Equal(t, len(tt.expected), len(completionList)) + assert.Equal(t, tt.expected, completionList) + }) + } + }) +} + +func TestBuildCompletionList_struct_type(t *testing.T) { + commonlog.Configure(2, nil) + logger := commonlog.GetLogger("C3-LSP.parser") + + source := ` + struct Cough { int a; } + <* doc *> + struct Color { int r; int g; int b; } +` + cases := []struct { + input string + expected []protocol.CompletionItem + }{ + {"Co", []protocol.CompletionItem{ + CreateCompletionItemWithDoc("Color", protocol.CompletionItemKindStruct, "doc"), + CreateCompletionItem("Cough", protocol.CompletionItemKindStruct), + }}, + {"Col", []protocol.CompletionItem{ + CreateCompletionItemWithDoc("Color", protocol.CompletionItemKindStruct, "doc"), + }}, + } + + for n, tt := range cases { + t.Run(fmt.Sprintf("Case #%d", n), func(t *testing.T) { + state := NewTestState(logger) + state.registerDoc("test.c3", source+tt.input+`}`) + + position := buildPosition(5, uint(len(tt.input))) // Cursor after `|` + + search := NewSearchWithoutLog() + completionList := search.BuildCompletionList( + context.CursorContext{ + Position: position, + DocURI: "test.c3", + }, + &state.state) + + assert.Equal(t, len(tt.expected), len(completionList)) + assert.Equal(t, tt.expected, completionList) + }) + } } func TestBuildCompletionList_struct_suggest_all_its_members(t *testing.T) { @@ -466,12 +611,13 @@ func TestBuildCompletionList_struct_suggest_all_its_members(t *testing.T) { source := `struct Color { int red; int green; int blue; } struct Square { int width; int height; Color color; } + <* member doc *> fn void Square.toCircle() {} fn void main() { Square inst; inst. }` - position := buildPosition(6, 7) // Cursor after `inst.|` + position := buildPosition(7, 7) // Cursor after `inst.|` state := NewTestState(logger) state.registerDoc("test.c3", source) @@ -493,8 +639,9 @@ func TestBuildCompletionList_struct_suggest_all_its_members(t *testing.T) { Kind: cast.ToPtr(protocol.CompletionItemKindMethod), TextEdit: protocol.TextEdit{ NewText: "toCircle", - Range: protocol_utils.NewLSPRange(5, 7, 5, 8), + Range: protocol_utils.NewLSPRange(6, 7, 6, 8), }, + Documentation: "member doc", }, {Label: "width", Kind: &expectedKind}, }, completionList) @@ -656,6 +803,7 @@ func TestBuildCompletionList_enums(t *testing.T) { t.Run("Should suggest Enum type", func(t *testing.T) { source := ` enum Cough { COH, COUGH, COUGHCOUGH} + <* doc *> enum Color { RED, GREEN, BLUE } ` cases := []struct { @@ -663,11 +811,11 @@ func TestBuildCompletionList_enums(t *testing.T) { expected []protocol.CompletionItem }{ {"Co", []protocol.CompletionItem{ - CreateCompletionItem("Color", protocol.CompletionItemKindEnum), + CreateCompletionItemWithDoc("Color", protocol.CompletionItemKindEnum, "doc"), CreateCompletionItem("Cough", protocol.CompletionItemKindEnum), }}, {"Col", []protocol.CompletionItem{ - CreateCompletionItem("Color", protocol.CompletionItemKindEnum), + CreateCompletionItemWithDoc("Color", protocol.CompletionItemKindEnum, "doc"), }}, } @@ -676,7 +824,7 @@ func TestBuildCompletionList_enums(t *testing.T) { state := NewTestState(logger) state.registerDoc("test.c3", source+tt.input+`}`) - position := buildPosition(4, uint(len(tt.input))) // Cursor after `|` + position := buildPosition(5, uint(len(tt.input))) // Cursor after `|` search := NewSearchWithoutLog() completionList := search.BuildCompletionList( @@ -756,6 +904,7 @@ func TestBuildCompletionList_faults(t *testing.T) { t.Run("Should suggest Fault type", func(t *testing.T) { source := ` fault WindowError { COH, COUGH, COUGHCOUGH} + <* doc *> fault WindowFileError { NOT_FOUND, NO_PERMISSIONS } ` cases := []struct { @@ -764,10 +913,10 @@ func TestBuildCompletionList_faults(t *testing.T) { }{ {"Wind", []protocol.CompletionItem{ CreateCompletionItem("WindowError", protocol.CompletionItemKindEnum), - CreateCompletionItem("WindowFileError", protocol.CompletionItemKindEnum), + CreateCompletionItemWithDoc("WindowFileError", protocol.CompletionItemKindEnum, "doc"), }}, {"WindowFile", []protocol.CompletionItem{ - CreateCompletionItem("WindowFileError", protocol.CompletionItemKindEnum), + CreateCompletionItemWithDoc("WindowFileError", protocol.CompletionItemKindEnum, "doc"), }}, } @@ -775,7 +924,7 @@ func TestBuildCompletionList_faults(t *testing.T) { t.Run(fmt.Sprintf("Case #%d", n), func(t *testing.T) { state := NewTestState() state.registerDoc("test.c3", source+tt.input+`}`) - position := buildPosition(4, uint(len(tt.input))) // Cursor after `|` + position := buildPosition(5, uint(len(tt.input))) // Cursor after `|` search := NewSearchWithoutLog() completionList := search.BuildCompletionList( @@ -863,11 +1012,12 @@ func TestBuildCompletionList_modules(t *testing.T) { }{ { ` + <* doc *> module app; int version = 1; a `, - buildPosition(4, 5), // Cursor at `a|` + buildPosition(5, 5), // Cursor at `a|` []protocol.CompletionItem{{ Label: "app", Kind: cast.ToPtr(protocol.CompletionItemKindModule), @@ -876,6 +1026,7 @@ func TestBuildCompletionList_modules(t *testing.T) { NewText: "app", Range: protocol_utils.NewLSPRange(3, 4, 3, 5), }, + Documentation: "doc", }, }, true, @@ -988,7 +1139,7 @@ func TestBuildCompletionList_modules(t *testing.T) { module app; int version = 1; module app::foo; - + app::`, buildPosition(6, 9), // Cursor at `a|` []protocol.CompletionItem{ @@ -1059,7 +1210,9 @@ func TestBuildCompletionList_interfaces(t *testing.T) { state := NewTestState() state.registerDoc( "app.c3", - `interface EmulatorConsole + ` + <* doc *> + interface EmulatorConsole { fn void run(); } @@ -1068,7 +1221,7 @@ func TestBuildCompletionList_interfaces(t *testing.T) { search := NewSearchWithoutLog() completionList := search.BuildCompletionList( context.CursorContext{ - Position: buildPosition(5, 18), + Position: buildPosition(7, 18), DocURI: "app.c3", }, &state.state) @@ -1078,8 +1231,9 @@ func TestBuildCompletionList_interfaces(t *testing.T) { t, []protocol.CompletionItem{ { - Label: "EmulatorConsole", - Kind: cast.ToPtr(protocol.CompletionItemKindInterface), + Label: "EmulatorConsole", + Kind: cast.ToPtr(protocol.CompletionItemKindInterface), + Documentation: "doc", }, }, completionList, @@ -1088,7 +1242,11 @@ func TestBuildCompletionList_interfaces(t *testing.T) { } func CreateCompletionItem(label string, kind protocol.CompletionItemKind) protocol.CompletionItem { - return protocol.CompletionItem{Label: label, Kind: &kind} + return protocol.CompletionItem{Label: label, Kind: &kind, Documentation: nil} +} + +func CreateCompletionItemWithDoc(label string, kind protocol.CompletionItemKind, doc string) protocol.CompletionItem { + return protocol.CompletionItem{Label: label, Kind: &kind, Documentation: doc} } func TestBuildCompletionList_should_resolve_(t *testing.T) { From 4c993ec7578c1b4a516a9cde0ffb789d561a28e2 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Sat, 25 Jan 2025 12:43:22 -0300 Subject: [PATCH 4/8] create doc comment structure --- .../lsp/search/search_completion_list.go | 5 +- .../lsp/search/search_completion_list_test.go | 6 +- .../internal/lsp/server/TextDocumentHover.go | 7 ++- server/pkg/parser/node_to_doc_comment.go | 61 +++++++++++++++++++ server/pkg/parser/parse_structs_test.go | 8 +-- server/pkg/parser/parser.go | 46 +++++--------- server/pkg/parser/parser_functions_test.go | 23 ++++--- server/pkg/parser/parser_test.go | 19 +++--- server/pkg/parser/parser_variables_test.go | 8 +-- server/pkg/symbols/doc_comment.go | 51 ++++++++++++++++ server/pkg/symbols/indexable.go | 10 +-- 11 files changed, 175 insertions(+), 69 deletions(-) create mode 100644 server/pkg/parser/node_to_doc_comment.go create mode 100644 server/pkg/symbols/doc_comment.go diff --git a/server/internal/lsp/search/search_completion_list.go b/server/internal/lsp/search/search_completion_list.go index b6e660e7..269a8164 100644 --- a/server/internal/lsp/search/search_completion_list.go +++ b/server/internal/lsp/search/search_completion_list.go @@ -131,10 +131,11 @@ func extractExplicitModulePath(possibleModulePath string) option.Option[symbols. // Returns: nil | string func GetCompletableDocComment(s symbols.Indexable) any { docComment := s.GetDocComment() - if docComment == "" { + if docComment == nil || docComment.GetBody() == "" { return nil } else { - return docComment + // Don't include contract information in completion, for brevity + return docComment.GetBody() } } diff --git a/server/internal/lsp/search/search_completion_list_test.go b/server/internal/lsp/search/search_completion_list_test.go index ccaf716b..3d1032db 100644 --- a/server/internal/lsp/search/search_completion_list_test.go +++ b/server/internal/lsp/search/search_completion_list_test.go @@ -521,10 +521,8 @@ func TestBuildCompletionList(t *testing.T) { sourceEnd := ` }` - expectedDoc := `abc -@param [in] a -@require a > 0, a < 1000 : "woah" -@ensure return > 1` + // Contracts are excluded + expectedDoc := "abc" expectedKind := protocol.CompletionItemKindFunction cases := []struct { diff --git a/server/internal/lsp/server/TextDocumentHover.go b/server/internal/lsp/server/TextDocumentHover.go index e60108c2..c822da83 100644 --- a/server/internal/lsp/server/TextDocumentHover.go +++ b/server/internal/lsp/server/TextDocumentHover.go @@ -25,9 +25,10 @@ func (h *Server) TextDocumentHover(context *glsp.Context, params *protocol.Hover // hovering on functions: display function signature + docs // hovering on members: same as variable - docComment := foundSymbol.GetDocComment() - if docComment != "" { - docComment = "\n\n" + docComment + docCommentData := foundSymbol.GetDocComment() + docComment := "" + if docCommentData != nil { + docComment = "\n\n" + docCommentData.DisplayBodyWithContracts() } extraLine := "" diff --git a/server/pkg/parser/node_to_doc_comment.go b/server/pkg/parser/node_to_doc_comment.go new file mode 100644 index 00000000..79f1672f --- /dev/null +++ b/server/pkg/parser/node_to_doc_comment.go @@ -0,0 +1,61 @@ +package parser + +import ( + "github.com/pherrymason/c3-lsp/pkg/cast" + idx "github.com/pherrymason/c3-lsp/pkg/symbols" + sitter "github.com/smacker/go-tree-sitter" +) + +/* +// From grammar.js in tree-sitter-c3 v0.2.3: + +// Doc comments and contracts +// ------------------------- +// NOTE parsed by scanner.c (scan_doc_comment_contract_text) +doc_comment_contract: $ => seq( + + field('name', $.at_ident), + optional($.doc_comment_contract_text) + +), +doc_comment: $ => seq( + + '<*', + optional($.doc_comment_text), // NOTE parsed by scanner.c (scan_doc_comment_text) + repeat($.doc_comment_contract), + '*>', + +), + +// (...) + +at_ident: _ => token(seq('@', IDENT)), +*/ +func (p *Parser) nodeToDocComment(node *sitter.Node, sourceCode []byte) idx.DocComment { + body := "" + bodyNode := node.Child(1) + if bodyNode.Type() == "doc_comment_text" { + body = bodyNode.Content(sourceCode) + } + + docComment := idx.NewDocComment(body) + + if node.ChildCount() >= 4 { + for i := 2; i <= int(node.ChildCount())-2; i++ { + contractNode := node.Child(i) + if contractNode.Type() == "doc_comment_contract" { + name := contractNode.ChildByFieldName("name").Content(sourceCode) + body := "" + if contractNode.ChildCount() >= 2 { + body = contractNode.Child(1).Content(sourceCode) + } + + contract := idx.NewDocCommentContract(name, body) + + docComment.AddContracts([]*idx.DocCommentContract{cast.ToPtr(contract)}) + } + } + } + + return docComment +} diff --git a/server/pkg/parser/parse_structs_test.go b/server/pkg/parser/parse_structs_test.go index 64148c3a..4a0db018 100644 --- a/server/pkg/parser/parse_structs_test.go +++ b/server/pkg/parser/parse_structs_test.go @@ -41,7 +41,7 @@ fn void MyStruct.init(&self) assert.False(t, found.IsUnion()) assert.Equal(t, idx.NewRange(4, 0, 8, 1), found.GetDocumentRange()) assert.Equal(t, idx.NewRange(4, 7, 4, 15), found.GetIdRange()) - assert.Equal(t, "docs", found.GetDocComment()) + assert.Equal(t, "docs", found.GetDocComment().GetBody()) }) t.Run("finds struct members", func(t *testing.T) { @@ -160,7 +160,7 @@ struct Foo { assert.Same(t, found.Children()[0], member) // Docs here are invalid - assert.Equal(t, "", member.GetDocComment()) + assert.Nil(t, member.GetDocComment()) } func TestParses_anonymous_substructs(t *testing.T) { @@ -320,7 +320,7 @@ func TestParse_Unions(t *testing.T) { assert.True(t, found.IsUnion()) assert.Equal(t, idx.NewRange(2, 1, 5, 2), found.GetDocumentRange()) assert.Equal(t, idx.NewRange(2, 7, 2, 14), found.GetIdRange()) - assert.Equal(t, "docs", found.GetDocComment()) + assert.Equal(t, "docs", found.GetDocComment().GetBody()) assert.Same(t, module.Children()[0], found) }) } @@ -345,7 +345,7 @@ func TestParse_bitstructs(t *testing.T) { assert.Same(t, symbols.Get("x").Children()[0], found) assert.Equal(t, "Test", found.GetName()) assert.Equal(t, "uint", found.Type().GetName()) - assert.Equal(t, "docs", found.GetDocComment()) + assert.Equal(t, "docs", found.GetDocComment().GetBody()) members := found.Members() assert.Equal(t, 3, len(members)) diff --git a/server/pkg/parser/parser.go b/server/pkg/parser/parser.go index 60fdf475..5afcbb8a 100644 --- a/server/pkg/parser/parser.go +++ b/server/pkg/parser/parser.go @@ -2,6 +2,7 @@ package parser import ( "github.com/pherrymason/c3-lsp/internal/lsp/cst" + "github.com/pherrymason/c3-lsp/pkg/cast" "github.com/pherrymason/c3-lsp/pkg/document" idx "github.com/pherrymason/c3-lsp/pkg/symbols" "github.com/pherrymason/c3-lsp/pkg/symbols_table" @@ -87,7 +88,7 @@ func (p *Parser) ParseSymbols(doc *document.Document) (symbols_table.UnitModules var moduleSymbol *idx.Module anonymousModuleName := true lastModuleName := "" - lastDocComment := "" + var lastDocComment *idx.DocComment = nil //subtyptingToResolve := []StructWithSubtyping{} for { @@ -110,22 +111,7 @@ func (p *Parser) ParseSymbols(doc *document.Document) (symbols_table.UnitModules switch nodeType { case "doc_comment": - lastDocComment = "" - doc_text := c.Node.Child(1) - if doc_text.Type() == "doc_comment_text" { - lastDocComment = doc_text.Content(sourceCode) - } - if c.Node.ChildCount() >= 4 { - // TODO: Save the contract nodes as structured data for richer information - // For now, just append and display their sources - for i := 2; i <= int(c.Node.ChildCount())-2; i++ { - contract_node := c.Node.Child(i) - if contract_node.Type() == "doc_comment_contract" { - lastDocComment += "\n" + contract_node.Content(sourceCode) - } - } - } - + lastDocComment = cast.ToPtr(p.nodeToDocComment(c.Node, sourceCode)) case "module": anonymousModuleName = false module, _, _ := p.nodeToModule(doc, c.Node, sourceCode) @@ -142,7 +128,7 @@ func (p *Parser) ParseSymbols(doc *document.Document) (symbols_table.UnitModules moduleSymbol.SetStartPosition(idx.NewPositionFromTreeSitterPoint(start)) moduleSymbol.ChangeModule(lastModuleName) - if lastDocComment != "" { + if lastDocComment != nil { moduleSymbol.SetDocComment(lastDocComment) } @@ -155,7 +141,7 @@ func (p *Parser) ParseSymbols(doc *document.Document) (symbols_table.UnitModules moduleSymbol.AddVariables(variables) pendingToResolve.AddVariableType(variables, moduleSymbol) - if lastDocComment != "" { + if lastDocComment != nil { for _, v := range variables { v.SetDocComment(lastDocComment) } @@ -167,7 +153,7 @@ func (p *Parser) ParseSymbols(doc *document.Document) (symbols_table.UnitModules moduleSymbol.AddFunction(&function) pendingToResolve.AddFunctionTypes(&function, moduleSymbol) - if lastDocComment != "" { + if lastDocComment != nil { function.SetDocComment(lastDocComment) } } @@ -176,7 +162,7 @@ func (p *Parser) ParseSymbols(doc *document.Document) (symbols_table.UnitModules enum := p.nodeToEnum(c.Node, moduleSymbol, &doc.URI, sourceCode) moduleSymbol.AddEnum(&enum) - if lastDocComment != "" { + if lastDocComment != nil { enum.SetDocComment(lastDocComment) } @@ -189,7 +175,7 @@ func (p *Parser) ParseSymbols(doc *document.Document) (symbols_table.UnitModules pendingToResolve.AddStructMemberTypes(&strukt, moduleSymbol) - if lastDocComment != "" { + if lastDocComment != nil { strukt.SetDocComment(lastDocComment) } @@ -197,7 +183,7 @@ func (p *Parser) ParseSymbols(doc *document.Document) (symbols_table.UnitModules bitstruct := p.nodeToBitStruct(c.Node, moduleSymbol, &doc.URI, sourceCode) moduleSymbol.AddBitstruct(&bitstruct) - if lastDocComment != "" { + if lastDocComment != nil { bitstruct.SetDocComment(lastDocComment) } @@ -206,7 +192,7 @@ func (p *Parser) ParseSymbols(doc *document.Document) (symbols_table.UnitModules moduleSymbol.AddDef(&def) pendingToResolve.AddDefType(&def, moduleSymbol) - if lastDocComment != "" { + if lastDocComment != nil { def.SetDocComment(lastDocComment) } @@ -214,7 +200,7 @@ func (p *Parser) ParseSymbols(doc *document.Document) (symbols_table.UnitModules _const := p.nodeToConstant(c.Node, moduleSymbol, &doc.URI, sourceCode) moduleSymbol.AddVariable(&_const) - if lastDocComment != "" { + if lastDocComment != nil { _const.SetDocComment(lastDocComment) } @@ -222,7 +208,7 @@ func (p *Parser) ParseSymbols(doc *document.Document) (symbols_table.UnitModules fault := p.nodeToFault(c.Node, moduleSymbol, &doc.URI, sourceCode) moduleSymbol.AddFault(&fault) - if lastDocComment != "" { + if lastDocComment != nil { fault.SetDocComment(lastDocComment) } @@ -230,7 +216,7 @@ func (p *Parser) ParseSymbols(doc *document.Document) (symbols_table.UnitModules interf := p.nodeToInterface(c.Node, moduleSymbol, &doc.URI, sourceCode) moduleSymbol.AddInterface(&interf) - if lastDocComment != "" { + if lastDocComment != nil { interf.SetDocComment(lastDocComment) } @@ -238,7 +224,7 @@ func (p *Parser) ParseSymbols(doc *document.Document) (symbols_table.UnitModules macro := p.nodeToMacro(c.Node, moduleSymbol, &doc.URI, sourceCode) moduleSymbol.AddFunction(¯o) - if lastDocComment != "" { + if lastDocComment != nil { macro.SetDocComment(lastDocComment) } default: @@ -249,13 +235,13 @@ func (p *Parser) ParseSymbols(doc *document.Document) (symbols_table.UnitModules // int value = 4; // v // } - lastDocComment = "" + lastDocComment = nil continue } if nodeType != "doc_comment" { // Ensure the next node won't receive the same doc comment - lastDocComment = "" + lastDocComment = nil moduleSymbol.SetEndPosition(nodeEndPoint) } } diff --git a/server/pkg/parser/parser_functions_test.go b/server/pkg/parser/parser_functions_test.go index 98ba2cdd..a71a49ad 100644 --- a/server/pkg/parser/parser_functions_test.go +++ b/server/pkg/parser/parser_functions_test.go @@ -61,7 +61,7 @@ func TestExtractSymbols_Functions_Declaration(t *testing.T) { assert.True(t, fn.IsSome(), "Function was not found") assert.Equal(t, "init_window", fn.Get().GetName(), "Function name") assert.Equal(t, "void", fn.Get().GetReturnType().GetName(), "Return type") - assert.Equal(t, "", fn.Get().GetDocComment()) + assert.Nil(t, fn.Get().GetDocComment()) assert.Equal(t, idx.NewRange(0, 8, 0, 19), fn.Get().GetIdRange()) assert.Equal(t, idx.NewRange(0, 0, 0, 78), fn.Get().GetDocumentRange()) }) @@ -82,7 +82,8 @@ func TestExtractSymbols_Functions_Declaration(t *testing.T) { assert.Equal(t, "void", fn.Get().GetReturnType().GetName(), "Return type") assert.Equal(t, idx.NewRange(3, 10, 3, 21), fn.Get().GetIdRange()) assert.Equal(t, idx.NewRange(3, 2, 3, 80), fn.Get().GetDocumentRange()) - assert.Equal(t, "abc", fn.Get().GetDocComment()) + assert.Equal(t, "abc", fn.Get().GetDocComment().GetBody()) + assert.Equal(t, "abc", fn.Get().GetDocComment().DisplayBodyWithContracts()) }) t.Run("Resolves function with unnamed parameters correctly", func(t *testing.T) { @@ -153,7 +154,7 @@ func TestExtractSymbols_FunctionsWithArguments(t *testing.T) { assert.Equal(t, "void", fn.Get().GetReturnType().GetName(), "Return type") assert.Equal(t, idx.NewRange(0, 8, 0, 12), fn.Get().GetIdRange()) assert.Equal(t, idx.NewRange(0, 0, 2, 2), fn.Get().GetDocumentRange()) - assert.Equal(t, "", fn.Get().GetDocComment()) + assert.Nil(t, fn.Get().GetDocComment()) }) t.Run("Finds function with simple doc comment", func(t *testing.T) { @@ -174,7 +175,8 @@ func TestExtractSymbols_FunctionsWithArguments(t *testing.T) { assert.Equal(t, "void", fn.Get().GetReturnType().GetName(), "Return type") assert.Equal(t, idx.NewRange(3, 10, 3, 14), fn.Get().GetIdRange()) assert.Equal(t, idx.NewRange(3, 2, 5, 3), fn.Get().GetDocumentRange()) - assert.Equal(t, "abc", fn.Get().GetDocComment()) + assert.Equal(t, "abc", fn.Get().GetDocComment().GetBody()) + assert.Equal(t, "abc", fn.Get().GetDocComment().DisplayBodyWithContracts()) }) t.Run("Finds function with doc comment with contracts", func(t *testing.T) { @@ -199,11 +201,16 @@ func TestExtractSymbols_FunctionsWithArguments(t *testing.T) { assert.Equal(t, "void", fn.Get().GetReturnType().GetName(), "Return type") assert.Equal(t, idx.NewRange(7, 10, 7, 14), fn.Get().GetIdRange()) assert.Equal(t, idx.NewRange(7, 2, 9, 3), fn.Get().GetDocumentRange()) + assert.Equal(t, "abc", fn.Get().GetDocComment().GetBody()) assert.Equal(t, `abc -@pure -@param [in] pointer -@require number > 0, number < 1000 : "invalid number" -@ensure return == 1`, fn.Get().GetDocComment()) + +**@pure** + +**@param** [in] pointer + +**@require** number > 0, number < 1000 : "invalid number" + +**@ensure** return == 1`, fn.Get().GetDocComment().DisplayBodyWithContracts()) }) t.Run("Finds function arguments", func(t *testing.T) { diff --git a/server/pkg/parser/parser_test.go b/server/pkg/parser/parser_test.go index d17588e1..d4fc60d8 100644 --- a/server/pkg/parser/parser_test.go +++ b/server/pkg/parser/parser_test.go @@ -3,6 +3,7 @@ package parser import ( "testing" + "github.com/pherrymason/c3-lsp/pkg/cast" "github.com/pherrymason/c3-lsp/pkg/document" "github.com/pherrymason/c3-lsp/pkg/option" idx "github.com/pherrymason/c3-lsp/pkg/symbols" @@ -62,7 +63,7 @@ func TestParses_TypedEnums(t *testing.T) { scope := symbols.Get("doc") enum := scope.Enums["Colors"] - assert.Equal(t, "abc", enum.GetDocComment()) + assert.Equal(t, "abc", enum.GetDocComment().GetBody()) }) t.Run("finds defined enumerators", func(t *testing.T) { @@ -145,7 +146,7 @@ func TestParses_UnTypedEnums(t *testing.T) { scope := symbols.Get("doc") enum := scope.Enums["Colors"] - assert.Equal(t, "abc", enum.GetDocComment()) + assert.Equal(t, "abc", enum.GetDocComment().GetBody()) }) t.Run("finds defined enumerators", func(t *testing.T) { @@ -201,7 +202,7 @@ func TestParse_fault(t *testing.T) { fault := symbols.Get("doc").Faults["IOResult"] - assert.Equal(t, "docs", fault.GetDocComment()) + assert.Equal(t, "docs", fault.GetDocComment().GetBody()) }) t.Run("finds defined fault constants", func(t *testing.T) { @@ -258,7 +259,7 @@ func TestParse_interface(t *testing.T) { symbols, _ := parser.ParseSymbols(&doc) found := symbols.Get("doc").Interfaces["MyName"] - assert.Equal(t, "docs", found.GetDocComment()) + assert.Equal(t, "docs", found.GetDocComment().GetBody()) }) t.Run("finds defined methods in interface", func(t *testing.T) { @@ -298,7 +299,7 @@ func TestExtractSymbols_finds_definition(t *testing.T) { WithIdentifierRange(2, 5, 2, 9). WithDocumentRange(2, 1, 2, 16). Build() - expectedDefKilo.SetDocComment("docs") + expectedDefKilo.SetDocComment(cast.ToPtr(idx.NewDocComment("docs"))) assert.Equal(t, expectedDefKilo, module.Defs["Kilo"]) assert.Same(t, module.Children()[0], module.Defs["Kilo"]) @@ -377,7 +378,7 @@ func TestExtractSymbols_find_macro(t *testing.T) { assert.Equal(t, "m", fn.Get().GetName()) assert.Equal(t, "x", fn.Get().Variables["x"].GetName()) assert.Equal(t, "", fn.Get().Variables["x"].GetType().String()) - assert.Equal(t, "docs", fn.Get().GetDocComment()) + assert.Equal(t, "docs", fn.Get().GetDocComment().GetBody()) assert.Same(t, module.NestedScopes()[0], fn.Get()) } @@ -406,7 +407,7 @@ func TestExtractSymbols_find_module(t *testing.T) { module := symbols.Get("foo") assert.Equal(t, "foo", module.GetModuleString(), "module name is wrong") - assert.Equal(t, "docs", module.GetDocComment(), "module doc comment is wrong") + assert.Equal(t, "docs", module.GetDocComment().GetBody(), "module doc comment is wrong") }) t.Run("finds different modules defined in single file", func(t *testing.T) { @@ -426,13 +427,13 @@ func TestExtractSymbols_find_module(t *testing.T) { module := symbols.Get("foo") assert.Equal(t, "foo", module.GetModuleString(), "module name is wrong") assert.Equal(t, "foo", module.GetName(), "module name is wrong") - assert.Equal(t, "docs foo", module.GetDocComment(), "module doc comment is wrong") + assert.Equal(t, "docs foo", module.GetDocComment().GetBody(), "module doc comment is wrong") assert.Equal(t, idx.NewRange(2, 1, 3, 15), module.GetDocumentRange(), "Wrong range for foo module") module = symbols.Get("foo2") assert.Equal(t, "foo2", module.GetModuleString(), "module name is wrong") assert.Equal(t, "foo2", module.GetName(), "module name is wrong") - assert.Equal(t, "docs foo2", module.GetDocComment(), "module doc comment is wrong") + assert.Equal(t, "docs foo2", module.GetDocComment().GetBody(), "module doc comment is wrong") assert.Equal(t, idx.NewRange(6, 1, 7, 15), module.GetDocumentRange(), "Wrong range for foo2 module") }) diff --git a/server/pkg/parser/parser_variables_test.go b/server/pkg/parser/parser_variables_test.go index 4f4d7362..81b6707b 100644 --- a/server/pkg/parser/parser_variables_test.go +++ b/server/pkg/parser/parser_variables_test.go @@ -38,7 +38,7 @@ func TestExtractSymbols_find_variables(t *testing.T) { assert.Equal(t, true, found.GetType().IsBaseTypeLanguage(), "Variable Type should be base type") assert.Equal(t, idx.NewRange(2, 1, 2, 15), found.GetDocumentRange()) assert.Equal(t, idx.NewRange(2, 5, 2, 10), found.GetIdRange()) - assert.Equal(t, "docs", found.GetDocComment(), "Variable docs") + assert.Equal(t, "docs", found.GetDocComment().GetBody(), "Variable docs") assert.Equal(t, 0, len(pendingToResolve.GetTypesByModule(docId)), "Basic types should not be registered as pending to resolve.") }) @@ -88,7 +88,7 @@ func TestExtractSymbols_find_variables(t *testing.T) { assert.Equal(t, true, found.GetType().IsBaseTypeLanguage(), "Variable Type should be base type") assert.Equal(t, idx.NewRange(line, 5, line, 8), found.GetIdRange(), "First variable identifier range") assert.Equal(t, idx.NewRange(line, 1, line, 15), found.GetDocumentRange(), "First variable declaration range") - assert.Equal(t, "multidocs", found.GetDocComment()) + assert.Equal(t, "multidocs", found.GetDocComment().GetBody()) found = symbols.Get("x").Variables["foo2"] assert.Equal(t, "foo2", found.GetName(), "Second variable name") @@ -96,7 +96,7 @@ func TestExtractSymbols_find_variables(t *testing.T) { assert.Equal(t, true, found.GetType().IsBaseTypeLanguage(), "Variable Type should be base type") assert.Equal(t, idx.NewRange(line, 10, line, 14), found.GetIdRange(), "Second variable identifier range") assert.Equal(t, idx.NewRange(line, 1, line, 15), found.GetDocumentRange(), "Second variable declaration range") - assert.Equal(t, "multidocs", found.GetDocComment()) + assert.Equal(t, "multidocs", found.GetDocComment().GetBody()) }) t.Run("finds variables declared inside function", func(t *testing.T) { @@ -152,7 +152,7 @@ func TestExtractSymbols_find_constants(t *testing.T) { assert.True(t, found.IsConstant()) assert.Equal(t, idx.NewRange(1, 1, 1, 24), found.GetDocumentRange()) assert.Equal(t, idx.NewRange(1, 11, 1, 18), found.GetIdRange()) - assert.Equal(t, "docs", found.GetDocComment(), "Variable doc comment") + assert.Equal(t, "docs", found.GetDocComment().GetBody(), "Variable doc comment") } func TestExtractSymbols_find_variables_flag_pending_to_resolve(t *testing.T) { diff --git a/server/pkg/symbols/doc_comment.go b/server/pkg/symbols/doc_comment.go new file mode 100644 index 00000000..c0c15afd --- /dev/null +++ b/server/pkg/symbols/doc_comment.go @@ -0,0 +1,51 @@ +package symbols + +type DocCommentContract struct { + name string + body string +} + +type DocComment struct { + body string + contracts []*DocCommentContract +} + +// Creates a doc comment with the given body. +func NewDocComment(body string) DocComment { + return DocComment{ + body: body, + contracts: []*DocCommentContract{}, + } +} + +// Creates a contract with the given name and body. +// It is expected that the name begins with '@'. +func NewDocCommentContract(name string, body string) DocCommentContract { + return DocCommentContract{ + name, + body, + } +} + +// Add contracts to the given doc comment. +func (d *DocComment) AddContracts(contracts []*DocCommentContract) { + d.contracts = append(d.contracts, contracts...) +} + +func (d *DocComment) GetBody() string { + return d.body +} + +// Return a string displaying the body and contracts as markdown. +func (d *DocComment) DisplayBodyWithContracts() string { + out := d.body + + for _, c := range d.contracts { + out += "\n\n**" + c.name + "**" + if c.body != "" { + out += " " + c.body + } + } + + return out +} diff --git a/server/pkg/symbols/indexable.go b/server/pkg/symbols/indexable.go index d8f0e5d4..da20627f 100644 --- a/server/pkg/symbols/indexable.go +++ b/server/pkg/symbols/indexable.go @@ -33,7 +33,7 @@ type Indexable interface { GetModule() ModulePath IsSubModuleOf(parentModule ModulePath) bool - GetDocComment() string + GetDocComment() *DocComment GetHoverInfo() string HasSourceCode() bool // This will return false for that code that is not accesible either because it belongs to the stdlib, or inside a .c3lib library. This results in disabling "Go to definition" / "Go to declaration" on these symbols @@ -60,7 +60,7 @@ type BaseIndexable struct { idRange Range docRange Range Kind protocol.CompletionItemKind - docComment string + docComment *DocComment attributes []string children []Indexable @@ -125,7 +125,7 @@ func (b *BaseIndexable) SetDocumentURI(docId string) { b.documentURI = docId } -func (b *BaseIndexable) GetDocComment() string { +func (b *BaseIndexable) GetDocComment() *DocComment { return b.docComment } @@ -162,7 +162,7 @@ func (b *BaseIndexable) InsertNestedScope(symbol Indexable) { b.nestedScopes = append(b.nestedScopes, symbol) } -func (b *BaseIndexable) SetDocComment(docComment string) { +func (b *BaseIndexable) SetDocComment(docComment *DocComment) { b.docComment = docComment } @@ -180,7 +180,7 @@ func NewBaseIndexable(name string, module string, docId protocol.DocumentUri, id docRange: docRange, Kind: kind, hasSourceCode: true, - docComment: "", + docComment: nil, attributes: []string{}, } } From ed25e9c7a079a306dab8c3cdfc10834bdbff7685 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Sat, 25 Jan 2025 13:02:50 -0300 Subject: [PATCH 5/8] return docs as markdown upon completion --- .../lsp/search/search_completion_list.go | 11 +++++--- .../lsp/search/search_completion_list_test.go | 25 ++++++++++++------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/server/internal/lsp/search/search_completion_list.go b/server/internal/lsp/search/search_completion_list.go index 269a8164..711e0bce 100644 --- a/server/internal/lsp/search/search_completion_list.go +++ b/server/internal/lsp/search/search_completion_list.go @@ -127,15 +127,18 @@ func extractExplicitModulePath(possibleModulePath string) option.Option[symbols. return option.None[symbols.ModulePath]() } -// TODO: Consider only returning the body text, not contracts here -// Returns: nil | string +// Obtains a doc comment's representation as markup, or nil. +// Only the body is included (not contracts) for brevity. +// Returns: nil | MarkupContent func GetCompletableDocComment(s symbols.Indexable) any { docComment := s.GetDocComment() if docComment == nil || docComment.GetBody() == "" { return nil } else { - // Don't include contract information in completion, for brevity - return docComment.GetBody() + return protocol.MarkupContent{ + Kind: protocol.MarkupKindMarkdown, + Value: docComment.GetBody(), + } } } diff --git a/server/internal/lsp/search/search_completion_list_test.go b/server/internal/lsp/search/search_completion_list_test.go index 3d1032db..77d91d4c 100644 --- a/server/internal/lsp/search/search_completion_list_test.go +++ b/server/internal/lsp/search/search_completion_list_test.go @@ -27,6 +27,13 @@ func filterOutKeywordSuggestions(completionList []protocol.CompletionItem) []pro return filteredCompletionList } +func asMarkdown(text string) protocol.MarkupContent { + return protocol.MarkupContent{ + Kind: protocol.MarkupKindMarkdown, + Value: text, + } +} + func Test_isCompletingAChain(t *testing.T) { cases := []struct { name string @@ -354,8 +361,8 @@ func TestBuildCompletionList(t *testing.T) { {"v", protocol.CompletionItem{Label: "variable", Kind: &expectedVarKind}}, {"va", protocol.CompletionItem{Label: "variable", Kind: &expectedVarKind}}, {"x", protocol.CompletionItem{Label: "xanadu", Kind: &expectedVarKind}}, - {"docu", protocol.CompletionItem{Label: "documented", Kind: &expectedVarKind, Documentation: "doc"}}, - {"MY_C", protocol.CompletionItem{Label: "MY_CONST", Kind: &expectedConstKind, Documentation: "const doc"}}, + {"docu", protocol.CompletionItem{Label: "documented", Kind: &expectedVarKind, Documentation: asMarkdown("doc")}}, + {"MY_C", protocol.CompletionItem{Label: "MY_CONST", Kind: &expectedConstKind, Documentation: asMarkdown("const doc")}}, } for n, tt := range cases { @@ -479,10 +486,10 @@ func TestBuildCompletionList(t *testing.T) { expected []protocol.CompletionItem }{ {"p", []protocol.CompletionItem{ - {Label: "process", Kind: &expectedKind, Documentation: "abc"}, + {Label: "process", Kind: &expectedKind, Documentation: asMarkdown("abc")}, }}, {"proc", []protocol.CompletionItem{ - {Label: "process", Kind: &expectedKind, Documentation: "abc"}, + {Label: "process", Kind: &expectedKind, Documentation: asMarkdown("abc")}, }}, } @@ -522,7 +529,7 @@ func TestBuildCompletionList(t *testing.T) { }` // Contracts are excluded - expectedDoc := "abc" + expectedDoc := asMarkdown("abc") expectedKind := protocol.CompletionItemKindFunction cases := []struct { @@ -639,7 +646,7 @@ func TestBuildCompletionList_struct_suggest_all_its_members(t *testing.T) { NewText: "toCircle", Range: protocol_utils.NewLSPRange(6, 7, 6, 8), }, - Documentation: "member doc", + Documentation: asMarkdown("member doc"), }, {Label: "width", Kind: &expectedKind}, }, completionList) @@ -1024,7 +1031,7 @@ func TestBuildCompletionList_modules(t *testing.T) { NewText: "app", Range: protocol_utils.NewLSPRange(3, 4, 3, 5), }, - Documentation: "doc", + Documentation: asMarkdown("doc"), }, }, true, @@ -1231,7 +1238,7 @@ func TestBuildCompletionList_interfaces(t *testing.T) { { Label: "EmulatorConsole", Kind: cast.ToPtr(protocol.CompletionItemKindInterface), - Documentation: "doc", + Documentation: asMarkdown("doc"), }, }, completionList, @@ -1244,7 +1251,7 @@ func CreateCompletionItem(label string, kind protocol.CompletionItemKind) protoc } func CreateCompletionItemWithDoc(label string, kind protocol.CompletionItemKind, doc string) protocol.CompletionItem { - return protocol.CompletionItem{Label: label, Kind: &kind, Documentation: doc} + return protocol.CompletionItem{Label: label, Kind: &kind, Documentation: asMarkdown(doc)} } func TestBuildCompletionList_should_resolve_(t *testing.T) { From f7c512937e49ae4d378cccac2930ea7993be6eb6 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Sat, 25 Jan 2025 13:28:00 -0300 Subject: [PATCH 6/8] dedent docstring body --- server/pkg/dedent/dedent.go | 56 ++++++++++++++++++++++ server/pkg/parser/node_to_doc_comment.go | 7 ++- server/pkg/parser/parser_functions_test.go | 34 +++++++++---- 3 files changed, 87 insertions(+), 10 deletions(-) create mode 100644 server/pkg/dedent/dedent.go diff --git a/server/pkg/dedent/dedent.go b/server/pkg/dedent/dedent.go new file mode 100644 index 00000000..b9db7f8e --- /dev/null +++ b/server/pkg/dedent/dedent.go @@ -0,0 +1,56 @@ +package dedent + +import ( + "regexp" + "strings" +) + +var ( + whitespaceOnly = regexp.MustCompile("(?m)^[ \t]+$") + leadingWhitespace = regexp.MustCompile("(?m)(^[ \t]*)(?:[^ \t\n])") +) + +// Original code from https://github.com/lithammer/dedent +// Includes a modified version of https://github.com/lithammer/dedent/pull/17 to support post-first-line indent. +// +// Dedent removes any common leading whitespace from every line in text. +// +// This can be used to make multiline strings to line up with the left edge of +// the display, while still presenting them in the source code in indented +// form. +func Dedent(text string) string { + var margin string + + text = whitespaceOnly.ReplaceAllString(text, "") + indents := leadingWhitespace.FindAllStringSubmatch(text, -1) + + // Look for the longest leading string of spaces and tabs common to all + // lines, except first non-indented lines. + firstIndentedLine := 0 + for i, indent := range indents { + if i == 0 && indent[1] == "" { + // no indent in first line, ignore it + firstIndentedLine = 1 + } else if i == firstIndentedLine { + margin = indent[1] + } else if strings.HasPrefix(indent[1], margin) { + // Current line more deeply indented than previous winner: + // no change (previous winner is still on top). + continue + } else if strings.HasPrefix(margin, indent[1]) { + // Current line consistent with and no deeper than previous winner: + // it's the new winner. + margin = indent[1] + } else { + // Current line and previous winner have no common whitespace: + // there is no margin. + margin = "" + break + } + } + + if margin != "" { + text = regexp.MustCompile("(?m)^"+margin).ReplaceAllString(text, "") + } + return text +} diff --git a/server/pkg/parser/node_to_doc_comment.go b/server/pkg/parser/node_to_doc_comment.go index 79f1672f..b90ed9e8 100644 --- a/server/pkg/parser/node_to_doc_comment.go +++ b/server/pkg/parser/node_to_doc_comment.go @@ -2,6 +2,7 @@ package parser import ( "github.com/pherrymason/c3-lsp/pkg/cast" + "github.com/pherrymason/c3-lsp/pkg/dedent" idx "github.com/pherrymason/c3-lsp/pkg/symbols" sitter "github.com/smacker/go-tree-sitter" ) @@ -35,7 +36,8 @@ func (p *Parser) nodeToDocComment(node *sitter.Node, sourceCode []byte) idx.DocC body := "" bodyNode := node.Child(1) if bodyNode.Type() == "doc_comment_text" { - body = bodyNode.Content(sourceCode) + // Dedent to accept indented doc strings. + body = dedent.Dedent(bodyNode.Content(sourceCode)) } docComment := idx.NewDocComment(body) @@ -47,6 +49,9 @@ func (p *Parser) nodeToDocComment(node *sitter.Node, sourceCode []byte) idx.DocC name := contractNode.ChildByFieldName("name").Content(sourceCode) body := "" if contractNode.ChildCount() >= 2 { + // Right now, contracts can only have a single line, so we don't dedent. + // They can also be arbitrary expressions, so it's best to not modify them + // at the moment. body = contractNode.Child(1).Content(sourceCode) } diff --git a/server/pkg/parser/parser_functions_test.go b/server/pkg/parser/parser_functions_test.go index a71a49ad..a19b81ff 100644 --- a/server/pkg/parser/parser_functions_test.go +++ b/server/pkg/parser/parser_functions_test.go @@ -160,6 +160,11 @@ func TestExtractSymbols_FunctionsWithArguments(t *testing.T) { t.Run("Finds function with simple doc comment", func(t *testing.T) { source := `<* abc + + def + + ghi + jkl *> fn void test(int number, char ch, int* pointer) { return 1; @@ -169,19 +174,28 @@ func TestExtractSymbols_FunctionsWithArguments(t *testing.T) { parser := createParser() symbols, _ := parser.ParseSymbols(&doc) + expectedDoc := `abc + +def + +ghi +jkl` + fn := symbols.Get("docid").GetChildrenFunctionByName("test") assert.True(t, fn.IsSome(), "Function was not found") assert.Equal(t, "test", fn.Get().GetName(), "Function name") assert.Equal(t, "void", fn.Get().GetReturnType().GetName(), "Return type") - assert.Equal(t, idx.NewRange(3, 10, 3, 14), fn.Get().GetIdRange()) - assert.Equal(t, idx.NewRange(3, 2, 5, 3), fn.Get().GetDocumentRange()) - assert.Equal(t, "abc", fn.Get().GetDocComment().GetBody()) - assert.Equal(t, "abc", fn.Get().GetDocComment().DisplayBodyWithContracts()) + assert.Equal(t, idx.NewRange(8, 10, 8, 14), fn.Get().GetIdRange()) + assert.Equal(t, idx.NewRange(8, 2, 10, 3), fn.Get().GetDocumentRange()) + assert.Equal(t, expectedDoc, fn.Get().GetDocComment().GetBody()) + assert.Equal(t, expectedDoc, fn.Get().GetDocComment().DisplayBodyWithContracts()) }) t.Run("Finds function with doc comment with contracts", func(t *testing.T) { source := `<* - abc + Hello world. + Hello world. + @pure @param [in] pointer @require number > 0, number < 1000 : "invalid number" @@ -199,10 +213,12 @@ func TestExtractSymbols_FunctionsWithArguments(t *testing.T) { assert.True(t, fn.IsSome(), "Function was not found") assert.Equal(t, "test", fn.Get().GetName(), "Function name") assert.Equal(t, "void", fn.Get().GetReturnType().GetName(), "Return type") - assert.Equal(t, idx.NewRange(7, 10, 7, 14), fn.Get().GetIdRange()) - assert.Equal(t, idx.NewRange(7, 2, 9, 3), fn.Get().GetDocumentRange()) - assert.Equal(t, "abc", fn.Get().GetDocComment().GetBody()) - assert.Equal(t, `abc + assert.Equal(t, idx.NewRange(9, 10, 9, 14), fn.Get().GetIdRange()) + assert.Equal(t, idx.NewRange(9, 2, 11, 3), fn.Get().GetDocumentRange()) + assert.Equal(t, `Hello world. +Hello world.`, fn.Get().GetDocComment().GetBody()) + assert.Equal(t, `Hello world. +Hello world. **@pure** From 60fe47c886132973292a364b3a4a5aa13fc40eea Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Sat, 25 Jan 2025 13:44:43 -0300 Subject: [PATCH 7/8] add function docs to signature help --- .../lsp/server/TextDocumentSignatureHelp.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/server/internal/lsp/server/TextDocumentSignatureHelp.go b/server/internal/lsp/server/TextDocumentSignatureHelp.go index 2dcf560d..6c103e56 100644 --- a/server/internal/lsp/server/TextDocumentSignatureHelp.go +++ b/server/internal/lsp/server/TextDocumentSignatureHelp.go @@ -48,16 +48,29 @@ func (h *Server) TextDocumentSignatureHelp(context *glsp.Context, params *protoc parameters, protocol.ParameterInformation{ Label: arg.GetType().String() + " " + arg.GetName(), + + // TODO: Parse '@param' contract text to get param docs + Documentation: nil, }, ) } // Count number of commas (,) written from previous `(` activeParameter := countWrittenArguments(posOption.Get(), doc.SourceCode) + + var docs any = nil + docComment := function.GetDocComment() + if docComment != nil { + docs = protocol.MarkupContent{ + Kind: protocol.MarkupKindMarkdown, + Value: docComment.DisplayBodyWithContracts(), + } + } + signature := protocol.SignatureInformation{ Label: function.GetFQN() + "(" + strings.Join(argsToStringify, ", ") + ")", Parameters: parameters, - Documentation: "", // TODO: Parse comments on functions to include them here. + Documentation: docs, } if activeParameter.IsSome() { arg := activeParameter.Get() From d8ab552928cf4ff4da48b168e58448cc8960452b Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Sat, 25 Jan 2025 13:54:36 -0300 Subject: [PATCH 8/8] fix parsing doc comments with only contracts --- server/pkg/parser/node_to_doc_comment.go | 9 ++++-- server/pkg/parser/parser_functions_test.go | 33 +++++++++++++++++++++- server/pkg/symbols/doc_comment.go | 5 +++- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/server/pkg/parser/node_to_doc_comment.go b/server/pkg/parser/node_to_doc_comment.go index b90ed9e8..3c262679 100644 --- a/server/pkg/parser/node_to_doc_comment.go +++ b/server/pkg/parser/node_to_doc_comment.go @@ -34,17 +34,22 @@ at_ident: _ => token(seq('@', IDENT)), */ func (p *Parser) nodeToDocComment(node *sitter.Node, sourceCode []byte) idx.DocComment { body := "" + hasBody := false bodyNode := node.Child(1) if bodyNode.Type() == "doc_comment_text" { // Dedent to accept indented doc strings. body = dedent.Dedent(bodyNode.Content(sourceCode)) + hasBody = true } docComment := idx.NewDocComment(body) - if node.ChildCount() >= 4 { - for i := 2; i <= int(node.ChildCount())-2; i++ { + if (hasBody && node.ChildCount() >= 4) || (!hasBody && node.ChildCount() >= 3) { + for i := 1; i <= int(node.ChildCount())-2; i++ { contractNode := node.Child(i) + + // Skip the body + // (We already skip '<*' and '*>' since we skip first and last indices above) if contractNode.Type() == "doc_comment_contract" { name := contractNode.ChildByFieldName("name").Content(sourceCode) body := "" diff --git a/server/pkg/parser/parser_functions_test.go b/server/pkg/parser/parser_functions_test.go index a19b81ff..488cc208 100644 --- a/server/pkg/parser/parser_functions_test.go +++ b/server/pkg/parser/parser_functions_test.go @@ -191,7 +191,7 @@ jkl` assert.Equal(t, expectedDoc, fn.Get().GetDocComment().DisplayBodyWithContracts()) }) - t.Run("Finds function with doc comment with contracts", func(t *testing.T) { + t.Run("Finds function with doc comment with body and contracts", func(t *testing.T) { source := `<* Hello world. Hello world. @@ -226,6 +226,37 @@ Hello world. **@require** number > 0, number < 1000 : "invalid number" +**@ensure** return == 1`, fn.Get().GetDocComment().DisplayBodyWithContracts()) + }) + + t.Run("Finds function with doc comment with only contracts", func(t *testing.T) { + source := `<* + @pure + @param [in] pointer + @require number > 0, number < 1000 : "invalid number" + @ensure return == 1 + *> + fn void test(int number, char ch, int* pointer) { + return 1; + }` + docId := "docId" + doc := document.NewDocument(docId, source) + parser := createParser() + symbols, _ := parser.ParseSymbols(&doc) + + fn := symbols.Get("docid").GetChildrenFunctionByName("test") + assert.True(t, fn.IsSome(), "Function was not found") + assert.Equal(t, "test", fn.Get().GetName(), "Function name") + assert.Equal(t, "void", fn.Get().GetReturnType().GetName(), "Return type") + assert.Equal(t, idx.NewRange(6, 10, 6, 14), fn.Get().GetIdRange()) + assert.Equal(t, idx.NewRange(6, 2, 8, 3), fn.Get().GetDocumentRange()) + assert.Equal(t, "", fn.Get().GetDocComment().GetBody()) + assert.Equal(t, `**@pure** + +**@param** [in] pointer + +**@require** number > 0, number < 1000 : "invalid number" + **@ensure** return == 1`, fn.Get().GetDocComment().DisplayBodyWithContracts()) }) diff --git a/server/pkg/symbols/doc_comment.go b/server/pkg/symbols/doc_comment.go index c0c15afd..4846519e 100644 --- a/server/pkg/symbols/doc_comment.go +++ b/server/pkg/symbols/doc_comment.go @@ -41,7 +41,10 @@ func (d *DocComment) DisplayBodyWithContracts() string { out := d.body for _, c := range d.contracts { - out += "\n\n**" + c.name + "**" + if out != "" { + out += "\n\n" + } + out += "**" + c.name + "**" if c.body != "" { out += " " + c.body }