diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index 13cfa1c2bc..6bf03992c9 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -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"}, @@ -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 { @@ -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 } diff --git a/internal/ls/completions.go b/internal/ls/completions.go index b80b044315..e64663af07 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -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, @@ -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( @@ -5054,6 +5068,7 @@ func (l *LanguageService) getCompletionItemDetails( file, position, contextToken, + docFormat, ) } @@ -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: @@ -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 @@ -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 } @@ -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 != "" { @@ -5272,7 +5290,7 @@ func createCompletionDetails( if documentation != "" { item.Documentation = &lsproto.StringOrMarkupContent{ MarkupContent: &lsproto.MarkupContent{ - Kind: lsproto.MarkupKindMarkdown, + Kind: docFormat, Value: documentation, }, } @@ -5293,6 +5311,7 @@ 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)) @@ -5300,12 +5319,12 @@ func (l *LanguageService) createCompletionDetailsForSymbol( 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 diff --git a/internal/ls/findallreferences.go b/internal/ls/findallreferences.go index a31c9c2e87..933a15252f 100644 --- a/internal/ls/findallreferences.go +++ b/internal/ls/findallreferences.go @@ -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) @@ -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 } @@ -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 { diff --git a/internal/ls/hover.go b/internal/ls/hover.go index 77099e8808..f2b867dc01 100644 --- a/internal/ls/hover.go +++ b/internal/ls/hover.go @@ -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 { @@ -28,18 +28,25 @@ 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, @@ -47,24 +54,31 @@ func (l *LanguageService) ProvideHover(ctx context.Context, documentURI lsproto. }, 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()) @@ -90,7 +104,7 @@ func (l *LanguageService) getQuickInfoAndDocumentationForSymbol(c *checker.Check b.WriteString("— ") } } - l.writeComments(&b, c, comments) + l.writeComments(&b, c, comments, isMarkdown) } } } @@ -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, "://") { @@ -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) @@ -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 { diff --git a/internal/ls/string_completions.go b/internal/ls/string_completions.go index 749ad880c6..cb34509554 100644 --- a/internal/ls/string_completions.go +++ b/internal/ls/string_completions.go @@ -670,6 +670,7 @@ func (l *LanguageService) getStringLiteralCompletionDetails( file *ast.SourceFile, position int, contextToken *ast.Node, + docFormat lsproto.MarkupKind, ) *lsproto.CompletionItem { if contextToken == nil || !ast.IsStringLiteralLike(contextToken) { return item @@ -683,7 +684,7 @@ func (l *LanguageService) getStringLiteralCompletionDetails( if completions == nil { return item } - return l.stringLiteralCompletionDetails(item, name, contextToken, completions, file, checker) + return l.stringLiteralCompletionDetails(item, name, contextToken, completions, file, checker, docFormat) } func (l *LanguageService) stringLiteralCompletionDetails( @@ -693,27 +694,28 @@ func (l *LanguageService) stringLiteralCompletionDetails( completion *stringLiteralCompletions, file *ast.SourceFile, checker *checker.Checker, + docFormat lsproto.MarkupKind, ) *lsproto.CompletionItem { switch { case completion.fromPaths != nil: pathCompletions := completion.fromPaths for _, pathCompletion := range pathCompletions { if pathCompletion.name == name { - return createCompletionDetails(item, name, "" /*documentation*/) + return createCompletionDetails(item, name, "" /*documentation*/, docFormat) } } 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*/, docFormat) } } 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*/, docFormat) } } } diff --git a/internal/ls/symbols.go b/internal/ls/symbols.go index ca56bc4951..98215dca31 100644 --- a/internal/ls/symbols.go +++ b/internal/ls/symbols.go @@ -18,10 +18,52 @@ import ( "github.com/microsoft/typescript-go/internal/stringutil" ) -func (l *LanguageService) ProvideDocumentSymbols(ctx context.Context, documentURI lsproto.DocumentUri) (lsproto.DocumentSymbolResponse, error) { +func (l *LanguageService) ProvideDocumentSymbols(ctx context.Context, documentURI lsproto.DocumentUri, hierarchicalSupport bool) (lsproto.DocumentSymbolResponse, error) { _, file := l.getProgramAndFile(documentURI) - symbols := l.getDocumentSymbolsForChildren(ctx, file.AsNode()) - return lsproto.SymbolInformationsOrDocumentSymbolsOrNull{DocumentSymbols: &symbols}, nil + if hierarchicalSupport { + symbols := l.getDocumentSymbolsForChildren(ctx, file.AsNode()) + return lsproto.SymbolInformationsOrDocumentSymbolsOrNull{DocumentSymbols: &symbols}, nil + } + // Client doesn't support hierarchical document symbols, return flat SymbolInformation array + symbolInfos := l.getDocumentSymbolInformations(ctx, file, documentURI) + symbolInfoPtrs := make([]*lsproto.SymbolInformation, len(symbolInfos)) + for i := range symbolInfos { + symbolInfoPtrs[i] = &symbolInfos[i] + } + return lsproto.SymbolInformationsOrDocumentSymbolsOrNull{SymbolInformations: &symbolInfoPtrs}, nil +} + +// getDocumentSymbolInformations converts hierarchical DocumentSymbols to a flat SymbolInformation array +func (l *LanguageService) getDocumentSymbolInformations(ctx context.Context, file *ast.SourceFile, documentURI lsproto.DocumentUri) []lsproto.SymbolInformation { + // First get hierarchical symbols + docSymbols := l.getDocumentSymbolsForChildren(ctx, file.AsNode()) + + // Flatten the hierarchy + var result []lsproto.SymbolInformation + var flatten func(symbols []*lsproto.DocumentSymbol, containerName *string) + flatten = func(symbols []*lsproto.DocumentSymbol, containerName *string) { + for _, symbol := range symbols { + info := lsproto.SymbolInformation{ + Name: symbol.Name, + Kind: symbol.Kind, + Location: lsproto.Location{ + Uri: documentURI, + Range: symbol.Range, + }, + ContainerName: containerName, + Tags: symbol.Tags, + Deprecated: symbol.Deprecated, + } + result = append(result, info) + + // Recursively flatten children with this symbol as container + if symbol.Children != nil && len(*symbol.Children) > 0 { + flatten(*symbol.Children, &symbol.Name) + } + } + } + flatten(docSymbols, nil) + return result } func (l *LanguageService) getDocumentSymbolsForChildren(ctx context.Context, node *ast.Node) []*lsproto.DocumentSymbol { diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 6e24ffd59a..0287340ffb 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -808,7 +808,7 @@ func (s *Server) handleDocumentDiagnostic(ctx context.Context, ls *ls.LanguageSe } func (s *Server) handleHover(ctx context.Context, ls *ls.LanguageService, params *lsproto.HoverParams) (lsproto.HoverResponse, error) { - return ls.ProvideHover(ctx, params.TextDocument.Uri, params.Position) + return ls.ProvideHover(ctx, params.TextDocument.Uri, params.Position, getHoverContentFormat(s.initializeParams)) } func (s *Server) handleSignatureHelp(ctx context.Context, languageService *ls.LanguageService, params *lsproto.SignatureHelpParams) (lsproto.SignatureHelpResponse, error) { @@ -836,7 +836,7 @@ func (s *Server) handleReferences(ctx context.Context, ls *ls.LanguageService, p func (s *Server) handleImplementations(ctx context.Context, ls *ls.LanguageService, params *lsproto.ImplementationParams) (lsproto.ImplementationResponse, error) { // goToImplementation - return ls.ProvideImplementations(ctx, params) + return ls.ProvideImplementations(ctx, params, getImplementationClientSupportsLink(s.initializeParams)) } func (s *Server) handleCompletion(ctx context.Context, languageService *ls.LanguageService, params *lsproto.CompletionParams) (lsproto.CompletionResponse, error) { @@ -903,7 +903,7 @@ func (s *Server) handleWorkspaceSymbol(ctx context.Context, params *lsproto.Work } func (s *Server) handleDocumentSymbol(ctx context.Context, ls *ls.LanguageService, params *lsproto.DocumentSymbolParams) (lsproto.DocumentSymbolResponse, error) { - return ls.ProvideDocumentSymbols(ctx, params.TextDocument.Uri) + return ls.ProvideDocumentSymbols(ctx, params.TextDocument.Uri, getDocumentSymbolClientSupportsHierarchical(s.initializeParams)) } func (s *Server) handleRename(ctx context.Context, ls *ls.LanguageService, params *lsproto.RenameParams) (lsproto.RenameResponse, error) { @@ -992,3 +992,32 @@ func getTypeDefinitionClientSupportsLink(params *lsproto.InitializeParams) bool } return ptrIsTrue(params.Capabilities.TextDocument.TypeDefinition.LinkSupport) } + +func getImplementationClientSupportsLink(params *lsproto.InitializeParams) bool { + if params == nil || params.Capabilities == nil || params.Capabilities.TextDocument == nil || + params.Capabilities.TextDocument.Implementation == nil { + return false + } + return ptrIsTrue(params.Capabilities.TextDocument.Implementation.LinkSupport) +} + +func getDocumentSymbolClientSupportsHierarchical(params *lsproto.InitializeParams) bool { + if params == nil || params.Capabilities == nil || params.Capabilities.TextDocument == nil || + params.Capabilities.TextDocument.DocumentSymbol == nil { + return false + } + return ptrIsTrue(params.Capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport) +} + +func getHoverContentFormat(params *lsproto.InitializeParams) lsproto.MarkupKind { + if params == nil || params.Capabilities == nil || params.Capabilities.TextDocument == nil || params.Capabilities.TextDocument.Hover == nil || params.Capabilities.TextDocument.Hover.ContentFormat == nil { + // Default to plaintext if no preference specified + return lsproto.MarkupKindPlainText + } + formats := *params.Capabilities.TextDocument.Hover.ContentFormat + if len(formats) == 0 { + return lsproto.MarkupKindPlainText + } + // Return the first (most preferred) format + return formats[0] +}