Skip to content

Conversation

@eincioch
Copy link

@eincioch eincioch commented Jan 9, 2026

Description

Type of Change

  • feat - New feature

Related Issues

Checklist

  • My code follows the project's code style
  • I have tested my changes locally
  • I have updated documentation if needed
  • My commits follow the conventional commit format
  • This PR has a descriptive title using conventional commit format

Screenshots (if applicable)

image

Additional Notes

Se agrega detección y visualización de Visual Studio Code y VS Code Insiders junto a instancias tradicionales de Visual Studio.
Incluye nuevos valores en los enums `VSSku` y `VSVersion`, servicios de detección específicos, ajustes en la visualización y lógica de lanzamiento, y exclusión de VS Code en la extracción de iconos. También se adapta el parsing de canales para soportar los modos Stable e Insiders de VS Code.
- Añadida detección automática de VS Code y VS Code Insiders, mostrando extensiones instaladas y acceso rápido a carpetas de datos y extensiones.
- Integración con Visual Studio Installer: modificar, actualizar y abrir el dashboard desde el menú contextual.
- Menú contextual adaptado según tipo de instancia (VS/VS Code) con nuevas opciones específicas.
- Soporte para iconos personalizados de VS Code; añadido script PowerShell para extraer iconos automáticamente.
- Documentación ampliada: guías de integración VS Code, VS Installer y gestión de iconos.
- Actualizada estructura del proyecto en README y añadidos archivos placeholder para iconos.
- Roadmap actualizado con futuras mejoras y troubleshooting detallado.
Se ha reemplazado la imagen del menú de acciones rápidas de instancias en el README.md por una nueva versión (instance-list-menu-v2.png), reflejando mejoras visuales o funcionales en la interfaz. La nueva imagen ha sido añadida al repositorio.
Se ha agregado a eincioch como nuevo colaborador en el README.md, mostrando su avatar y enlace a su perfil de GitHub en la lista de contribuyentes.
Se refactoriza el servicio para buscar proyectos recientes de Visual Studio únicamente en ApplicationPrivateSettings.xml, recorriendo todas las carpetas de configuración (hives) que coincidan con la versión principal. Se elimina la lógica de búsqueda en otros archivos JSON y en el registro de Windows. Se simplifica y mejora el análisis del XML y del JSON embebido, asegurando que solo se incluyan soluciones y proyectos válidos existentes. Se mantiene la lógica para VS Code y se elimina código duplicado. Mejora la compatibilidad con múltiples perfiles y configuraciones.
Copilot AI review requested due to automatic review settings January 9, 2026 16:39
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds comprehensive VS Code integration and several new features to the Visual Studio Toolbox application. Despite the vague title "Add some other options", this is a substantial feature addition that includes:

  • VS Code Detection & Integration: Full support for VS Code and VS Code Insiders with extension detection
  • Recent Projects Feature: Quick access to recently opened solutions/folders for both VS and VS Code
  • VS Installer Integration: Direct access to modify, update, and manage VS installations
  • Enhanced Documentation: Comprehensive guides for all new features

Reviewed changes

Copilot reviewed 21 out of 23 changed files in this pull request and generated 18 comments.

Show a summary per file
File Description
MainViewModel.cs Added new services and commands for VS Code, recent projects, and VS Installer integration
MainPage.xaml.cs Implemented context menus with recent projects and VS Code-specific options
VSCodeDetectionService.cs New service to detect VS Code installations and extensions
RecentProjectsService.cs New service to retrieve recent projects from VS and VS Code
IconExtractionService.cs Enhanced to support VS Code icon extraction
extract_vscode_icons.ps1 PowerShell script to extract icons from VS Code executables
RecentProject.cs, VSVersion.cs, VSSku.cs, VisualStudioInstance.cs Model updates to support VS Code
Documentation files Comprehensive guides for new features (5 new markdown files)
README.md Updated to document all new features and capabilities
Asset files Placeholder files for VS Code icons and updated screenshot

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +157 to +158
var capturedProject = project;
projectItem.Click += (s, args) => ViewModel.OpenRecentProject(instance, capturedProject);
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variables 'capturedProject' on lines 94, 157 are created to avoid closure issues, but the current C# version supports proper closure capture. These intermediate variables are unnecessary and can be simplified by directly using 'project' in the lambda expression.

