Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
7 changes: 7 additions & 0 deletions internal/fourslash/fourslash.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ var (
PreselectSupport: ptrTrue,
LabelDetailsSupport: ptrTrue,
InsertReplaceSupport: ptrTrue,
DocumentationFormat: &[]lsproto.MarkupKind{lsproto.MarkupKindMarkdown, lsproto.MarkupKindPlainText},
},
CompletionList: &lsproto.CompletionListCapabilities{
ItemDefaults: &[]string{"commitCharacters", "editRange"},
Expand All @@ -251,6 +252,9 @@ var (
defaultTypeDefinitionCapabilities = &lsproto.TypeDefinitionClientCapabilities{
LinkSupport: ptrTrue,
}
defaultHoverCapabilities = &lsproto.HoverClientCapabilities{
ContentFormat: &[]lsproto.MarkupKind{lsproto.MarkupKindMarkdown, lsproto.MarkupKindPlainText},
}
)

func getCapabilitiesWithDefaults(capabilities *lsproto.ClientCapabilities) *lsproto.ClientCapabilities {
Expand Down Expand Up @@ -290,6 +294,9 @@ func getCapabilitiesWithDefaults(capabilities *lsproto.ClientCapabilities) *lspr
if capabilitiesWithDefaults.TextDocument.TypeDefinition == nil {
capabilitiesWithDefaults.TextDocument.TypeDefinition = defaultTypeDefinitionCapabilities
}
if capabilitiesWithDefaults.TextDocument.Hover == nil {
capabilitiesWithDefaults.TextDocument.Hover = defaultHoverCapabilities
}
return &capabilitiesWithDefaults
}

Expand Down
38 changes: 28 additions & 10 deletions internal/ls/completions.go
Original file line number Diff line number Diff line change
Expand Up @@ -5033,6 +5033,19 @@ func GetCompletionItemData(item *lsproto.CompletionItem) (*CompletionItemData, e
return &itemData, nil
}

func getCompletionDocumentationFormat(clientOptions *lsproto.CompletionClientCapabilities) lsproto.MarkupKind {
if clientOptions == nil || clientOptions.CompletionItem == nil || clientOptions.CompletionItem.DocumentationFormat == nil {
// Default to plaintext if no preference specified
return lsproto.MarkupKindPlainText
}
formats := *clientOptions.CompletionItem.DocumentationFormat
if len(formats) == 0 {
return lsproto.MarkupKindPlainText
}
// Return the first (most preferred) format
return formats[0]
}

func (l *LanguageService) getCompletionItemDetails(
ctx context.Context,
program *compiler.Program,
Expand All @@ -5044,6 +5057,7 @@ func (l *LanguageService) getCompletionItemDetails(
) *lsproto.CompletionItem {
checker, done := program.GetTypeCheckerForFile(ctx, file)
defer done()
docFormat := getCompletionDocumentationFormat(clientOptions)
contextToken, previousToken := getRelevantTokens(position, file)
if IsInString(file, position, previousToken) {
return l.getStringLiteralCompletionDetails(
Expand Down Expand Up @@ -5073,16 +5087,16 @@ func (l *LanguageService) getCompletionItemDetails(
request := *symbolCompletion.request
switch request := request.(type) {
case *completionDataJSDocTagName:
return createSimpleDetails(item, itemData.Name)
return createSimpleDetails(item, itemData.Name, docFormat)
case *completionDataJSDocTag:
return createSimpleDetails(item, itemData.Name)
return createSimpleDetails(item, itemData.Name, docFormat)
case *completionDataJSDocParameterName:
return createSimpleDetails(item, itemData.Name)
return createSimpleDetails(item, itemData.Name, docFormat)
case *completionDataKeyword:
if core.Some(request.keywordCompletions, func(c *lsproto.CompletionItem) bool {
return c.Label == itemData.Name
}) {
return createSimpleDetails(item, itemData.Name)
return createSimpleDetails(item, itemData.Name, docFormat)
}
return item
default:
Expand All @@ -5097,10 +5111,11 @@ func (l *LanguageService) getCompletionItemDetails(
checker,
symbolDetails.location,
actions,
docFormat,
)
case symbolCompletion.literal != nil:
literal := symbolCompletion.literal
return createSimpleDetails(item, completionNameForLiteral(file, preferences, *literal))
return createSimpleDetails(item, completionNameForLiteral(file, preferences, *literal), docFormat)
case symbolCompletion.cases != nil:
// !!! exhaustive case completions
return item
Expand All @@ -5109,7 +5124,7 @@ func (l *LanguageService) getCompletionItemDetails(
if core.Some(allKeywordCompletions(), func(c *lsproto.CompletionItem) bool {
return c.Label == itemData.Name
}) {
return createSimpleDetails(item, itemData.Name)
return createSimpleDetails(item, itemData.Name, docFormat)
}
return item
}
Expand Down Expand Up @@ -5256,14 +5271,16 @@ func (l *LanguageService) getAutoImportSymbolFromCompletionEntryData(ch *checker
func createSimpleDetails(
item *lsproto.CompletionItem,
name string,
docFormat lsproto.MarkupKind,
) *lsproto.CompletionItem {
return createCompletionDetails(item, name, "" /*documentation*/)
return createCompletionDetails(item, name, "" /*documentation*/, docFormat)
}

func createCompletionDetails(
item *lsproto.CompletionItem,
detail string,
documentation string,
docFormat lsproto.MarkupKind,
) *lsproto.CompletionItem {
// !!! fill in additionalTextEdits from code actions
if item.Detail == nil && detail != "" {
Expand All @@ -5272,7 +5289,7 @@ func createCompletionDetails(
if documentation != "" {
item.Documentation = &lsproto.StringOrMarkupContent{
MarkupContent: &lsproto.MarkupContent{
Kind: lsproto.MarkupKindMarkdown,
Kind: docFormat,
Value: documentation,
},
}
Expand All @@ -5293,19 +5310,20 @@ func (l *LanguageService) createCompletionDetailsForSymbol(
checker *checker.Checker,
location *ast.Node,
actions []codeAction,
docFormat lsproto.MarkupKind,
) *lsproto.CompletionItem {
details := make([]string, 0, len(actions)+1)
edits := make([]*lsproto.TextEdit, 0, len(actions))
for _, action := range actions {
details = append(details, action.description)
edits = append(edits, action.changes...)
}
quickInfo, documentation := l.getQuickInfoAndDocumentationForSymbol(checker, symbol, location)
quickInfo, documentation := l.getQuickInfoAndDocumentationForSymbol(checker, symbol, location, docFormat)
details = append(details, quickInfo)
if len(edits) != 0 {
item.AdditionalTextEdits = &edits
}
return createCompletionDetails(item, strings.Join(details, "\n\n"), documentation)
return createCompletionDetails(item, strings.Join(details, "\n\n"), documentation, docFormat)
}

// !!! snippets
Expand Down
48 changes: 47 additions & 1 deletion internal/ls/findallreferences.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ func (l *LanguageService) ProvideReferences(ctx context.Context, params *lsproto
return lsproto.LocationsOrNull{Locations: &locations}, nil
}

func (l *LanguageService) ProvideImplementations(ctx context.Context, params *lsproto.ImplementationParams) (lsproto.ImplementationResponse, error) {
func (l *LanguageService) ProvideImplementations(ctx context.Context, params *lsproto.ImplementationParams, clientSupportsLink bool) (lsproto.ImplementationResponse, error) {
program, sourceFile := l.getProgramAndFile(params.TextDocument.Uri)
position := int(l.converters.LineAndCharacterToPosition(sourceFile, params.Position))
node := astnav.GetTouchingPropertyName(sourceFile, position)
Expand All @@ -452,6 +452,14 @@ func (l *LanguageService) ProvideImplementations(ctx context.Context, params *ls
}
}

if clientSupportsLink {
links := l.convertEntriesToLocationLinks(entries)
linkPtrs := make([]*lsproto.LocationLink, len(links))
for i := range links {
linkPtrs[i] = &links[i]
}
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{DefinitionLinks: &linkPtrs}, nil
}
locations := l.convertEntriesToLocations(entries)
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{Locations: &locations}, nil
}
Expand Down Expand Up @@ -554,6 +562,44 @@ func (l *LanguageService) convertEntriesToLocations(entries []*ReferenceEntry) [
return locations
}

func (l *LanguageService) convertEntriesToLocationLinks(entries []*ReferenceEntry) []lsproto.LocationLink {
links := make([]lsproto.LocationLink, len(entries))
for i, entry := range entries {
var targetSelectionRange, targetRange *lsproto.Range

// For entries with nodes, compute ranges directly from the node
if entry.node != nil {
sourceFile := ast.GetSourceFileOfNode(entry.node)
entry.fileName = sourceFile.FileName()

// Get the selection range (the actual reference)
selectionTextRange := getRangeOfNode(entry.node, sourceFile, nil /*endNode*/)
targetSelectionRange = l.createLspRangeFromRange(selectionTextRange, sourceFile)

// Get the context range (broader scope including declaration context)
contextNode := core.OrElse(getContextNode(entry.node), entry.node)
contextTextRange := toContextRange(&selectionTextRange, sourceFile, contextNode)
if contextTextRange != nil {
targetRange = l.createLspRangeFromRange(*contextTextRange, sourceFile)
} else {
targetRange = targetSelectionRange
}
} else {
// For range entries, use the pre-computed range
l.resolveEntry(entry)
targetSelectionRange = entry.textRange
targetRange = targetSelectionRange
}

links[i] = lsproto.LocationLink{
TargetUri: lsconv.FileNameToDocumentURI(entry.fileName),
TargetRange: *targetRange,
TargetSelectionRange: *targetSelectionRange,
}
}
return links
}

func (l *LanguageService) mergeReferences(program *compiler.Program, referencesToMerge ...[]*SymbolAndEntries) []*SymbolAndEntries {
result := []*SymbolAndEntries{}
getSourceFileIndexOfEntry := func(program *compiler.Program, entry *ReferenceEntry) int {
Expand Down
65 changes: 46 additions & 19 deletions internal/ls/hover.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const (
typeFormatFlags = checker.TypeFormatFlagsUseAliasDefinedOutsideCurrentScope
)

func (l *LanguageService) ProvideHover(ctx context.Context, documentURI lsproto.DocumentUri, position lsproto.Position) (lsproto.HoverResponse, error) {
func (l *LanguageService) ProvideHover(ctx context.Context, documentURI lsproto.DocumentUri, position lsproto.Position, contentFormat lsproto.MarkupKind) (lsproto.HoverResponse, error) {
program, file := l.getProgramAndFile(documentURI)
node := astnav.GetTouchingPropertyName(file, int(l.converters.LineAndCharacterToPosition(file, position)))
if node.Kind == ast.KindSourceFile {
Expand All @@ -28,43 +28,57 @@ func (l *LanguageService) ProvideHover(ctx context.Context, documentURI lsproto.
c, done := program.GetTypeCheckerForFile(ctx, file)
defer done()
rangeNode := getNodeForQuickInfo(node)
quickInfo, documentation := l.getQuickInfoAndDocumentationForSymbol(c, c.GetSymbolAtLocation(node), rangeNode)
quickInfo, documentation := l.getQuickInfoAndDocumentationForSymbol(c, c.GetSymbolAtLocation(node), rangeNode, contentFormat)
if quickInfo == "" {
return lsproto.HoverOrNull{}, nil
}
hoverRange := l.getLspRangeOfNode(rangeNode, nil, nil)

var content string
if contentFormat == lsproto.MarkupKindMarkdown {
content = formatQuickInfo(quickInfo) + documentation
} else {
content = quickInfo + documentation
}

return lsproto.HoverOrNull{
Hover: &lsproto.Hover{
Contents: lsproto.MarkupContentOrStringOrMarkedStringWithLanguageOrMarkedStrings{
MarkupContent: &lsproto.MarkupContent{
Kind: lsproto.MarkupKindMarkdown,
Value: formatQuickInfo(quickInfo) + documentation,
Kind: contentFormat,
Value: content,
},
},
Range: hoverRange,
},
}, nil
}

func (l *LanguageService) getQuickInfoAndDocumentationForSymbol(c *checker.Checker, symbol *ast.Symbol, node *ast.Node) (string, string) {
func (l *LanguageService) getQuickInfoAndDocumentationForSymbol(c *checker.Checker, symbol *ast.Symbol, node *ast.Node, contentFormat lsproto.MarkupKind) (string, string) {
quickInfo, declaration := getQuickInfoAndDeclarationAtLocation(c, symbol, node)
if quickInfo == "" {
return "", ""
}
isMarkdown := contentFormat == lsproto.MarkupKindMarkdown
var b strings.Builder
if declaration != nil {
if jsdoc := getJSDocOrTag(declaration); jsdoc != nil && !containsTypedefTag(jsdoc) {
l.writeComments(&b, c, jsdoc.Comments())
l.writeComments(&b, c, jsdoc.Comments(), isMarkdown)
if jsdoc.Kind == ast.KindJSDoc {
if tags := jsdoc.AsJSDoc().Tags; tags != nil {
for _, tag := range tags.Nodes {
if tag.Kind == ast.KindJSDocTypeTag {
continue
}
b.WriteString("\n\n*@")
b.WriteString(tag.TagName().Text())
b.WriteString("*")
b.WriteString("\n\n")
if isMarkdown {
b.WriteString("*@")
b.WriteString(tag.TagName().Text())
b.WriteString("*")
} else {
b.WriteString("@")
b.WriteString(tag.TagName().Text())
}
switch tag.Kind {
case ast.KindJSDocParameterTag, ast.KindJSDocPropertyTag:
writeOptionalEntityName(&b, tag.Name())
Expand All @@ -90,7 +104,7 @@ func (l *LanguageService) getQuickInfoAndDocumentationForSymbol(c *checker.Check
b.WriteString("— ")
}
}
l.writeComments(&b, c, comments)
l.writeComments(&b, c, comments, isMarkdown)
}
}
}
Expand Down Expand Up @@ -425,24 +439,24 @@ func writeCode(b *strings.Builder, lang string, code string) {
b.WriteByte('\n')
}

func (l *LanguageService) writeComments(b *strings.Builder, c *checker.Checker, comments []*ast.Node) {
func (l *LanguageService) writeComments(b *strings.Builder, c *checker.Checker, comments []*ast.Node, isMarkdown bool) {
for _, comment := range comments {
switch comment.Kind {
case ast.KindJSDocText:
b.WriteString(comment.Text())
case ast.KindJSDocLink, ast.KindJSDocLinkPlain:
l.writeJSDocLink(b, c, comment, false /*quote*/)
l.writeJSDocLink(b, c, comment, false /*quote*/, isMarkdown)
case ast.KindJSDocLinkCode:
l.writeJSDocLink(b, c, comment, true /*quote*/)
l.writeJSDocLink(b, c, comment, true /*quote*/, isMarkdown)
}
}
}

func (l *LanguageService) writeJSDocLink(b *strings.Builder, c *checker.Checker, link *ast.Node, quote bool) {
func (l *LanguageService) writeJSDocLink(b *strings.Builder, c *checker.Checker, link *ast.Node, quote bool, isMarkdown bool) {
name := link.Name()
text := strings.Trim(link.Text(), " ")
if name == nil {
writeQuotedString(b, text, quote)
writeQuotedString(b, text, quote && isMarkdown)
return
}
if ast.IsIdentifier(name) && (name.Text() == "http" || name.Text() == "https") && strings.HasPrefix(text, "://") {
Expand All @@ -455,7 +469,16 @@ func (l *LanguageService) writeJSDocLink(b *strings.Builder, c *checker.Checker,
linkText = linkUri
}
}
writeMarkdownLink(b, linkText, linkUri, quote)
if isMarkdown {
writeMarkdownLink(b, linkText, linkUri, quote)
} else {
writeQuotedString(b, linkText, false)
if linkText != linkUri {
b.WriteString(" (")
b.WriteString(linkUri)
b.WriteString(")")
}
}
return
}
declarations := getDeclarationsFromLocation(c, name)
Expand All @@ -469,11 +492,15 @@ func (l *LanguageService) writeJSDocLink(b *strings.Builder, c *checker.Checker,
if linkText == "" {
linkText = getEntityNameString(name) + text[:prefixLen]
}
linkUri := fmt.Sprintf("%s#%d,%d-%d,%d", loc.Uri, loc.Range.Start.Line+1, loc.Range.Start.Character+1, loc.Range.End.Line+1, loc.Range.End.Character+1)
writeMarkdownLink(b, linkText, linkUri, quote)
if isMarkdown {
linkUri := fmt.Sprintf("%s#%d,%d-%d,%d", loc.Uri, loc.Range.Start.Line+1, loc.Range.Start.Character+1, loc.Range.End.Line+1, loc.Range.End.Character+1)
writeMarkdownLink(b, linkText, linkUri, quote)
} else {
writeQuotedString(b, linkText, false)
}
return
}
writeQuotedString(b, getEntityNameString(name)+" "+text, quote)
writeQuotedString(b, getEntityNameString(name)+" "+text, quote && isMarkdown)
}

func trimCommentPrefix(text string) string {
Expand Down
6 changes: 3 additions & 3 deletions internal/ls/string_completions.go
Original file line number Diff line number Diff line change
Expand Up @@ -699,21 +699,21 @@ func (l *LanguageService) stringLiteralCompletionDetails(
pathCompletions := completion.fromPaths
for _, pathCompletion := range pathCompletions {
if pathCompletion.name == name {
return createCompletionDetails(item, name, "" /*documentation*/)
return createCompletionDetails(item, name, "" /*documentation*/, lsproto.MarkupKindPlainText)
}
}
case completion.fromProperties != nil:
properties := completion.fromProperties
for _, symbol := range properties.symbols {
if symbol.Name == name {
return l.createCompletionDetailsForSymbol(item, symbol, checker, location, nil /*actions*/)
return l.createCompletionDetailsForSymbol(item, symbol, checker, location, nil /*actions*/, lsproto.MarkupKindPlainText)
}
}
case completion.fromTypes != nil:
types := completion.fromTypes
for _, t := range types.types {
if t.AsLiteralType().Value().(string) == name {
return createCompletionDetails(item, name, "" /*documentation*/)
return createCompletionDetails(item, name, "" /*documentation*/, lsproto.MarkupKindPlainText)
}
}
}
Expand Down
Loading