Skip to content
Merged
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
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
39 changes: 29 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 All @@ -5054,6 +5068,7 @@ func (l *LanguageService) getCompletionItemDetails(
file,
position,
contextToken,
docFormat,
)
}

Expand All @@ -5073,16 +5088,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 +5112,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 +5125,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 +5272,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 +5290,7 @@ func createCompletionDetails(
if documentation != "" {
item.Documentation = &lsproto.StringOrMarkupContent{
MarkupContent: &lsproto.MarkupContent{
Kind: lsproto.MarkupKindMarkdown,
Kind: docFormat,
Value: documentation,
},
}
Expand All @@ -5293,19 +5311,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
44 changes: 43 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,10 @@ func (l *LanguageService) ProvideImplementations(ctx context.Context, params *ls
}
}

if clientSupportsLink {
links := l.convertEntriesToLocationLinks(entries)
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{DefinitionLinks: &links}, nil
}
locations := l.convertEntriesToLocations(entries)
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{Locations: &locations}, nil
}
Expand Down Expand Up @@ -554,6 +558,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
Loading