Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions languageserver/server/completion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
* Cadence languageserver - The Cadence language server
*
* Copyright Flow Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package server

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/onflow/cadence-tools/languageserver/protocol"
)

func TestCompletionDocString(t *testing.T) {
t.Parallel()

t.Run("member completion", func(t *testing.T) {
server, err := NewServer()
require.NoError(t, err)

// Struct with a documented method; trigger member completion on an instance.
// Use "f.b" so the parser creates a member access expression.
const code = `
access(all) struct Foo {
/// Does something.
///
/// @param x: The input
/// @return The output
access(all) fun bar(x: Int): String {
return ""
}
}

access(all) fun test() {
let f = Foo()
f.b
}
`
// Line 13 (0-indexed): " f.b"
// 14 spaces + "f.b": dot is at column 15, "b" at column 16.
// Completion is requested after the dot = column 16.

uri := protocol.DocumentURI("file:///completion_member.cdc")
server.documents[uri] = Document{Text: code, Version: 1}
_, err = server.getDiagnostics(uri, code, 1, func(*protocol.LogMessageParams) {})
require.NoError(t, err)

items, err := server.Completion(
nil,
&protocol.CompletionParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{URI: uri},
Position: protocol.Position{Line: 13, Character: 16},
},
},
)
require.NoError(t, err)

// Find the "bar" completion item
var barItem *protocol.CompletionItem
for _, item := range items {
if item.Label == "bar" {
barItem = item
break
}
}
require.NotNil(t, barItem, "expected 'bar' in completion items")

resolved, err := server.ResolveCompletionItem(nil, barItem)
require.NoError(t, err)
require.NotNil(t, resolved.Documentation)

assert.Equal(
t,
&protocol.Or_CompletionItem_documentation{
Value: protocol.MarkupContent{
Kind: "markdown",
Value: "Does something.\n\n" +
"**Parameters**\n\n" +
"- `x`: The input\n\n" +
"**Returns** The output",
},
},
resolved.Documentation,
)
})

t.Run("range completion", func(t *testing.T) {
server, err := NewServer()
require.NoError(t, err)

// A documented function; trigger range (non-member) completion inside another function body.
const code = `
/// Adds two numbers.
///
/// @param a: The first number
/// @param b: The second number
/// @return The sum
access(all) fun add(a: Int, b: Int): Int {
return a + b
}

access(all) fun test() {
ad
}
`
// "ad" is on line 12, columns 14-15. Request completion at column 16 (end of "ad").

uri := protocol.DocumentURI("file:///completion_range.cdc")
server.documents[uri] = Document{Text: code, Version: 1}
// There will be checker errors since "ad" is not a valid expression,
// but we only need the ranges to be populated.
_, _ = server.getDiagnostics(uri, code, 1, func(*protocol.LogMessageParams) {})

items, err := server.Completion(
nil,
&protocol.CompletionParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{URI: uri},
Position: protocol.Position{Line: 12, Character: 16},
},
},
)
require.NoError(t, err)

// Find the "add" completion item
var addItem *protocol.CompletionItem
for _, item := range items {
if item.Label == "add" {
addItem = item
break
}
}
require.NotNil(t, addItem, "expected 'add' in completion items")

resolved, err := server.ResolveCompletionItem(nil, addItem)
require.NoError(t, err)
require.NotNil(t, resolved.Documentation)

assert.Equal(
t,
&protocol.Or_CompletionItem_documentation{
Value: "Adds two numbers.\n\n" +
"**Parameters**\n\n" +
"- `a`: The first number\n" +
"- `b`: The second number\n\n" +
"**Returns** The sum",
},
resolved.Documentation,
)
})
}
103 changes: 103 additions & 0 deletions languageserver/server/docstring.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Cadence languageserver - The Cadence language server
*
* Copyright Flow Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package server

import (
"fmt"
"strings"
)

const (
paramAnnotation = "@param "
returnAnnotation = "@return "
)