Suggested change
var capturedProject = project;
projectItem.Click += (s, args) => ViewModel.OpenRecentProject(instance, capturedProject);
projectItem.Click += (s, args) => ViewModel.OpenRecentProject(instance, project);

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +103
StatusText = totalVSCode > 0
? $"{totalVS} Visual Studio + {totalVSCode} VS Code instance{(totalVSCode != 1 ? "s" : "")} found."
: launchables.Count switch
{
0 => "No Visual Studio instances found.",
1 => "1 Visual Studio instance found.",
_ => $"{launchables.Count} Visual Studio instances found."
};
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The switch expression on line 96-103 could be cleaner by using a ternary operator since it only has two cases with a default. Consider: StatusText = totalVSCode > 0 ? $"{totalVS} Visual Studio + {totalVSCode} VS Code instance{(totalVSCode != 1 ? "s" : "")} found." : GetVisualStudioStatusMessage(launchables.Count); with a helper method for the VS-only message.

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +141
var recentProjects = ViewModel.GetRecentProjects(instance, 10);
if (recentProjects.Count > 0)
{
var recentSubmenu = new MenuFlyoutSubItem
{
Text = "Recent Folders",
Icon = new FontIcon { Glyph = "\uE823", Foreground = new SolidColorBrush(Color.FromArgb(255, 0, 122, 204)) }
};

foreach (var project in recentProjects)
{
var projectItem = new MenuFlyoutItem
{
Text = project.DisplayName,
Icon = new FontIcon { Glyph = project.IsFolder ? "\uE8B7" : "\uE8A5" }
};
var capturedProject = project;
projectItem.Click += (s, args) => ViewModel.OpenRecentProject(instance, capturedProject);
recentSubmenu.Items.Add(projectItem);
}

flyout.Items.Add(recentSubmenu);
flyout.Items.Add(new MenuFlyoutSeparator());
}

var openExtensionsItem = new MenuFlyoutItem
{
Text = "Open Extensions Folder",
Icon = new FontIcon { Glyph = "\uE74C", Foreground = new SolidColorBrush(Color.FromArgb(255, 0, 122, 204)) }
};
openExtensionsItem.Click += (s, args) => ViewModel.OpenVSCodeExtensionsFolderCommand.Execute(instance);
flyout.Items.Add(openExtensionsItem);

var openNewWindowItem = new MenuFlyoutItem
{
Text = "Open New Window",
Icon = new FontIcon { Glyph = "\uE8A7", Foreground = new SolidColorBrush(Color.FromArgb(255, 0, 122, 204)) }
};
openNewWindowItem.Click += (s, args) => ViewModel.LaunchInstanceCommand.Execute(instance);
flyout.Items.Add(openNewWindowItem);

flyout.Items.Add(new MenuFlyoutSeparator());

var openExplorerItem = new MenuFlyoutItem
{
Text = "Open Installation Folder",
Icon = new FontIcon { Glyph = "\uE838", Foreground = new SolidColorBrush(Color.FromArgb(255, 234, 179, 8)) }
};
openExplorerItem.Click += (s, args) => ViewModel.OpenInstanceFolderCommand.Execute(instance);
flyout.Items.Add(openExplorerItem);

var appDataItem = new MenuFlyoutItem
{
Text = "Open VS Code Data Folder",
Icon = new FontIcon { Glyph = "\uE8B7", Foreground = new SolidColorBrush(Color.FromArgb(255, 139, 92, 246)) }
};
appDataItem.Click += (s, args) => ViewModel.OpenAppDataFolderCommand.Execute(instance);
flyout.Items.Add(appDataItem);

return;
}

// Recent projects for Visual Studio
var vsRecentProjects = ViewModel.GetRecentProjects(instance, 10);
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method GetRecentProjects is called with magic number 10 for maxCount in lines 78, 141. Consider defining this as a named constant (e.g., private const int DefaultMaxRecentProjects = 10;) to improve code readability and maintainability.

Copilot uses AI. Check for mistakes.
Comment on lines +50 to +106
catch
{
// Ignore errors reading recent projects
}

// Remove duplicates and sort by last accessed
return recentProjects
.GroupBy(p => p.Path.ToLowerInvariant())
.Select(g => g.OrderByDescending(p => p.LastAccessed).First())
.Where(p => p.Exists)
.OrderByDescending(p => p.LastAccessed)
.Take(maxCount)
.ToList();
}

