@@ -199,7 +199,7 @@ func (c *VSCode) readSettings(path string) ([]byte, map[string]interface{}) {
199199 }, localizer .Sprintf ("Failed to read VS Code settings" ))
200200 }
201201
202- settings , err := parseJSONCSettings (data )
202+ settings , err := parseJSONCSettings (append ([] byte ( nil ), data ... ) )
203203 if err != nil {
204204 c .Output ().FatalWithHintExamples ([][]string {
205205 {localizer .Sprintf ("Error" ), err .Error ()},
@@ -344,8 +344,27 @@ func ensureRootGroup(existing interface{}) []interface{} {
344344}
345345
346346func (c * VSCode ) getVSCodeSettingsPath (build string ) string {
347+ path , err := vsCodeSettingsPath (build )
348+ if err != nil {
349+ hint := [][]string {
350+ {localizer .Sprintf ("Set the HOME environment variable" ), "export HOME=/your/home" },
351+ }
352+ if runtime .GOOS == "windows" {
353+ hint = [][]string {
354+ {localizer .Sprintf ("Set the USERPROFILE environment variable" ), `set USERPROFILE=C:\Users\you` },
355+ }
356+ }
357+ c .Output ().FatalWithHintExamples (hint , localizer .Sprintf ("Could not resolve home directory: %s" , err .Error ()))
358+ }
359+ return path
360+ }
361+
362+ // vsCodeSettingsPath resolves the settings.json path for the given VS Code
363+ // build without exiting on failure, so callers like the uninstall cleanup
364+ // path can degrade gracefully when the home directory is unavailable.
365+ func vsCodeSettingsPath (build string ) (string , error ) {
347366 if testSettingsPathOverride != "" {
348- return testSettingsPathOverride
367+ return testSettingsPathOverride , nil
349368 }
350369
351370 appName := "Code"
@@ -354,20 +373,11 @@ func (c *VSCode) getVSCodeSettingsPath(build string) string {
354373 }
355374
356375 home , err := os .UserHomeDir ()
357- if err != nil || home == "" {
358- hint := [][]string {
359- {localizer .Sprintf ("Set the HOME environment variable" ), "export HOME=/your/home" },
360- }
361- if runtime .GOOS == "windows" {
362- hint = [][]string {
363- {localizer .Sprintf ("Set the USERPROFILE environment variable" ), `set USERPROFILE=C:\Users\you` },
364- }
365- }
366- reason := localizer .Sprintf ("empty home directory" )
367- if err != nil {
368- reason = err .Error ()
369- }
370- c .Output ().FatalWithHintExamples (hint , localizer .Sprintf ("Could not resolve home directory: %s" , reason ))
376+ if err != nil {
377+ return "" , err
378+ }
379+ if home == "" {
380+ return "" , fmt .Errorf ("empty home directory" )
371381 }
372382
373383 var configDir string
@@ -384,7 +394,7 @@ func (c *VSCode) getVSCodeSettingsPath(build string) string {
384394 configDir = linuxVSCodeConfigDir (home , appName , build , vsCodeExePath (build ), os .Getenv ("XDG_CONFIG_HOME" ))
385395 }
386396
387- return filepath .Join (configDir , "settings.json" )
397+ return filepath .Join (configDir , "settings.json" ), nil
388398}
389399
390400// linuxVSCodeConfigDir resolves the VS Code User config directory on Linux.
@@ -446,3 +456,80 @@ func isLocalEndpoint(endpoint sqlconfig.Endpoint) bool {
446456 asset := endpoint .AssetDetails
447457 return asset != nil && asset .ContainerDetails != nil
448458}
459+
460+ // RemoveContextFromVSCodeSettings deletes any mssql connection profile named
461+ // contextName from each known VS Code build's settings.json. It is best
462+ // effort: missing files, unresolvable home dirs, and parse/write errors are
463+ // swallowed so `sqlcmd delete` never fails on cleanup. Returns the list of
464+ // settings paths that were actually modified, so callers can report what
465+ // they cleaned up.
466+ func RemoveContextFromVSCodeSettings (contextName string ) []string {
467+ if contextName == "" {
468+ return nil
469+ }
470+ var cleaned []string
471+ seen := map [string ]bool {}
472+ for _ , build := range []string {"stable" , "insiders" } {
473+ path , err := vsCodeSettingsPath (build )
474+ if err != nil || path == "" || seen [path ] {
475+ continue
476+ }
477+ seen [path ] = true
478+ removed , err := removeProfileFromVSCodeSettings (path , contextName )
479+ if err == nil && removed {
480+ cleaned = append (cleaned , path )
481+ }
482+ }
483+ return cleaned
484+ }
485+
486+ // removeProfileFromVSCodeSettings rewrites settings.json with any
487+ // mssql.connections entry whose profileName matches contextName stripped out.
488+ // Returns (true, nil) when the file was modified, (false, nil) when no
489+ // matching profile was present or the file does not exist.
490+ func removeProfileFromVSCodeSettings (settingsPath , contextName string ) (bool , error ) {
491+ data , err := os .ReadFile (settingsPath )
492+ if err != nil {
493+ if os .IsNotExist (err ) {
494+ return false , nil
495+ }
496+ return false , err
497+ }
498+
499+ // parseJSONCSettings may mutate the input bytes via hujson.Standardize,
500+ // so peek on a copy and keep the pristine original for the JSONC-aware
501+ // rewrite below.
502+ settings , err := parseJSONCSettings (append ([]byte (nil ), data ... ))
503+ if err != nil {
504+ return false , err
505+ }
506+
507+ existing , ok := settings ["mssql.connections" ].([]interface {})
508+ if ! ok || len (existing ) == 0 {
509+ return false , nil
510+ }
511+
512+ filtered := make ([]interface {}, 0 , len (existing ))
513+ for _ , conn := range existing {
514+ if connMap , ok := conn .(map [string ]interface {}); ok {
515+ if name , _ := connMap ["profileName" ].(string ); name == contextName {
516+ continue
517+ }
518+ }
519+ filtered = append (filtered , conn )
520+ }
521+ if len (filtered ) == len (existing ) {
522+ return false , nil
523+ }
524+
525+ out , err := applyJSONCSettingsUpdates (data , map [string ]interface {}{
526+ "mssql.connections" : filtered ,
527+ })
528+ if err != nil {
529+ return false , err
530+ }
531+ if err := os .WriteFile (settingsPath , out , 0600 ); err != nil {
532+ return false , err
533+ }
534+ return true , nil
535+ }
0 commit comments