Skip to content

Commit 48da144

Browse files
feat: VSCode tools auto-install + go.mod version handling improvements (#554)
* [AB#542] Fix goenv vscode init Currently goenv vscode init added a random json flag, and that's not correct. Clean up and remove the line from the JSON. * feat: Add VSCode Go extension tools installation support Add comprehensive VSCode tools management with automatic installation support for all 8 required tools from the VSCode Go extension. New Features: - Add 'goenv tools install-vscode <version>' command to install all VSCode Go extension tools (gopls, dlv, vscgo, goplay, gomodifytags, impl, gotests, staticcheck) - Add --install-tools flag to 'goenv vscode init' command - Add --install-tools flag to 'goenv vscode setup' command Bug Fixes: - Fix vscgo package path to github.com/golang/vscode-go/vscgo - Fix goplay package path to github.com/haya14busa/goplay/cmd/goplay - Update documentation examples to use 'default-tools' instead of 'default' - Fix test assertion to match new command name Implementation Details: - Add VSCodeTools list with all 8 required tools - Add BuildVSCodeToolsConfig() helper for DRY configuration building - Add InstallVSCodeToolsForVersion() shared installation logic - Integrate tools installation into vscode init and setup workflows Closes #542 * test: Add comprehensive tests for go.mod forward compatibility Add test coverage for GetCurrentVersionResolved() with go.mod scenarios, particularly for Go 1.26+ behavior where 'go mod init' defaults to (N-1).0. Version selection strategy when go.mod is the version source: • Lowest minor version that satisfies the constraint (most conservative) • Highest patch of that minor (always want bug/security fixes) Test scenarios include: - PRIMARY: go.mod 1.25 + installed [1.25.4, 1.26.1, 1.27.0] → uses 1.25.4 (validates lowest satisfying minor, highest patch strategy) - Forward compatibility: go.mod 1.25 with only newer minors (1.26+) → uses minimum compatible (e.g., 1.26.0 over 1.27.0) - Highest patch preference: multiple patches of target minor available → always selects highest patch (e.g., 1.25.4 over 1.25.2) - Backward incompatibility: go.mod 1.26 with only older versions → errors correctly (can't use older version) - Edge cases: no versions installed, full version specs This validates the existing forward compatibility logic in GetCurrentVersionResolved() and findCompatibleVersion() that allows users with go.mod requiring Go 1.25 to successfully use Go 1.26+ while preferring exact minor matches when available. Related to issue #542 (go.mod version handling) * fix: Make cache timing tests more robust for Windows CI Increase TTL and sleep durations in TestCacheGetStats and TestCachePrune to account for Windows timing precision and slower CI environments. Before: - TestCacheGetStats: 100ms TTL with 50ms + 60ms sleeps - TestCachePrune: 50ms TTL with 60ms sleep After: - TestCacheGetStats: 200ms TTL with 50ms + 160ms sleeps - TestCachePrune: 100ms TTL with 120ms sleep This provides more margin for test execution overhead while maintaining the same test logic and coverage.
1 parent 547784a commit 48da144

10 files changed

Lines changed: 419 additions & 31 deletions

File tree

cmd/core/current.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ func runCurrent(cmd *cobra.Command, args []string) error {
5252
resolvedVersion, versionSpec, source, err := mgr.GetCurrentVersionResolved()
5353
if err != nil {
5454
if versionSpec != "" && source != "" {
55-
return fmt.Errorf("goenv: version '%s' is not installed (set by %s)", versionSpec, source)
55+
// Version specified but not installed - provide helpful error
56+
installed, _ := mgr.ListInstalledVersions()
57+
return errors.VersionNotInstalledDetailed(versionSpec, source, installed)
5658
}
5759
return errors.FailedTo("determine active version", err)
5860
}

cmd/integrations/vscode.go

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/go-nv/goenv/internal/config"
1515
"github.com/go-nv/goenv/internal/errors"
1616
"github.com/go-nv/goenv/internal/helptext"
17+
"github.com/go-nv/goenv/internal/tools"
1718
"github.com/go-nv/goenv/internal/utils"
1819
"github.com/go-nv/goenv/internal/vscode"
1920
"github.com/spf13/cobra"
@@ -49,10 +50,15 @@ The settings configure:
4950
- go.toolsGopath for Go tools installation
5051
- Recommended Go extension
5152
52-
This makes VS Code automatically detect and use goenv-managed Go versions.`,
53+
This makes VS Code automatically detect and use goenv-managed Go versions.
54+
55+
Optionally install VSCode Go extension tools with --install-tools flag.`,
5356
Example: ` # Initialize VS Code in current directory
5457
goenv vscode init
5558
59+
# Initialize and install VSCode tools
60+
goenv vscode init --install-tools
61+
5662
# Force overwrite existing configuration
5763
goenv vscode init --force
5864
@@ -152,11 +158,15 @@ This command performs all necessary configuration:
152158
2. Initializes workspace .vscode/settings.json
153159
3. Syncs with current Go version
154160
4. Validates configuration (doctor checks)
161+
5. Optionally installs VSCode Go extension tools (with --install-tools)
155162
156163
Perfect for both first-time setup and fixing integration issues.`,
157164
Example: ` # Complete VS Code setup in one command
158165
goenv vscode setup
159166
167+
# Setup and install all VSCode tools
168+
goenv vscode setup --install-tools
169+
160170
# Setup with advanced template
161171
goenv vscode setup --template advanced
162172
@@ -180,6 +190,7 @@ var VSCodeInitFlags struct {
180190
Launch bool
181191
TerminalEnv bool
182192
Devcontainer bool
193+
InstallTools bool
183194
}
184195

185196
var vscodeSyncFlags struct {
@@ -196,10 +207,11 @@ var vscodeDoctorFlags struct {
196207
}
197208

198209
var vscodeSetupFlags struct {
199-
template string
200-
dryRun bool
201-
strict bool
202-
json bool
210+
template string
211+
dryRun bool
212+
strict bool
213+
json bool
214+
installTools bool
203215
}
204216

205217
func init() {
@@ -223,6 +235,7 @@ func init() {
223235
vscodeInitCmd.Flags().BoolVar(&VSCodeInitFlags.Launch, "launch", false, "Generate launch.json for debugging")
224236
vscodeInitCmd.Flags().BoolVar(&VSCodeInitFlags.TerminalEnv, "terminal-env", false, "Configure integrated terminal environment")
225237
vscodeInitCmd.Flags().BoolVar(&VSCodeInitFlags.Devcontainer, "devcontainer", false, "Generate .devcontainer configuration")
238+
vscodeInitCmd.Flags().BoolVar(&VSCodeInitFlags.InstallTools, "install-tools", false, "Install VSCode Go extension tools after initialization")
226239

227240
// Sync flags
228241
vscodeSyncCmd.Flags().BoolVar(&vscodeSyncFlags.dryRun, "dry-run", false, "Preview changes without writing files")
@@ -239,6 +252,7 @@ func init() {
239252
vscodeSetupCmd.Flags().BoolVar(&vscodeSetupFlags.dryRun, "dry-run", false, "Preview what would be done")
240253
vscodeSetupCmd.Flags().BoolVar(&vscodeSetupFlags.strict, "strict", false, "Exit with error if doctor validation fails")
241254
vscodeSetupCmd.Flags().BoolVar(&vscodeSetupFlags.json, "json", false, "Output doctor results in JSON")
255+
vscodeSetupCmd.Flags().BoolVar(&vscodeSetupFlags.installTools, "install-tools", false, "Install VSCode Go extension tools after setup")
242256

243257
vscodeSetupCmd.SilenceUsage = true
244258
vscodeInitCmd.SilenceUsage = true
@@ -545,6 +559,18 @@ func InitializeVSCodeWorkspaceWithVersion(cmd *cobra.Command, version string) er
545559
}
546560
fmt.Fprintln(cmd.OutOrStdout(), "")
547561

562+
// Install VSCode tools if requested
563+
if VSCodeInitFlags.InstallTools && !VSCodeInitFlags.DryRun {
564+
fmt.Fprintln(cmd.OutOrStdout())
565+
fmt.Fprintf(cmd.OutOrStdout(), "%sInstalling VSCode Go extension tools...\n", utils.Emoji("🔧 "))
566+
fmt.Fprintln(cmd.OutOrStdout())
567+
568+
if err := installVSCodeTools(cmd, version); err != nil {
569+
fmt.Fprintf(cmd.OutOrStderr(), "%sFailed to install tools: %v\n", utils.Emoji("⚠️ "), err)
570+
fmt.Fprintln(cmd.OutOrStderr(), "You can install them later with: goenv tools install-vscode <version>")
571+
}
572+
}
573+
548574
return nil
549575
}
550576

@@ -562,15 +588,13 @@ func generateSettings(template string) (VSCodeSettings, error) {
562588
"go.goroot": "${env:GOROOT}",
563589
"go.gopath": "${env:GOPATH}",
564590
"go.toolsGopath": homeEnvVar + "/go/tools",
565-
"goenv.autoSync": false, // Set to true to auto-update on version change
566591
}, nil
567592

568593
case "advanced":
569594
return VSCodeSettings{
570595
"go.goroot": "${env:GOROOT}",
571596
"go.gopath": "${env:GOPATH}",
572597
"go.toolsGopath": homeEnvVar + "/go/tools",
573-
"goenv.autoSync": false, // Set to true to auto-update on version change
574598
"go.toolsManagement.autoUpdate": true,
575599
"go.formatTool": "gofumpt",
576600
"go.lintTool": "golangci-lint",
@@ -598,7 +622,6 @@ func generateSettings(template string) (VSCodeSettings, error) {
598622
"go.goroot": "${env:GOROOT}",
599623
"go.gopath": "${env:GOPATH}",
600624
"go.toolsGopath": homeEnvVar + "/go/tools",
601-
"goenv.autoSync": false, // Set to true to auto-update on version change
602625
"go.inferGopath": false,
603626
"go.formatTool": "gofumpt",
604627
"go.testExplorer.enable": true,
@@ -1345,8 +1368,53 @@ func runVSCodeSetup(cmd *cobra.Command, args []string) error {
13451368
utils.Emoji("⚠️ "))
13461369
}
13471370

1371+
// Step 5: Install VSCode tools if requested
1372+
if vscodeSetupFlags.installTools && !vscodeSetupFlags.dryRun {
1373+
fmt.Fprintln(cmd.OutOrStdout())
1374+
fmt.Fprintf(cmd.OutOrStdout(), "%sStep 5/5: Installing VSCode Go extension tools\n",
1375+
utils.Emoji("🔧 "))
1376+
1377+
ctx := cmdutil.GetContexts(cmd)
1378+
mgr := ctx.Manager
1379+
version, _, _, err := mgr.GetCurrentVersionResolved()
1380+
if err != nil {
1381+
fmt.Fprintf(cmd.OutOrStderr(), "%sFailed to get current Go version: %v\n", utils.Emoji("⚠️ "), err)
1382+
fmt.Fprintln(cmd.OutOrStderr(), "You can install tools later with: goenv tools install-vscode <version>")
1383+
} else {
1384+
if err := installVSCodeTools(cmd, version); err != nil {
1385+
fmt.Fprintf(cmd.OutOrStderr(), "%sFailed to install tools: %v\n", utils.Emoji("⚠️ "), err)
1386+
fmt.Fprintln(cmd.OutOrStderr(), "You can install them later with: goenv tools install-vscode <version>")
1387+
}
1388+
}
1389+
}
1390+
13481391
fmt.Fprintf(cmd.OutOrStdout(), "\n%sSetup complete! VS Code is ready to use with goenv.\n",
13491392
utils.Emoji("✅ "))
13501393

13511394
return nil
13521395
}
1396+
1397+
// installVSCodeTools installs all VSCode Go extension tools for the given Go version
1398+
func installVSCodeTools(cmd *cobra.Command, goVersion string) error {
1399+
ctx := cmdutil.GetContexts(cmd)
1400+
cfg := ctx.Config
1401+
mgr := ctx.Manager
1402+
1403+
// Validate version is installed
1404+
if !mgr.IsVersionInstalled(goVersion) {
1405+
return fmt.Errorf("Go %s is not installed", goVersion)
1406+
}
1407+
1408+
versionPath := cfg.SafeResolvePath(goVersion)
1409+
1410+
// Install VSCode tools
1411+
if err := tools.InstallVSCodeToolsForVersion(goVersion, cfg.Root, versionPath, false); err != nil {
1412+
return err
1413+
}
1414+
1415+
fmt.Fprintln(cmd.OutOrStdout())
1416+
fmt.Fprintf(cmd.OutOrStdout(), "%sVSCode tools installed successfully\n", utils.Emoji("✅ "))
1417+
fmt.Fprintf(cmd.OutOrStdout(), "%sRun 'goenv rehash' to make tools available as shims\n", utils.Emoji("💡 "))
1418+
1419+
return nil
1420+
}

cmd/tools/default_tools.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ Common use cases:
2424
- Reduce manual setup after installing new Go versions
2525
2626
Examples:
27-
goenv tools default list # Show configured tools
28-
goenv tools default init # Create default config file
29-
goenv tools default enable # Enable auto-installation
30-
goenv tools default disable # Disable auto-installation
31-
goenv tools default install 1.25.2 # Install tools for specific version`,
27+
goenv tools default-tools list # Show configured tools
28+
goenv tools default-tools init # Create default config file
29+
goenv tools default-tools enable # Enable auto-installation
30+
goenv tools default-tools disable # Disable auto-installation
31+
goenv tools default-tools install 1.25.2 # Install tools for specific version`,
3232
}
3333

3434
var defaultToolsListCmd = &cobra.Command{
@@ -104,7 +104,7 @@ func runDefaultToolsList(cmd *cobra.Command, args []string) error {
104104
// Check if config exists
105105
if utils.FileNotExists(configPath) {
106106
fmt.Fprintln(cmd.OutOrStdout(), "No default tools configuration found.")
107-
fmt.Fprintln(cmd.OutOrStdout(), "Run 'goenv tools default init' to create one.")
107+
fmt.Fprintln(cmd.OutOrStdout(), "Run 'goenv tools default-tools init' to create one.")
108108
return nil
109109
}
110110

@@ -248,7 +248,7 @@ func runInstallTools(cmd *cobra.Command, args []string) error {
248248

249249
if len(toolConfig.Tools) == 0 {
250250
fmt.Fprintln(cmd.OutOrStdout(), "No tools configured to install.")
251-
fmt.Fprintln(cmd.OutOrStdout(), "Run 'goenv tools default init' to create a default configuration.")
251+
fmt.Fprintln(cmd.OutOrStdout(), "Run 'goenv tools default-tools init' to create a default configuration.")
252252
return nil
253253
}
254254

@@ -307,7 +307,7 @@ func runVerify(cmd *cobra.Command, args []string) error {
307307

308308
if len(missing) > 0 {
309309
fmt.Fprintln(cmd.OutOrStdout())
310-
fmt.Fprintf(cmd.OutOrStdout(), "To install missing tools: goenv tools default install %s\n", goVersion)
310+
fmt.Fprintf(cmd.OutOrStdout(), "To install missing tools: goenv tools default-tools install %s\n", goVersion)
311311
}
312312

313313
return nil

cmd/tools/default_tools_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func TestDefaultToolsList_NoConfig(t *testing.T) {
2727

2828
output := buf.String()
2929
assert.Contains(t, output, "No default tools configuration found", "Expected 'No default tools configuration found' message %v", output)
30-
assert.Contains(t, output, "goenv tools default init", "Expected init suggestion %v", output)
30+
assert.Contains(t, output, "goenv tools default-tools init", "Expected init suggestion %v", output)
3131
}
3232

3333
func TestDefaultToolsList_WithConfig(t *testing.T) {

cmd/tools/install_vscode.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package tools
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/go-nv/goenv/internal/cmdutil"
7+
"github.com/go-nv/goenv/internal/errors"
8+
"github.com/go-nv/goenv/internal/tools"
9+
"github.com/go-nv/goenv/internal/utils"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
var installVSCodeCmd = &cobra.Command{
14+
Use: "install-vscode <version>",
15+
Short: "Install VSCode Go extension tools for a Go version",
16+
Long: `Installs all tools required by the VSCode Go extension for a specific Go version.
17+
18+
This command installs the following tools as documented at:
19+
https://github.com/golang/vscode-go/wiki/tools
20+
21+
Tools installed:
22+
- gopls (Go language server)
23+
- dlv (Delve debugger)
24+
- vscgo (VSCode Go utilities)
25+
- goplay (Go Playground support)
26+
- gomodifytags (Struct tag editor)
27+
- impl (Interface stub generator)
28+
- gotests (Test generator)
29+
- staticcheck (Static analysis tool)
30+
31+
These tools provide the full IntelliSense, debugging, and code editing
32+
capabilities of the VSCode Go extension.
33+
34+
Examples:
35+
# Install VSCode tools for specific version
36+
goenv tools install-vscode 1.25.2
37+
38+
# Install for current version
39+
goenv tools install-vscode $(goenv version-name)
40+
41+
# Show what would be installed
42+
goenv tools install-vscode 1.25.2 --dry-run`,
43+
Args: cobra.ExactArgs(1),
44+
RunE: runInstallVSCode,
45+
}
46+
47+
var installVSCodeFlags struct {
48+
dryRun bool
49+
verbose bool
50+
}
51+
52+
func init() {
53+
installVSCodeCmd.Flags().BoolVar(&installVSCodeFlags.dryRun, "dry-run", false, "Show what would be installed without installing")
54+
installVSCodeCmd.Flags().BoolVarP(&installVSCodeFlags.verbose, "verbose", "v", false, "Show detailed installation output")
55+
}
56+
57+
func runInstallVSCode(cmd *cobra.Command, args []string) error {
58+
ctx := cmdutil.GetContexts(cmd)
59+
cfg := ctx.Config
60+
mgr := ctx.Manager
61+
goVersion := args[0]
62+
63+
// Validate version is installed
64+
if !mgr.IsVersionInstalled(goVersion) {
65+
return fmt.Errorf("Go %s is not installed. Run 'goenv install %s' first", goVersion, goVersion)
66+
}
67+
68+
versionPath := cfg.SafeResolvePath(goVersion)
69+
70+
fmt.Fprintf(cmd.OutOrStdout(), "Installing VSCode Go extension tools for Go %s...\n", goVersion)
71+
fmt.Fprintln(cmd.OutOrStdout())
72+
73+
if installVSCodeFlags.dryRun {
74+
fmt.Fprintf(cmd.OutOrStdout(), "%sDry run mode - no tools will be installed\n", utils.Emoji("ℹ️ "))
75+
fmt.Fprintln(cmd.OutOrStdout())
76+
}
77+
78+
// Build list of tools to install
79+
toolPackages := make([]string, 0, len(tools.VSCodeTools))
80+
for _, toolName := range tools.VSCodeTools {
81+
pkg := tools.NormalizePackagePath(toolName)
82+
toolPackages = append(toolPackages, pkg)
83+
}
84+
85+
// Show what will be installed
86+
fmt.Fprintln(cmd.OutOrStdout(), "Tools to install:")
87+
for i, pkg := range toolPackages {
88+
toolName := tools.ExtractToolName(pkg)
89+
fmt.Fprintf(cmd.OutOrStdout(), " %d. %-15s %s\n", i+1, toolName, pkg)
90+
}
91+
fmt.Fprintln(cmd.OutOrStdout())
92+
93+
if installVSCodeFlags.dryRun {
94+
return nil
95+
}
96+
97+
// Install VSCode tools
98+
if err := tools.InstallVSCodeToolsForVersion(goVersion, cfg.Root, versionPath, installVSCodeFlags.verbose); err != nil {
99+
return errors.FailedTo("install VSCode tools", err)
100+
}
101+
102+
fmt.Fprintln(cmd.OutOrStdout())
103+
fmt.Fprintf(cmd.OutOrStdout(), "%sVSCode tools installed successfully\n", utils.Emoji("✅ "))
104+
fmt.Fprintf(cmd.OutOrStdout(), "%sRun 'goenv rehash' to make tools available as shims\n", utils.Emoji("💡 "))
105+
106+
return nil
107+
}

cmd/tools/tools.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,12 @@ func init() {
5454
cmdpkg.RootCmd.AddCommand(toolsCmd)
5555

5656
// Add subcommands (each defined in their own file)
57-
toolsCmd.AddCommand(installToolsCmd) // from install_tools.go
58-
toolsCmd.AddCommand(listToolsCmd) // from list_tools.go
59-
toolsCmd.AddCommand(updateToolsCmd) // from update_tools.go
60-
toolsCmd.AddCommand(syncToolsCmd) // from sync_tools.go
61-
toolsCmd.AddCommand(defaultToolsCmd) // from default_tools.go
57+
toolsCmd.AddCommand(installToolsCmd) // from install_tools.go
58+
toolsCmd.AddCommand(installVSCodeCmd) // from install_vscode.go
59+
toolsCmd.AddCommand(listToolsCmd) // from list_tools.go
60+
toolsCmd.AddCommand(updateToolsCmd) // from update_tools.go
61+
toolsCmd.AddCommand(syncToolsCmd) // from sync_tools.go
62+
toolsCmd.AddCommand(defaultToolsCmd) // from default_tools.go
6263

6364
// Add uninstall command
6465
cfg := config.Load()

internal/errors/goenv.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ func VersionNotInstalledDetailed(version, source string, installedVersions []str
3030

3131
sb.WriteString("\n")
3232

33+
// Add context for go.mod files (Go 1.26+ behavior)
34+
if source != "" && strings.Contains(source, "go.mod") {
35+
sb.WriteString("Note: Starting in Go 1.26, 'go mod init' defaults to setting the go\n")
36+
sb.WriteString("directive to one version behind for backward compatibility.\n")
37+
sb.WriteString("This is normal and expected behavior.\n")
38+
sb.WriteString("\n")
39+
}
40+
3341
// Suggest installation
3442
sb.WriteString("To install this version:\n")
3543
sb.WriteString(fmt.Sprintf(" goenv install %s\n", version))

0 commit comments

Comments
 (0)