Skip to content

Commit a724001

Browse files
committed
Fix Open in VSCode for remote codespaces and WSL workspaces in Open in Explorer
Fix "Open in Explorer" for WSL and remote Codespaces Hide "Open in Explorer" for remote workspace types Validate Windows paths using new IsPathValid helper Improve FileUriParser to support WSL URIs and UNC paths Add WslPathHelper to extract distro and paths from WSL URIs
1 parent c9ff325 commit a724001

File tree

12 files changed

+176
-66
lines changed

12 files changed

+176
-66
lines changed

WorkspaceLauncherForVSCode/Classes/VisualStudioCodeWorkspace.cs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public class VisualStudioCodeWorkspace
2424
public VisualStudioCodeWorkspaceSource Source { get; set; }
2525
public List<string> SourcePath { get; set; } = new();
2626
public string WorkspaceName { get; set; } = "";
27-
public string VSTypeString { get; set; } = "";
27+
public VsCodeRemoteType VsCodeRemoteType { get; set; }
2828
public string WorkspaceTypeString { get; set; } = "";
2929
public DetailsElement[] Details { get; set; } = [];
3030
public int Frequency { get; set; }
@@ -100,11 +100,15 @@ public void SetVSCodeType()
100100
if (Path == null) return;
101101
if (Path.StartsWith("vscode-remote://wsl", System.StringComparison.OrdinalIgnoreCase))
102102
{
103-
VSTypeString = "WSL";
103+
VsCodeRemoteType = VsCodeRemoteType.WSL;
104104
}
105105
else if (Path.StartsWith("vscode-remote://", System.StringComparison.OrdinalIgnoreCase))
106106
{
107-
VSTypeString = "Remote";
107+
VsCodeRemoteType = VsCodeRemoteType.Remote;
108+
}
109+
else
110+
{
111+
VsCodeRemoteType = VsCodeRemoteType.Local;
108112
}
109113
}
110114