private static IEnumerable<RecentProject> ParseApplicationPrivateSettingsXml(string settingsPath)
{
var projects = new List<RecentProject>();

try
{
var xmlContent = File.ReadAllText(settingsPath);
var doc = System.Xml.Linq.XDocument.Parse(xmlContent);

// Find CodeContainers.Offline collection
var codeContainersNode = doc.Descendants("collection")
.FirstOrDefault(c => c.Attribute("name")?.Value == "CodeContainers.Offline");

if (codeContainersNode is null)
return projects;

// Get the value element
var valueNode = codeContainersNode.Elements("value")
.FirstOrDefault(v => v.Attribute("name")?.Value == "value");

if (valueNode is null)
return projects;

var jsonContent = valueNode.Value?.Trim();
if (string.IsNullOrEmpty(jsonContent))
return projects;

// Parse JSON to extract projects
projects.AddRange(ParseCodeContainersJsonFromXml(jsonContent));
}
catch
{
// Ignore parsing errors
}

return projects;
}

private static IEnumerable<RecentProject> ParseCodeContainersJsonFromXml(string jsonContent)
{
var projects = new List<RecentProject>();

Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty catch blocks that suppress all exceptions without logging make debugging difficult and can hide serious errors. The catches on lines 52, 95, 103, and 182 should at least log the exception or have a comment explaining why it's safe to ignore.

Copilot uses AI. Check for mistakes.
Comment on lines +300 to +318
private static string CleanVSCodePath(string path)
{
if (path.StartsWith("file:///", StringComparison.OrdinalIgnoreCase))
{
path = path[8..];
if (path.Length > 2 && path[0] == '/' && path[2] == ':')
{
path = path[1..];
}
}
else if (path.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
path = path[7..];
}

path = Uri.UnescapeDataString(path);
path = path.Replace('/', '\\');

return path;
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CleanVSCodePath method uses string slicing with hardcoded indices (e.g., path[8..], path[1..]) which could throw IndexOutOfRangeException if the path is shorter than expected. Add length checks before slicing to prevent runtime errors.

Copilot uses AI. Check for mistakes.
Comment on lines +212 to +228
foreach (var workspace in workspaces.EnumerateArray())
{
var path = workspace.GetString();
if (!string.IsNullOrEmpty(path))
{
path = CleanVSCodePath(path);
if (Directory.Exists(path) || File.Exists(path))
{
projects.Add(new RecentProject
{
Name = Path.GetFileName(path.TrimEnd('/', '\\')),
Path = path,
LastAccessed = GetFileLastAccess(path)
});
}
}
}
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +234 to +250
foreach (var folder in folders.EnumerateArray())
{
var path = folder.GetString();
if (!string.IsNullOrEmpty(path))
{
path = CleanVSCodePath(path);
if (Directory.Exists(path))
{
projects.Add(new RecentProject
{
Name = Path.GetFileName(path.TrimEnd('/', '\\')),
Path = path,
LastAccessed = GetFileLastAccess(path)
});
}
}
}
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +84 to +99
foreach (var dir in directories)
{
var dirName = Path.GetFileName(dir);
if (!string.IsNullOrEmpty(dirName) && !dirName.StartsWith('.'))
{
var parts = dirName.Split('-');
if (parts.Length >= 2)
{
var extensionName = string.Join("-", parts.Take(parts.Length - 1));
if (!extensions.Contains(extensionName))
{
extensions.Add(extensionName);
}
}
}
}
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +58 to +64
if (!string.IsNullOrEmpty(instance.ProductPath) && File.Exists(instance.ProductPath))
{
if (TryExtractIcon(instance.ProductPath, cachePath))
{
return cachePath;
}
}
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 'if' statements can be combined.

Copilot uses AI. Check for mistakes.
Comment on lines +123 to +128
if (valueElement.TryGetProperty("LocalProperties", out var localProps))
{
if (localProps.TryGetProperty("FullPath", out var fullPathElement))
{
fullPath = fullPathElement.GetString();
}
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 'if' statements can be combined.

Suggested change
if (valueElement.TryGetProperty("LocalProperties", out var localProps))
{
if (localProps.TryGetProperty("FullPath", out var fullPathElement))
{
fullPath = fullPathElement.GetString();
}
if (valueElement.TryGetProperty("LocalProperties", out var localProps)
&& localProps.TryGetProperty("FullPath", out var fullPathElement))
{
fullPath = fullPathElement.GetString();

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant