@@ -12,14 +12,23 @@ import (
1212)
1313
1414type ideSpec struct {
15- AppName string
16- IDEType string
17- Vendor string
18- AppPath string // macOS: /Applications/X.app
19- BinaryPath string // macOS: relative to AppPath
20- WinPaths []string // Windows: candidate install dirs (may contain %ENVVAR% and glob patterns)
21- WinBinary string // Windows: binary relative to install dir
22- VersionFlag string
15+ AppName string
16+ IDEType string
17+ Vendor string
18+ AppPath string // macOS: /Applications/X.app
19+ BinaryPath string // macOS: relative to AppPath
20+ WinPaths []string // Windows: candidate install dirs (may contain %ENVVAR% and glob patterns)
21+ WinBinary string // Windows: binary relative to install dir
22+ VersionFlag string
23+ RegistryName string // Windows: override for registry search if DisplayName differs from AppName
24+ }
25+
26+ // registrySearchName returns the name to use for registry searches.
27+ func (s ideSpec ) registrySearchName () string {
28+ if s .RegistryName != "" {
29+ return s .RegistryName
30+ }
31+ return s .AppName
2332}
2433
2534var ideDefinitions = []ideSpec {
@@ -97,9 +106,10 @@ var ideDefinitions = []ideSpec{
97106 WinPaths : []string {`%PROGRAMFILES%\JetBrains\GoLand *` },
98107 },
99108 {
100- AppName : "Rider" , IDEType : "rider" , Vendor : "JetBrains" ,
101- AppPath : "/Applications/Rider.app" ,
102- WinPaths : []string {`%PROGRAMFILES%\JetBrains\JetBrains Rider *` },
109+ AppName : "Rider" , IDEType : "rider" , Vendor : "JetBrains" ,
110+ AppPath : "/Applications/Rider.app" ,
111+ WinPaths : []string {`%PROGRAMFILES%\JetBrains\JetBrains Rider *` },
112+ RegistryName : "JetBrains Rider" ,
103113 },
104114 {
105115 AppName : "PhpStorm" , IDEType : "phpstorm" , Vendor : "JetBrains" ,
@@ -198,6 +208,7 @@ func (d *IDEDetector) detectDarwin(ctx context.Context, spec ideSpec) (model.IDE
198208}
199209
200210func (d * IDEDetector ) detectWindows (ctx context.Context , spec ideSpec ) (model.IDE , bool ) {
211+ // Phase 1: Try hardcoded paths (fast, no registry)
201212 for _ , winPath := range spec .WinPaths {
202213 resolved := resolveEnvPath (d .exec , winPath )
203214
@@ -206,39 +217,81 @@ func (d *IDEDetector) detectWindows(ctx context.Context, spec ideSpec) (model.ID
206217 continue
207218 }
208219
209- version := "unknown"
210-
211- // Try version from binary
212- if spec .WinBinary != "" && spec .VersionFlag != "" {
213- binaryFull := filepath .Join (installDir , spec .WinBinary )
214- if d .exec .FileExists (binaryFull ) {
215- version = runVersionCmd (ctx , d .exec , binaryFull , spec .VersionFlag )
216- }
217- }
218-
219- // Fallback: product-info.json (JetBrains IDEs)
220- if version == "unknown" {
221- version = readProductInfoVersion (d .exec , filepath .Join (installDir , "product-info.json" ))
222- }
223-
224- // Fallback: .eclipseproduct
225- if version == "unknown" {
226- version = readEclipseProductVersion (d .exec , filepath .Join (installDir , ".eclipseproduct" ))
227- }
228-
229- // Fallback: registry
230- if version == "unknown" {
231- version = readRegistryVersion (ctx , d .exec , spec .AppName )
232- }
220+ version := d .resolveWindowsVersion (ctx , spec , installDir )
221+ return model.IDE {
222+ IDEType : spec .IDEType , Version : version , InstallPath : installDir ,
223+ Vendor : spec .Vendor , IsInstalled : true ,
224+ }, true
225+ }
233226
227+ // Phase 2: Registry fallback — discover install path from Uninstall keys.
228+ // Catches IDEs installed at non-standard paths (e.g., D:\Tools\VSCode).
229+ if installDir , version , ok := d .discoverViaRegistry (ctx , spec ); ok {
234230 return model.IDE {
235231 IDEType : spec .IDEType , Version : version , InstallPath : installDir ,
236232 Vendor : spec .Vendor , IsInstalled : true ,
237233 }, true
238234 }
235+
239236 return model.IDE {}, false
240237}
241238
239+ // resolveWindowsVersion determines the IDE version using multiple strategies.
240+ func (d * IDEDetector ) resolveWindowsVersion (ctx context.Context , spec ideSpec , installDir string ) string {
241+ version := d .resolveWindowsVersionFromDir (ctx , spec , installDir )
242+ if version == "unknown" {
243+ version = readRegistryVersion (ctx , d .exec , spec .registrySearchName ())
244+ }
245+ return version
246+ }
247+
248+ // resolveWindowsVersionFromDir tries binary, product-info.json, and .eclipseproduct.
249+ // Does NOT query the registry (caller handles that to avoid redundant queries).
250+ func (d * IDEDetector ) resolveWindowsVersionFromDir (ctx context.Context , spec ideSpec , installDir string ) string {
251+ version := "unknown"
252+
253+ if spec .WinBinary != "" && spec .VersionFlag != "" {
254+ binaryFull := filepath .Join (installDir , spec .WinBinary )
255+ if d .exec .FileExists (binaryFull ) {
256+ version = runVersionCmd (ctx , d .exec , binaryFull , spec .VersionFlag )
257+ }
258+ }
259+
260+ if version == "unknown" {
261+ version = readProductInfoVersion (d .exec , filepath .Join (installDir , "product-info.json" ))
262+ }
263+
264+ if version == "unknown" {
265+ version = readEclipseProductVersion (d .exec , filepath .Join (installDir , ".eclipseproduct" ))
266+ }
267+
268+ return version
269+ }
270+
271+ // discoverViaRegistry attempts to find an IDE's install location from Windows
272+ // Uninstall registry keys. This is a fallback for IDEs installed at non-standard paths.
273+ func (d * IDEDetector ) discoverViaRegistry (ctx context.Context , spec ideSpec ) (string , string , bool ) {
274+ info := readRegistryInstallInfo (ctx , d .exec , spec .registrySearchName ())
275+
276+ if info .InstallLocation == "" {
277+ return "" , "" , false
278+ }
279+
280+ if ! d .exec .DirExists (info .InstallLocation ) {
281+ return "" , "" , false
282+ }
283+
284+ // Resolve version from the discovered directory
285+ version := d .resolveWindowsVersionFromDir (ctx , spec , info .InstallLocation )
286+
287+ // Use registry DisplayVersion as final fallback (avoids redundant registry query)
288+ if version == "unknown" && info .Version != "" {
289+ version = info .Version
290+ }
291+
292+ return info .InstallLocation , version , true
293+ }
294+
242295// resolveInstallDir resolves a Windows path to an install directory.
243296// Supports glob patterns (e.g., "C:\Program Files\JetBrains\GoLand *")
244297// for IDEs that embed version numbers in folder names.
@@ -351,8 +404,15 @@ func readPlistVersion(ctx context.Context, exec executor.Executor, plistPath str
351404 return "unknown"
352405}
353406
354- // readRegistryVersion searches Windows Uninstall registry keys for DisplayVersion.
355- func readRegistryVersion (ctx context.Context , exec executor.Executor , appName string ) string {
407+ // registryInstallInfo holds version and install path from Windows Uninstall registry keys.
408+ type registryInstallInfo struct {
409+ Version string
410+ InstallLocation string
411+ }
412+
413+ // readRegistryInstallInfo searches Windows Uninstall registry keys and extracts
414+ // both DisplayVersion and InstallLocation for the given app name.
415+ func readRegistryInstallInfo (ctx context.Context , exec executor.Executor , appName string ) registryInstallInfo {
356416 for _ , root := range []string {
357417 `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall` ,
358418 `HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall` ,
@@ -362,16 +422,40 @@ func readRegistryVersion(ctx context.Context, exec executor.Executor, appName st
362422 if err != nil {
363423 continue
364424 }
365- // Parse "DisplayVersion REG_SZ x.y.z" from reg query output
425+
426+ var info registryInstallInfo
366427 for _ , line := range strings .Split (stdout , "\n " ) {
367428 line = strings .TrimSpace (line )
368429 if strings .Contains (line , "DisplayVersion" ) {
369430 parts := strings .Fields (line )
370431 if len (parts ) >= 3 {
371- return parts [len (parts )- 1 ]
432+ info .Version = parts [len (parts )- 1 ]
433+ }
434+ }
435+ if strings .Contains (line , "InstallLocation" ) {
436+ // InstallLocation may contain spaces, so split on REG_SZ and trim
437+ parts := strings .SplitN (line , "REG_SZ" , 2 )
438+ if len (parts ) == 2 {
439+ loc := strings .TrimSpace (parts [1 ])
440+ if loc != "" {
441+ info .InstallLocation = loc
442+ }
372443 }
373444 }
374445 }
446+
447+ if info .Version != "" || info .InstallLocation != "" {
448+ return info
449+ }
450+ }
451+ return registryInstallInfo {}
452+ }
453+
454+ // readRegistryVersion searches Windows Uninstall registry keys for DisplayVersion.
455+ func readRegistryVersion (ctx context.Context , exec executor.Executor , appName string ) string {
456+ info := readRegistryInstallInfo (ctx , exec , appName )
457+ if info .Version != "" {
458+ return info .Version
375459 }
376460 return "unknown"
377461}
0 commit comments