@@ -139,9 +143,9 @@ public void SetVSCodeMetadata()
139143
{
140144
if (Path == null) return;
141145
var typeTags = new List<Tag>() { new Tag(WorkspaceTypeString) };
142-
if (VSTypeString != "")
146+
if (VsCodeRemoteType != VsCodeRemoteType.Local)
143147
{
144-
typeTags.Add(new Tag(VSTypeString));
148+
typeTags.Add(new Tag(VsCodeRemoteType.ToString()));
145149
}
146150

147151
var detailsElements = new List<DetailsElement>();
@@ -178,9 +182,9 @@ public void SetVSMetadata()
178182
{
179183
if (Path == null) return;
180184
var typeTags = new List<Tag>() { new Tag(WorkspaceTypeString) };
181-
if (VSTypeString != "")
185+
if (VsCodeRemoteType != VsCodeRemoteType.Local)
182186
{
183-
typeTags.Add(new Tag(VSTypeString));
187+
typeTags.Add(new Tag(VsCodeRemoteType.ToString()));
184188
}
185189

186190
var detailsElements = new List<DetailsElement>();

WorkspaceLauncherForVSCode/Commands/CommandHelpers.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ namespace WorkspaceLauncherForVSCode.Commands
77
{
88
internal static class CommandHelpers
99
{
10-
public static CommandResult? IsPathNotFound(string path)
10+
public static CommandResult? IsPathValid(string path)
1111
{
1212
if (!Directory.Exists(path) && !File.Exists(path))
1313
{
14-
new ToastStatusMessage($"Path does not exist").Show();
14+
new ToastStatusMessage($"Path does not exist: {path}").Show();
1515
return CommandResult.KeepOpen();
1616
}
1717
return null;
Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,59 @@
1-
// Copyright (c) Microsoft Corporation
2-
// The Microsoft Corporation licenses this file to you under the MIT license.
3-
// See the LICENSE file in the project root for more information.
1+
// Modifications copyright (c) 2025 tanchekwei
2+
// Licensed under the MIT License. See the LICENSE file in the project root for details.
43
using System.IO;
54
using Microsoft.CmdPal.Ext.System.Helpers;
65
using Microsoft.CommandPalette.Extensions.Toolkit;
6+
using WorkspaceLauncherForVSCode.Enums;
77

88
namespace WorkspaceLauncherForVSCode.Commands
99
{
1010
public sealed partial class OpenInExplorerCommand : InvokableCommand
1111
{
1212
private readonly VisualStudioCodeWorkspace? workspace;
13-
private readonly VisualStudioCodePage? page;
14-
public OpenInExplorerCommand(string arguments, VisualStudioCodeWorkspace? workspace, VisualStudioCodePage? page, string name = "Open in Explorer", string path = "explorer.exe", string? workingDir = null, OpenInShellHelper.ShellRunAsType runAs = OpenInShellHelper.ShellRunAsType.None, bool runWithHiddenWindow = false)
13+
private readonly string _path;
14+
private string _arguments;
15+
16+
public OpenInExplorerCommand(string arguments, VisualStudioCodeWorkspace? workspace, string name = "Open in Explorer", string path = "explorer.exe")
1517
{
1618
Name = name;
1719
_path = path;
1820
_arguments = arguments;
19-
_workingDir = workingDir;
20-
_runAs = runAs;
21-
_runWithHiddenWindow = runWithHiddenWindow;
2221
Icon = Classes.Icon.FileExplorer;
2322
this.workspace = workspace;
24-
this.page = page;
2523
}
2624

2725
public override CommandResult Invoke()
2826
{
29-
if (_arguments == null)
27+
string pathToOpen = workspace?.WindowsPath ?? _arguments;
28+
29+
if (string.IsNullOrEmpty(pathToOpen))
3030
{
3131
return CommandResult.Dismiss();
3232
}
33-
var pathNotFoundResult = CommandHelpers.IsPathNotFound(_arguments);
34-
if (pathNotFoundResult != null)
33+
if (workspace?.VsCodeRemoteType == VsCodeRemoteType.Remote)
3534
{
36-
return pathNotFoundResult;
35+
new ToastStatusMessage($"Not supported.").Show();
36+
return CommandResult.KeepOpen();
3737
}
3838
if (workspace?.WorkspaceType == Enums.WorkspaceType.Solution)
3939
{
40-
_arguments = Path.GetDirectoryName(_arguments);
40+
pathToOpen = Path.GetDirectoryName(pathToOpen) ?? string.Empty;
41+
}
42+
43+
if (string.IsNullOrEmpty(pathToOpen))
44+
{
45+
new ToastStatusMessage($"Path does not exist").Show();
46+
return CommandResult.KeepOpen();
4147
}
42-
OpenInShellHelper.OpenInShell(_path, _arguments);
48+
49+
var pathInvalidResult = CommandHelpers.IsPathValid(pathToOpen);
50+
if (pathInvalidResult != null)
51+
{
52+
return pathInvalidResult;
53+
}
54+
55+
OpenInShellHelper.OpenInShell(_path, pathToOpen);
4356
return CommandResult.Dismiss();
4457
}
45-
46-
private string _path;
47-
private string? _workingDir;
48-
private string? _arguments;
49-
private OpenInShellHelper.ShellRunAsType _runAs;
50-
private bool _runWithHiddenWindow;
5158
}
5259
}

WorkspaceLauncherForVSCode/Commands/OpenSolutionCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public override CommandResult Invoke()
2626
{
2727
if (Workspace.WindowsPath is not null)
2828
{
29-
var pathNotFoundResult = CommandHelpers.IsPathNotFound(Workspace.WindowsPath);
29+
var pathNotFoundResult = CommandHelpers.IsPathValid(Workspace.WindowsPath);
3030
if (pathNotFoundResult != null)
3131
{
3232
return pathNotFoundResult;

WorkspaceLauncherForVSCode/Commands/OpenVisualStudioCodeCommand.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,18 @@ public override CommandResult Invoke()
5050
return CommandResult.Confirm(new ConfirmationArgs { Title = "Error", Description = "Cannot open a solution with this command." });
5151
}
5252

53-
if (Workspace.WindowsPath is null || Workspace.Path is null || Workspace.VSCodeInstance is null)
53+
if (Workspace.Path is null || Workspace.VSCodeInstance is null)
5454
{
5555
return CommandResult.Confirm(new ConfirmationArgs { Title = "Error", Description = "Workspace path, or instance is null. Cannot open." });
5656
}
5757

58-
if (Workspace.VSTypeString != "WSL" && Workspace.VSTypeString != "Remote")
58+
var pathToValidate = Workspace.WindowsPath ?? Workspace.Path;
59+
if (Workspace.VsCodeRemoteType != VsCodeRemoteType.Remote)
5960
{
60-
var pathNotFoundResult = CommandHelpers.IsPathNotFound(Workspace.WindowsPath);
61-
if (pathNotFoundResult != null)
61+
var pathInvalidResult = CommandHelpers.IsPathValid(pathToValidate);
62+
if (pathInvalidResult != null)
6263
{
63-
return pathNotFoundResult;
64+
return pathInvalidResult;
6465
}
6566
}
6667

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace WorkspaceLauncherForVSCode.Enums
2+
{
3+
public enum VsCodeRemoteType
4+
{
5+
Local,
6+
WSL,
7+
Remote
8+
}
9+
}

WorkspaceLauncherForVSCode/Helpers/FileUriParser.cs

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,68 @@
33
using System;
44
using System.Diagnostics.CodeAnalysis;
55

6-
namespace WorkspaceLauncherForVSCode.Helpers;
7-
8-
public static class FileUriParser
6+
namespace WorkspaceLauncherForVSCode.Helpers
97
{
10-
public static bool TryConvertToWindowsPath(string fileUri, [NotNullWhen(true)] out string? windowsPath)
11-
{
12-
windowsPath = null;
8+
public static class FileUriParser
9+
{
10+
public static bool TryConvertToWindowsPath(string fileUri, [NotNullWhen(true)] out string? windowsPath)
11+
{
12+
windowsPath = null;
1313

14-
if (string.IsNullOrWhiteSpace(fileUri))
15-
return false;
14+
if (string.IsNullOrWhiteSpace(fileUri))
15+
{
16+
return false;
17+
}
1618

17-
// Fix lowercase %3A issues in drive letters
18-
fileUri = fileUri.Replace("%3A", ":").Replace("%3a", ":");
19+
// Case 1: Handle vscode-remote WSL URIs (e.g., vscode-remote://wsl+Ubuntu/home/user/project)
20+
if (fileUri.StartsWith("vscode-remote://wsl%2", StringComparison.OrdinalIgnoreCase))
21+
{
22+
return WslPathHelper.TryGetWindowsPathFromWslUri(fileUri, out windowsPath);
23+
}
1924

20-
if (!Uri.TryCreate(fileUri, UriKind.Absolute, out var uri))
21-
return false;
25+
// Case 2: Handle file URIs for WSL (e.g., file://wsl.localhost/Ubuntu/home/user/project)
26+
const string fileUriScheme = "file://";
27+
if (fileUri.StartsWith(fileUriScheme, StringComparison.OrdinalIgnoreCase))
28+
{
29+
var potentialWslPath = fileUri.Substring(fileUriScheme.Length);
30+
if (potentialWslPath.StartsWith("wsl.localhost/", StringComparison.OrdinalIgnoreCase) || potentialWslPath.StartsWith("wsl$/", StringComparison.OrdinalIgnoreCase))
31+
{
32+
windowsPath = "\\\\" + potentialWslPath.Replace('/', '\\');
33+
return true;
34+
}
35+
}
2236

23-
if (!uri.IsFile)
24-
return false;
37+
// Case 3: Handle direct UNC paths for WSL (e.g., \\wsl$\Ubuntu\home\user\project)
38+
if (fileUri.StartsWith(@"\\wsl$\", StringComparison.OrdinalIgnoreCase) || fileUri.StartsWith(@"\\wsl.localhost\", StringComparison.OrdinalIgnoreCase))
39+
{
40+
windowsPath = fileUri;
41+
return true;
42+
}
2543

26-
string localPath = uri.LocalPath.TrimStart('/');
27-
windowsPath = CapitalizeDriveLetter(localPath);
44+
// Case 4: Handle standard file URIs (e.g., file:///c:/Users/user/project)
45+
// Fix for lowercase drive letters in URIs
46+
fileUri = fileUri.Replace("%3A", ":").Replace("%3a", ":");
2847

29-
return true;
30-
}
48+
if (Uri.TryCreate(fileUri, UriKind.Absolute, out var uri) && uri.IsFile)
49+
{
50+
string localPath = uri.LocalPath.TrimStart('/');
51+
windowsPath = CapitalizeDriveLetter(localPath);
52+
return true;
53+
}
3154

32-
private static string CapitalizeDriveLetter(string path)
33-
{
34-
if (path.Length >= 2 && char.IsLetter(path[0]) && path[1] == ':')
35-
{
36-
return char.ToUpperInvariant(path[0]) + path.Substring(1);
55+
// If none of the above, assume it might be a direct file path already
56+
// For example, vscode-remote://codespaces+...
57+
windowsPath = fileUri;
58+
return true;
59+
}
60+
61+
private static string CapitalizeDriveLetter(string path)
62+
{
63+
if (path.Length >= 2 && char.IsLetter(path[0]) && path[1] == ':')
64+
{
65+
return char.ToUpperInvariant(path[0]) + path.Substring(1);
66+
}
67+
return path;
68+
}
3769
}
38-
return path;
39-
}
4070
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Modifications Copyright (c) 2025 tanchekwei
2+
// Licensed under the MIT License. See the LICENSE file in the project root for details.
3+
using System;
4+
using System.Net;
5+
6+
namespace WorkspaceLauncherForVSCode.Helpers
7+
{
8+
public static class WslPathHelper
9+
{
10+
public static bool TryGetWindowsPathFromWslUri(string wslUri, out string? windowsPath)
11+
{
12+
windowsPath = null;
13+
14+
if (TryGetWslPath(wslUri, out var distro, out var wslPath))
15+
{
16+
windowsPath = $"\\\\wsl$\\{distro}{wslPath.Replace('/', '\\')}";
17+
return true;
18+
}
19+
20+
return false;
21+
}
22+
23+
private static bool TryGetWslPath(string uri, out string distro, out string wslPath)
24+
{
25+
distro = string.Empty;
26+
wslPath = string.Empty;
27+
28+
if (uri.StartsWith("vscode-remote://", StringComparison.OrdinalIgnoreCase))
29+
{
30+
var pathPart = uri.Substring("vscode-remote://".Length);
31+
var authorityEndIndex = pathPart.IndexOf('/');
32+
if (authorityEndIndex <= 0) return false;
33+
34+
var encodedAuthority = pathPart.Substring(0, authorityEndIndex);
35+
wslPath = pathPart.Substring(authorityEndIndex);
36+
var authority = WebUtility.UrlDecode(encodedAuthority);
37+
38+
if (!authority.StartsWith("wsl+", StringComparison.Ordinal)) return false;
39+
40+
distro = authority.Substring("wsl+".Length);
41+
return true;
42+
}
43+
else if (uri.StartsWith("file://wsl.localhost/", StringComparison.OrdinalIgnoreCase))
44+
{
45+
var pathPart = uri.Substring("file://wsl.localhost/".Length);
46+
var firstSlashIndex = pathPart.IndexOf('/');
47+
if (firstSlashIndex == -1) return false;
48+
49+
distro = pathPart.Substring(0, firstSlashIndex);
50+
wslPath = pathPart.Substring(firstSlashIndex);
51+
return true;
52+
}
53+
54+
return false;
55+
}
56+
}
57+
}

WorkspaceLauncherForVSCode/Pages/HelpPage.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace WorkspaceLauncherForVSCode.Pages
1111
{
1212
public sealed partial class HelpPage : ListPage
1313
{
14-
private static readonly ListItem _openSettingsItem = new ListItem(new OpenInExplorerCommand(Utilities.BaseSettingsPath(Constant.AppName), null, null, "Open extension settings / logs folder"));
14+
private static readonly ListItem _openSettingsItem = new ListItem(new OpenInExplorerCommand(Utilities.BaseSettingsPath(Constant.AppName), null, "Open extension settings / logs folder"));
1515
private static readonly ListItem _viewSourceItem = new ListItem(new OpenUrlCommand("https://github.com/tanchekwei/WorkspaceLauncherForVSCode", "View source code", Classes.Icon.GitHub));
1616
private static readonly ListItem _reportBugItem = new ListItem(new OpenUrlCommand("https://github.com/tanchekwei/WorkspaceLauncherForVSCode/issues/new", "Report issue", Classes.Icon.GitHub));
1717
private int _total;

0 commit comments

Comments
 (0)