1+ package hover
2+
3+ import (
4+ "context"
5+ "fmt"
6+ "path/filepath"
7+ "sort"
8+ "strings"
9+
10+ "github.com/shopware/shopware-lsp/internal/lsp"
11+ "github.com/shopware/shopware-lsp/internal/lsp/protocol"
12+ "github.com/shopware/shopware-lsp/internal/snippet"
13+ treesitterhelper "github.com/shopware/shopware-lsp/internal/tree_sitter_helper"
14+ )
15+
16+ type SnippetHoverProvider struct {
17+ snippetIndexer * snippet.SnippetIndexer
18+ projectRoot string
19+ }
20+
21+ func NewSnippetHoverProvider (projectRoot string , lspServer * lsp.Server ) * SnippetHoverProvider {
22+ snippetIndexer , _ := lspServer .GetIndexer ("snippet.indexer" )
23+ return & SnippetHoverProvider {
24+ snippetIndexer : snippetIndexer .(* snippet.SnippetIndexer ),
25+ projectRoot : projectRoot ,
26+ }
27+ }
28+
29+ func (p * SnippetHoverProvider ) GetHover (ctx context.Context , params * protocol.HoverParams ) (* protocol.Hover , error ) {
30+ if params .Node == nil {
31+ return nil , nil
32+ }
33+
34+ // Handle both .twig and .php files
35+ switch strings .ToLower (filepath .Ext (params .TextDocument .URI )) {
36+ case ".twig" :
37+ return p .twigHover (ctx , params )
38+ case ".php" :
39+ return p .phpHover (ctx , params )
40+ default :
41+ return nil , nil
42+ }
43+ }
44+
45+ func (p * SnippetHoverProvider ) twigHover (_ context.Context , params * protocol.HoverParams ) (* protocol.Hover , error ) {
46+ if treesitterhelper .TwigTransPattern ().Matches (params .Node , params .DocumentContent ) {
47+ snippetKey := treesitterhelper .GetNodeText (params .Node , params .DocumentContent )
48+ return p .createHoverForSnippet (snippetKey , params )
49+ }
50+ return nil , nil
51+ }
52+
53+ func (p * SnippetHoverProvider ) phpHover (_ context.Context , params * protocol.HoverParams ) (* protocol.Hover , error ) {
54+ if treesitterhelper .IsPHPThisMethodCall ("trans" ).Matches (params .Node , params .DocumentContent ) {
55+ snippetKey := treesitterhelper .GetNodeText (params .Node , params .DocumentContent )
56+ return p .createHoverForSnippet (snippetKey , params )
57+ }
58+ return nil , nil
59+ }
60+
61+ func (p * SnippetHoverProvider ) createHoverForSnippet (snippetKey string , params * protocol.HoverParams ) (* protocol.Hover , error ) {
62+ snippets , err := p .snippetIndexer .GetFrontendSnippet (snippetKey )
63+ if err != nil || len (snippets ) == 0 {
64+ return nil , nil
65+ }
66+
67+ // Sort snippets by file path for consistent display
68+ sort .Slice (snippets , func (i , j int ) bool {
69+ return snippets [i ].File < snippets [j ].File
70+ })
71+
72+ // Build markdown content showing all translations
73+ var markdownContent strings.Builder
74+ markdownContent .WriteString (fmt .Sprintf ("**Snippet**: `%s`\n \n " , snippetKey ))
75+ markdownContent .WriteString ("**Translations**:\n \n " )
76+
77+ for _ , snippet := range snippets {
78+ // Extract locale from file path (e.g., "de-DE" or "en-GB" from the path)
79+ locale := extractLocaleFromPath (snippet .File )
80+
81+ // Make path relative to project root
82+ displayPath , err := filepath .Rel (p .projectRoot , snippet .File )
83+ if err != nil {
84+ displayPath = snippet .File
85+ }
86+
87+ // Format the translation entry
88+ markdownContent .WriteString (fmt .Sprintf ("- **%s**: `%s`\n " , locale , snippet .Text ))
89+ markdownContent .WriteString (fmt .Sprintf (" <small>%s:%d</small>\n \n " , displayPath , snippet .Line ))
90+ }
91+
92+ return & protocol.Hover {
93+ Contents : protocol.MarkupContent {
94+ Kind : protocol .Markdown ,
95+ Value : markdownContent .String (),
96+ },
97+ Range : & protocol.Range {
98+ Start : protocol.Position {
99+ Line : params .Position .Line ,
100+ Character : params .Position .Character ,
101+ },
102+ End : protocol.Position {
103+ Line : params .Position .Line ,
104+ Character : params .Position .Character + len (snippetKey ),
105+ },
106+ },
107+ }, nil
108+ }
109+
110+ // extractLocaleFromPath tries to extract the locale from the file path
111+ // e.g., "/path/to/Resources/snippet/de-DE/snippet.json" -> "de-DE"
112+ // e.g., "/path/to/Resources/snippet/de_DE/storefront.de-DE.json" -> "de-DE"
113+ func extractLocaleFromPath (path string ) string {
114+ // Normalize path separators to forward slashes for consistent handling
115+ // Handle both Unix and Windows path separators
116+ normalizedPath := strings .ReplaceAll (path , "\\ " , "/" )
117+
118+ // First, try to extract from filename (e.g., "storefront.de-DE.json")
119+ parts := strings .Split (normalizedPath , "/" )
120+ if len (parts ) > 0 {
121+ filename := parts [len (parts )- 1 ]
122+ if strings .Contains (filename , "." ) {
123+ filenameParts := strings .Split (filename , "." )
124+ for _ , part := range filenameParts {
125+ if isLocalePattern (part ) {
126+ return normalizeLocale (part )
127+ }
128+ }
129+ }
130+ }
131+
132+ // Then, try to extract from directory structure
133+ for i , part := range parts {
134+ // Check if this part looks like a locale
135+ if isLocalePattern (part ) {
136+ return normalizeLocale (part )
137+ }
138+ // Also check if we're in a snippet directory
139+ if part == "snippet" && i + 1 < len (parts ) {
140+ // The next part might be the locale
141+ nextPart := parts [i + 1 ]
142+ if isLocalePattern (nextPart ) {
143+ return normalizeLocale (nextPart )
144+ }
145+ }
146+ }
147+
148+ return "unknown"
149+ }
150+
151+ // isLocalePattern checks if a string matches common locale patterns
152+ func isLocalePattern (s string ) bool {
153+ // Check for patterns like "de-DE", "en-GB", "de_DE", "en_GB"
154+ if len (s ) == 5 && (s [2 ] == '-' || s [2 ] == '_' ) {
155+ return true
156+ }
157+ // Check for patterns like "de", "en", "fr"
158+ if len (s ) == 2 {
159+ return true
160+ }
161+ return false
162+ }
163+
164+ // normalizeLocale converts locale to standard format (e.g., "de_DE" -> "de-DE")
165+ func normalizeLocale (locale string ) string {
166+ return strings .ReplaceAll (locale , "_" , "-" )
167+ }
0 commit comments