Skip to content

Commit ae5d110

Browse files
authored
Implement more handling of client capabilities (#1998)
1 parent e84065c commit ae5d110

File tree

7 files changed

+208
-40
lines changed

7 files changed

+208
-40
lines changed

internal/fourslash/fourslash.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ var (
240240
PreselectSupport: ptrTrue,
241241
LabelDetailsSupport: ptrTrue,
242242
InsertReplaceSupport: ptrTrue,
243+
DocumentationFormat: &[]lsproto.MarkupKind{lsproto.MarkupKindMarkdown, lsproto.MarkupKindPlainText},
243244
},
244245
CompletionList: &lsproto.CompletionListCapabilities{
245246
ItemDefaults: &[]string{"commitCharacters", "editRange"},
@@ -251,6 +252,9 @@ var (
251252
defaultTypeDefinitionCapabilities = &lsproto.TypeDefinitionClientCapabilities{
252253
LinkSupport: ptrTrue,
253254
}
255+
defaultHoverCapabilities = &lsproto.HoverClientCapabilities{
256+
ContentFormat: &[]lsproto.MarkupKind{lsproto.MarkupKindMarkdown, lsproto.MarkupKindPlainText},
257+
}
254258
)
255259

256260
func getCapabilitiesWithDefaults(capabilities *lsproto.ClientCapabilities) *lsproto.ClientCapabilities {
@@ -290,6 +294,9 @@ func getCapabilitiesWithDefaults(capabilities *lsproto.ClientCapabilities) *lspr
290294
if capabilitiesWithDefaults.TextDocument.TypeDefinition == nil {
291295
capabilitiesWithDefaults.TextDocument.TypeDefinition = defaultTypeDefinitionCapabilities
292296
}
297+
if capabilitiesWithDefaults.TextDocument.Hover == nil {
298+
capabilitiesWithDefaults.TextDocument.Hover = defaultHoverCapabilities
299+
}
293300
return &capabilitiesWithDefaults
294301
}
295302

internal/ls/completions.go

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5034,6 +5034,19 @@ func GetCompletionItemData(item *lsproto.CompletionItem) (*CompletionItemData, e
50345034
return &itemData, nil
50355035
}
50365036

5037+
func getCompletionDocumentationFormat(clientOptions *lsproto.CompletionClientCapabilities) lsproto.MarkupKind {
5038+
if clientOptions == nil || clientOptions.CompletionItem == nil || clientOptions.CompletionItem.DocumentationFormat == nil {
5039+
// Default to plaintext if no preference specified
5040+
return lsproto.MarkupKindPlainText
5041+
}
5042+
formats := *clientOptions.CompletionItem.DocumentationFormat
5043+
if len(formats) == 0 {
5044+
return lsproto.MarkupKindPlainText
5045+
}
5046+
// Return the first (most preferred) format
5047+
return formats[0]
5048+
}
5049+
50375050
func (l *LanguageService) getCompletionItemDetails(
50385051
ctx context.Context,
50395052
program *compiler.Program,
@@ -5045,6 +5058,7 @@ func (l *LanguageService) getCompletionItemDetails(
50455058
) *lsproto.CompletionItem {
50465059
checker, done := program.GetTypeCheckerForFile(ctx, file)
50475060
defer done()
5061+
docFormat := getCompletionDocumentationFormat(clientOptions)
50485062
contextToken, previousToken := getRelevantTokens(position, file)
50495063
if IsInString(file, position, previousToken) {
50505064
return l.getStringLiteralCompletionDetails(
@@ -5055,6 +5069,7 @@ func (l *LanguageService) getCompletionItemDetails(
50555069
file,
50565070
position,
50575071
contextToken,
5072+
docFormat,
50585073
)
50595074
}
50605075

@@ -5074,16 +5089,16 @@ func (l *LanguageService) getCompletionItemDetails(
50745089
request := *symbolCompletion.request
50755090
switch request := request.(type) {
50765091
case *completionDataJSDocTagName:
5077-
return createSimpleDetails(item, itemData.Name)
5092+
return createSimpleDetails(item, itemData.Name, docFormat)
50785093
case *completionDataJSDocTag:
5079-
return createSimpleDetails(item, itemData.Name)
5094+
return createSimpleDetails(item, itemData.Name, docFormat)
50805095
case *completionDataJSDocParameterName:
5081-
return createSimpleDetails(item, itemData.Name)
5096+
return createSimpleDetails(item, itemData.Name, docFormat)
50825097
case *completionDataKeyword:
50835098
if core.Some(request.keywordCompletions, func(c *lsproto.CompletionItem) bool {
50845099
return c.Label == itemData.Name
50855100
}) {
5086-
return createSimpleDetails(item, itemData.Name)
5101+
return createSimpleDetails(item, itemData.Name, docFormat)
50875102
}
50885103
return item
50895104
default:
@@ -5098,10 +5113,11 @@ func (l *LanguageService) getCompletionItemDetails(
50985113
checker,
50995114
symbolDetails.location,
51005115
actions,
5116+
docFormat,
51015117
)
51025118
case symbolCompletion.literal != nil:
51035119
literal := symbolCompletion.literal
5104-
return createSimpleDetails(item, completionNameForLiteral(file, preferences, *literal))
5120+
return createSimpleDetails(item, completionNameForLiteral(file, preferences, *literal), docFormat)
51055121
case symbolCompletion.cases != nil:
51065122
// !!! exhaustive case completions
51075123
return item
@@ -5110,7 +5126,7 @@ func (l *LanguageService) getCompletionItemDetails(
51105126
if core.Some(allKeywordCompletions(), func(c *lsproto.CompletionItem) bool {
51115127
return c.Label == itemData.Name
51125128
}) {
5113-
return createSimpleDetails(item, itemData.Name)
5129+
return createSimpleDetails(item, itemData.Name, docFormat)
51145130
}
51155131
return item
51165132
}
@@ -5257,14 +5273,16 @@ func (l *LanguageService) getAutoImportSymbolFromCompletionEntryData(ch *checker
52575273
func createSimpleDetails(
52585274
item *lsproto.CompletionItem,
52595275
name string,
5276+
docFormat lsproto.MarkupKind,
52605277
) *lsproto.CompletionItem {
5261-
return createCompletionDetails(item, name, "" /*documentation*/)
5278+
return createCompletionDetails(item, name, "" /*documentation*/, docFormat)
52625279
}
52635280

52645281
func createCompletionDetails(
52655282
item *lsproto.CompletionItem,
52665283
detail string,
52675284
documentation string,
5285+
docFormat lsproto.MarkupKind,
52685286
) *lsproto.CompletionItem {
52695287
// !!! fill in additionalTextEdits from code actions
52705288
if item.Detail == nil && detail != "" {
@@ -5273,7 +5291,7 @@ func createCompletionDetails(
52735291
if documentation != "" {
52745292
item.Documentation = &lsproto.StringOrMarkupContent{
52755293
MarkupContent: &lsproto.MarkupContent{
5276-
Kind: lsproto.MarkupKindMarkdown,
5294+
Kind: docFormat,
52775295
Value: documentation,
52785296
},
52795297
}
@@ -5294,19 +5312,20 @@ func (l *LanguageService) createCompletionDetailsForSymbol(
52945312
checker *checker.Checker,
52955313
location *ast.Node,
52965314
actions []codeAction,
5315+
docFormat lsproto.MarkupKind,
52975316
) *lsproto.CompletionItem {
52985317
details := make([]string, 0, len(actions)+1)
52995318
edits := make([]*lsproto.TextEdit, 0, len(actions))
53005319
for _, action := range actions {
53015320
details = append(details, action.description)
53025321
edits = append(edits, action.changes...)
53035322
}
5304-
quickInfo, documentation := l.getQuickInfoAndDocumentationForSymbol(checker, symbol, location)
5323+
quickInfo, documentation := l.getQuickInfoAndDocumentationForSymbol(checker, symbol, location, docFormat)
53055324
details = append(details, quickInfo)
53065325
if len(edits) != 0 {
53075326
item.AdditionalTextEdits = &edits
53085327
}
5309-
return createCompletionDetails(item, strings.Join(details, "\n\n"), documentation)
5328+
return createCompletionDetails(item, strings.Join(details, "\n\n"), documentation, docFormat)
53105329
}
53115330

53125331
// !!! snippets

internal/ls/findallreferences.go

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ func (l *LanguageService) ProvideReferences(ctx context.Context, params *lsproto
430430
return lsproto.LocationsOrNull{Locations: &locations}, nil
431431
}
432432

433-
func (l *LanguageService) ProvideImplementations(ctx context.Context, params *lsproto.ImplementationParams) (lsproto.ImplementationResponse, error) {
433+
func (l *LanguageService) ProvideImplementations(ctx context.Context, params *lsproto.ImplementationParams, clientSupportsLink bool) (lsproto.ImplementationResponse, error) {
434434
program, sourceFile := l.getProgramAndFile(params.TextDocument.Uri)
435435
position := int(l.converters.LineAndCharacterToPosition(sourceFile, params.Position))
436436
node := astnav.GetTouchingPropertyName(sourceFile, position)
@@ -452,6 +452,10 @@ func (l *LanguageService) ProvideImplementations(ctx context.Context, params *ls
452452
}
453453
}
454454

455+
if clientSupportsLink {
456+
links := l.convertEntriesToLocationLinks(entries)
457+
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{DefinitionLinks: &links}, nil
458+
}
455459
locations := l.convertEntriesToLocations(entries)
456460
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{Locations: &locations}, nil
457461
}
@@ -554,6 +558,44 @@ func (l *LanguageService) convertEntriesToLocations(entries []*ReferenceEntry) [
554558
return locations
555559
}
556560

561+
func (l *LanguageService) convertEntriesToLocationLinks(entries []*ReferenceEntry) []*lsproto.LocationLink {
562+
links := make([]*lsproto.LocationLink, len(entries))
563+
for i, entry := range entries {
564+
var targetSelectionRange, targetRange *lsproto.Range
565+
566+
// For entries with nodes, compute ranges directly from the node
567+
if entry.node != nil {
568+
sourceFile := ast.GetSourceFileOfNode(entry.node)
569+
entry.fileName = sourceFile.FileName()
570+
571+
// Get the selection range (the actual reference)
572+
selectionTextRange := getRangeOfNode(entry.node, sourceFile, nil /*endNode*/)
573+
targetSelectionRange = l.createLspRangeFromRange(selectionTextRange, sourceFile)
574+
575+
// Get the context range (broader scope including declaration context)
576+
contextNode := core.OrElse(getContextNode(entry.node), entry.node)
577+
contextTextRange := toContextRange(&selectionTextRange, sourceFile, contextNode)
578+
if contextTextRange != nil {
579+
targetRange = l.createLspRangeFromRange(*contextTextRange, sourceFile)
580+
} else {
581+
targetRange = targetSelectionRange
582+
}
583+
} else {
584+
// For range entries, use the pre-computed range
585+
l.resolveEntry(entry)
586+
targetSelectionRange = entry.textRange
587+
targetRange = targetSelectionRange
588+
}
589+
590+
links[i] = &lsproto.LocationLink{
591+
TargetUri: lsconv.FileNameToDocumentURI(entry.fileName),
592+
TargetRange: *targetRange,
593+
TargetSelectionRange: *targetSelectionRange,
594+
}
595+
}
596+
return links
597+
}
598+
557599
func (l *LanguageService) mergeReferences(program *compiler.Program, referencesToMerge ...[]*SymbolAndEntries) []*SymbolAndEntries {
558600
result := []*SymbolAndEntries{}
559601
getSourceFileIndexOfEntry := func(program *compiler.Program, entry *ReferenceEntry) int {

internal/ls/hover.go

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const (
1818
typeFormatFlags = checker.TypeFormatFlagsUseAliasDefinedOutsideCurrentScope
1919
)
2020

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

37+
var content string
38+
if contentFormat == lsproto.MarkupKindMarkdown {
39+
content = formatQuickInfo(quickInfo) + documentation
40+
} else {
41+
content = quickInfo + documentation
42+
}
43+
3744
return lsproto.HoverOrNull{
3845
Hover: &lsproto.Hover{
3946
Contents: lsproto.MarkupContentOrStringOrMarkedStringWithLanguageOrMarkedStrings{
4047
MarkupContent: &lsproto.MarkupContent{
41-
Kind: lsproto.MarkupKindMarkdown,
42-
Value: formatQuickInfo(quickInfo) + documentation,
48+
Kind: contentFormat,
49+
Value: content,
4350
},
4451
},
4552
Range: hoverRange,
4653
},
4754
}, nil
4855
}
4956

50-
func (l *LanguageService) getQuickInfoAndDocumentationForSymbol(c *checker.Checker, symbol *ast.Symbol, node *ast.Node) (string, string) {
57+
func (l *LanguageService) getQuickInfoAndDocumentationForSymbol(c *checker.Checker, symbol *ast.Symbol, node *ast.Node, contentFormat lsproto.MarkupKind) (string, string) {
5158
quickInfo, declaration := getQuickInfoAndDeclarationAtLocation(c, symbol, node)
5259
if quickInfo == "" {
5360
return "", ""
5461
}
62+
isMarkdown := contentFormat == lsproto.MarkupKindMarkdown
5563
var b strings.Builder
5664
if declaration != nil {
5765
if jsdoc := getJSDocOrTag(declaration); jsdoc != nil && !containsTypedefTag(jsdoc) {
58-
l.writeComments(&b, c, jsdoc.Comments())
66+
l.writeComments(&b, c, jsdoc.Comments(), isMarkdown)
5967
if jsdoc.Kind == ast.KindJSDoc {
6068
if tags := jsdoc.AsJSDoc().Tags; tags != nil {
6169
for _, tag := range tags.Nodes {
6270
if tag.Kind == ast.KindJSDocTypeTag {
6371
continue
6472
}
65-
b.WriteString("\n\n*@")
66-
b.WriteString(tag.TagName().Text())
67-
b.WriteString("*")
73+
b.WriteString("\n\n")
74+
if isMarkdown {
75+
b.WriteString("*@")
76+
b.WriteString(tag.TagName().Text())
77+
b.WriteString("*")
78+
} else {
79+
b.WriteString("@")
80+
b.WriteString(tag.TagName().Text())
81+
}
6882
switch tag.Kind {
6983
case ast.KindJSDocParameterTag, ast.KindJSDocPropertyTag:
7084
writeOptionalEntityName(&b, tag.Name())
@@ -90,7 +104,7 @@ func (l *LanguageService) getQuickInfoAndDocumentationForSymbol(c *checker.Check
90104
b.WriteString("— ")
91105
}
92106
}
93-
l.writeComments(&b, c, comments)
107+
l.writeComments(&b, c, comments, isMarkdown)
94108
}
95109
}
96110
}
@@ -425,24 +439,24 @@ func writeCode(b *strings.Builder, lang string, code string) {
425439
b.WriteByte('\n')
426440
}
427441

428-
func (l *LanguageService) writeComments(b *strings.Builder, c *checker.Checker, comments []*ast.Node) {
442+
func (l *LanguageService) writeComments(b *strings.Builder, c *checker.Checker, comments []*ast.Node, isMarkdown bool) {
429443
for _, comment := range comments {
430444
switch comment.Kind {
431445
case ast.KindJSDocText:
432446
b.WriteString(comment.Text())
433447
case ast.KindJSDocLink, ast.KindJSDocLinkPlain:
434-
l.writeJSDocLink(b, c, comment, false /*quote*/)
448+
l.writeJSDocLink(b, c, comment, false /*quote*/, isMarkdown)
435449
case ast.KindJSDocLinkCode:
436-
l.writeJSDocLink(b, c, comment, true /*quote*/)
450+
l.writeJSDocLink(b, c, comment, true /*quote*/, isMarkdown)
437451
}
438452
}
439453
}
440454

441-
func (l *LanguageService) writeJSDocLink(b *strings.Builder, c *checker.Checker, link *ast.Node, quote bool) {
455+
func (l *LanguageService) writeJSDocLink(b *strings.Builder, c *checker.Checker, link *ast.Node, quote bool, isMarkdown bool) {
442456
name := link.Name()
443457
text := strings.Trim(link.Text(), " ")
444458
if name == nil {
445-
writeQuotedString(b, text, quote)
459+
writeQuotedString(b, text, quote && isMarkdown)
446460
return
447461
}
448462
if ast.IsIdentifier(name) && (name.Text() == "http" || name.Text() == "https") && strings.HasPrefix(text, "://") {
@@ -455,7 +469,16 @@ func (l *LanguageService) writeJSDocLink(b *strings.Builder, c *checker.Checker,
455469
linkText = linkUri
456470
}
457471
}
458-
writeMarkdownLink(b, linkText, linkUri, quote)
472+
if isMarkdown {
473+
writeMarkdownLink(b, linkText, linkUri, quote)
474+
} else {
475+
writeQuotedString(b, linkText, false)
476+
if linkText != linkUri {
477+
b.WriteString(" (")
478+
b.WriteString(linkUri)
479+
b.WriteString(")")
480+
}
481+
}
459482
return
460483
}
461484
declarations := getDeclarationsFromLocation(c, name)
@@ -469,11 +492,15 @@ func (l *LanguageService) writeJSDocLink(b *strings.Builder, c *checker.Checker,
469492
if linkText == "" {
470493
linkText = getEntityNameString(name) + text[:prefixLen]
471494
}
472-
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)
473-
writeMarkdownLink(b, linkText, linkUri, quote)
495+
if isMarkdown {
496+
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)
497+
writeMarkdownLink(b, linkText, linkUri, quote)
498+
} else {
499+
writeQuotedString(b, linkText, false)
500+
}
474501
return
475502
}
476-
writeQuotedString(b, getEntityNameString(name)+" "+text, quote)
503+
writeQuotedString(b, getEntityNameString(name)+" "+text, quote && isMarkdown)
477504
}
478505

479506
func trimCommentPrefix(text string) string {

0 commit comments

Comments
 (0)