func formatDocString(docString string) string {
docString = strings.TrimSpace(docString)
if docString == "" {
return ""
}

var content strings.Builder
var params []string
var returnDoc string
var prevLineEmpty bool
var contentLines int

for line := range strings.SplitSeq(docString, "\n") {
trimmed := strings.TrimSpace(line)

if paramInfo, ok := strings.CutPrefix(trimmed, paramAnnotation); ok {
paramName, paramDesc, hasColon := strings.Cut(paramInfo, ":")
paramName = strings.TrimSpace(paramName)
if hasColon && len(paramName) > 0 {
paramDesc = strings.TrimSpace(paramDesc)
if len(paramDesc) > 0 {
params = append(params, fmt.Sprintf("- `%s`: %s", paramName, paramDesc))
} else {
params = append(params, fmt.Sprintf("- `%s`", paramName))
}
continue
}
// No valid colon/name — fall through to treat as normal content
} else if returnInfo, ok := strings.CutPrefix(trimmed, returnAnnotation); ok {
returnDoc = strings.TrimSpace(returnInfo)
continue
}

isEmpty := len(trimmed) == 0
if prevLineEmpty && isEmpty {
continue
}

if contentLines > 0 {
content.WriteByte('\n')
}
content.WriteString(trimmed)
prevLineEmpty = isEmpty
contentLines++
}

// Trim trailing whitespace/newlines from content before appending sections
resultStr := strings.TrimRight(content.String(), "\n ")

var finalBuilder strings.Builder
finalBuilder.WriteString(resultStr)

if len(params) > 0 {
if finalBuilder.Len() > 0 {
finalBuilder.WriteString("\n\n")
}
finalBuilder.WriteString("**Parameters**\n")
for _, p := range params {
finalBuilder.WriteByte('\n')
finalBuilder.WriteString(p)
}
}

if len(returnDoc) > 0 {
if finalBuilder.Len() > 0 {
finalBuilder.WriteString("\n\n")
}
finalBuilder.WriteString("**Returns** ")
finalBuilder.WriteString(returnDoc)
}

return finalBuilder.String()
}
108 changes: 108 additions & 0 deletions languageserver/server/docstring_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Cadence languageserver - The Cadence language server
*
* Copyright Flow Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package server

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestFormatDocString(t *testing.T) {
t.Parallel()

t.Run("empty", func(t *testing.T) {
assert.Equal(t, "", formatDocString(""))
assert.Equal(t, "", formatDocString(" "))
})

t.Run("no annotations", func(t *testing.T) {
assert.Equal(t,
"Some description of a function.",
formatDocString("Some description of a function."),
)
})

t.Run("only params", func(t *testing.T) {
input := `@param foo: Description of foo
@param bar: Description of bar`
expected := "**Parameters**\n\n- `foo`: Description of foo\n- `bar`: Description of bar"
assert.Equal(t, expected, formatDocString(input))
})

t.Run("only return", func(t *testing.T) {
input := `@return Description of result`
expected := "**Returns** Description of result"
assert.Equal(t, expected, formatDocString(input))
})

t.Run("content with params and return", func(t *testing.T) {
input := `Does something useful.

@param foo: Description of foo
@param bar: Description of bar
@return The result`
expected := `Does something useful.

**Parameters**

- ` + "`foo`" + `: Description of foo
- ` + "`bar`" + `: Description of bar

**Returns** The result`
assert.Equal(t, expected, formatDocString(input))
})

t.Run("params interspersed in content", func(t *testing.T) {
input := `First line.
@param foo: Description of foo
Some middle text.
@param bar: Description of bar
@return The result`
expected := `First line.
Some middle text.

**Parameters**

- ` + "`foo`" + `: Description of foo
- ` + "`bar`" + `: Description of bar

**Returns** The result`
assert.Equal(t, expected, formatDocString(input))
})

t.Run("param without colon treated as content", func(t *testing.T) {
input := `@param noColon
@param valid: has description`
expected := "@param noColon\n\n**Parameters**\n\n- `valid`: has description"
assert.Equal(t, expected, formatDocString(input))
})

t.Run("param without description", func(t *testing.T) {
input := `@param foo:`
expected := "**Parameters**\n\n- `foo`"
assert.Equal(t, expected, formatDocString(input))
})

t.Run("consecutive blank lines collapsed", func(t *testing.T) {
input := "First line.\n\n\n\nSecond line."
expected := "First line.\n\nSecond line."
assert.Equal(t, expected, formatDocString(input))
})
}
Loading
Loading