-
-
Notifications
You must be signed in to change notification settings - Fork 1
Add some other options #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
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.
There was a problem hiding this 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.
| var capturedProject = project; | ||
| projectItem.Click += (s, args) => ViewModel.OpenRecentProject(instance, capturedProject); |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
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.
| var capturedProject = project; | |
| projectItem.Click += (s, args) => ViewModel.OpenRecentProject(instance, capturedProject); | |
| projectItem.Click += (s, args) => ViewModel.OpenRecentProject(instance, project); |
| 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." | ||
| }; |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
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.
| 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); |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
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.
| 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>(); | ||
|
|
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
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.
| 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; |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
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.
| 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) | ||
| }); | ||
| } | ||
| } | ||
| } |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
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(...)'.
| 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) | ||
| }); | ||
| } | ||
| } | ||
| } |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
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(...)'.
| 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); | ||
| } | ||
| } | ||
| } | ||
| } |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
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(...)'.
| if (!string.IsNullOrEmpty(instance.ProductPath) && File.Exists(instance.ProductPath)) | ||
| { | ||
| if (TryExtractIcon(instance.ProductPath, cachePath)) | ||
| { | ||
| return cachePath; | ||
| } | ||
| } |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
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.
| if (valueElement.TryGetProperty("LocalProperties", out var localProps)) | ||
| { | ||
| if (localProps.TryGetProperty("FullPath", out var fullPathElement)) | ||
| { | ||
| fullPath = fullPathElement.GetString(); | ||
| } |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
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.
| 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(); |
Description
Type of Change
feat- New featureRelated Issues
Checklist
Screenshots (if applicable)
Additional Notes