diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces/VSCodeHelper/VSCodeInstances.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces/VSCodeHelper/VSCodeInstances.cs index 98b2d69169..86837628a3 100644 --- a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces/VSCodeHelper/VSCodeInstances.cs +++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces/VSCodeHelper/VSCodeInstances.cs @@ -74,7 +74,7 @@ namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.VSCodeHelper if (Directory.Exists(path)) { var files = Directory.GetFiles(path) - .Where(x => (x.Contains("code", StringComparison.OrdinalIgnoreCase) || x.Contains("VSCodium", StringComparison.OrdinalIgnoreCase)) + .Where(x => (x.Contains("code", StringComparison.OrdinalIgnoreCase) || x.Contains("codium", StringComparison.OrdinalIgnoreCase)) && !x.EndsWith(".cmd", StringComparison.OrdinalIgnoreCase)).ToArray(); var iconPath = Path.GetDirectoryName(path); @@ -104,10 +104,15 @@ namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.VSCodeHelper version = "Code - Exploration"; instance.VSCodeVersion = VSCodeVersion.Exploration; } - else if (file.EndsWith("VSCodium", StringComparison.OrdinalIgnoreCase)) + else if (file.EndsWith("codium", StringComparison.OrdinalIgnoreCase)) { version = "VSCodium"; - instance.VSCodeVersion = VSCodeVersion.Stable; // ? + instance.VSCodeVersion = VSCodeVersion.Stable; + } + else if (file.EndsWith("codium-insiders", StringComparison.OrdinalIgnoreCase)) + { + version = "VSCodium - Insiders"; + instance.VSCodeVersion = VSCodeVersion.Insiders; } if (version != string.Empty) diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces/WorkspacesHelper/ParseVSCodeAuthority.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces/WorkspacesHelper/ParseVSCodeAuthority.cs new file mode 100644 index 0000000000..c59a113f1a --- /dev/null +++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces/WorkspacesHelper/ParseVSCodeAuthority.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.WorkspacesHelper +{ + public class ParseVSCodeAuthority + { + private static readonly Dictionary EnvironmentTypes = new() + { + { string.Empty, WorkspaceEnvironment.Local }, + { "ssh-remote", WorkspaceEnvironment.RemoteSSH }, + { "wsl", WorkspaceEnvironment.RemoteWSL }, + { "vsonline", WorkspaceEnvironment.Codespaces }, + { "dev-container", WorkspaceEnvironment.DevContainer }, + { "tunnel", WorkspaceEnvironment.RemoteTunnel }, + }; + + private static string GetRemoteName(string authority) + { + if (authority is null) + { + return null; + } + + var pos = authority.IndexOf('+'); + if (pos < 0) + { + return authority; + } + + return authority[..pos]; + } + + public static (WorkspaceEnvironment? WorkspaceEnvironment, string MachineName) GetWorkspaceEnvironment(string authority) + { + var remoteName = GetRemoteName(authority); + var machineName = remoteName.Length < authority.Length ? authority[(remoteName.Length + 1)..] : null; + return EnvironmentTypes.TryGetValue(remoteName, out WorkspaceEnvironment workspace) ? + (workspace, machineName) : + (null, null); + } + } +} diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces/WorkspacesHelper/ParseVSCodeUri.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces/WorkspacesHelper/ParseVSCodeUri.cs deleted file mode 100644 index db224d1633..0000000000 --- a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces/WorkspacesHelper/ParseVSCodeUri.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Text.RegularExpressions; - -namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.WorkspacesHelper -{ - public class ParseVSCodeUri - { - private static readonly Regex LocalWorkspace = new Regex("^file:///(.+)$", RegexOptions.Compiled); - - private static readonly Regex RemoteSSHWorkspace = new Regex(@"^vscode-remote://ssh-remote\+(.+?(?=\/))(.+)$", RegexOptions.Compiled); - - private static readonly Regex RemoteWSLWorkspace = new Regex(@"^vscode-remote://wsl\+(.+?(?=\/))(.+)$", RegexOptions.Compiled); - - private static readonly Regex CodespacesWorkspace = new Regex(@"^vscode-remote://vsonline\+(.+?(?=\/))(.+)$", RegexOptions.Compiled); - - private static readonly Regex DevContainerWorkspace = new Regex(@"^vscode-remote://dev-container\+(.+?(?=\/))(.+)$", RegexOptions.Compiled); - - public static (WorkspaceEnvironment? WorkspaceEnvironment, string MachineName, string Path) GetWorkspaceEnvironment(string uri) - { - if (LocalWorkspace.IsMatch(uri)) - { - var match = LocalWorkspace.Match(uri); - - if (match.Groups.Count > 1) - { - return (WorkspaceEnvironment.Local, null, match.Groups[1].Value); - } - } - else if (RemoteSSHWorkspace.IsMatch(uri)) - { - var match = RemoteSSHWorkspace.Match(uri); - - if (match.Groups.Count > 1) - { - return (WorkspaceEnvironment.RemoteSSH, match.Groups[1].Value, match.Groups[2].Value); - } - } - else if (RemoteWSLWorkspace.IsMatch(uri)) - { - var match = RemoteWSLWorkspace.Match(uri); - - if (match.Groups.Count > 1) - { - return (WorkspaceEnvironment.RemoteWSL, match.Groups[1].Value, match.Groups[2].Value); - } - } - else if (CodespacesWorkspace.IsMatch(uri)) - { - var match = CodespacesWorkspace.Match(uri); - - if (match.Groups.Count > 1) - { - return (WorkspaceEnvironment.Codespaces, null, match.Groups[2].Value); - } - } - else if (DevContainerWorkspace.IsMatch(uri)) - { - var match = DevContainerWorkspace.Match(uri); - - if (match.Groups.Count > 1) - { - return (WorkspaceEnvironment.DevContainer, null, match.Groups[2].Value); - } - } - - return (null, null, null); - } - } -} diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces/WorkspacesHelper/Rfc3986Uri.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces/WorkspacesHelper/Rfc3986Uri.cs new file mode 100644 index 0000000000..804253d132 --- /dev/null +++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces/WorkspacesHelper/Rfc3986Uri.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; + +namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.WorkspacesHelper +{ + // Use regex to parse URI since System.Uri is not compliant with RFC 3986, see https://github.com/dotnet/runtime/issues/64707 + public partial class Rfc3986Uri + { + // The following regex is referenced from https://www.rfc-editor.org/rfc/rfc3986.html#appendix-B + [GeneratedRegex(@"^((?[^:/?#]+):)?(//(?[^/?#]*))?(?[^?#]*)(\?(?[^#]*))?(#(?.*))?$")] + private static partial Regex Rfc3986(); + + public string Scheme { get; private set; } + + public string Authority { get; private set; } + + public string Path { get; private set; } + + public string Query { get; private set; } + + public string Fragment { get; private set; } + + public static Rfc3986Uri Parse([StringSyntax("Uri")] string uriString) + { + return uriString is not null && Rfc3986().Match(uriString) is { Success: true } match + ? new Rfc3986Uri() + { + Scheme = match.Groups["scheme"].Value, + Authority = match.Groups["authority"].Value, + Path = match.Groups["path"].Value, + Query = match.Groups["query"].Value, + Fragment = match.Groups["fragment"].Value, + } + : null; + } + } +} diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces/WorkspacesHelper/VSCodeWorkspace.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces/WorkspacesHelper/VSCodeWorkspace.cs index fb5a78d118..dc0f479532 100644 --- a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces/WorkspacesHelper/VSCodeWorkspace.cs +++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces/WorkspacesHelper/VSCodeWorkspace.cs @@ -29,10 +29,10 @@ namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.WorkspacesHelper { case WorkspaceEnvironment.Local: return Resources.TypeWorkspaceLocal; case WorkspaceEnvironment.Codespaces: return "Codespaces"; - case WorkspaceEnvironment.RemoteContainers: return Resources.TypeWorkspaceContainer; case WorkspaceEnvironment.RemoteSSH: return "SSH"; case WorkspaceEnvironment.RemoteWSL: return "WSL"; case WorkspaceEnvironment.DevContainer: return Resources.TypeWorkspaceDevContainer; + case WorkspaceEnvironment.RemoteTunnel: return "Tunnel"; } return string.Empty; @@ -45,8 +45,8 @@ namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.WorkspacesHelper Codespaces = 2, RemoteWSL = 3, RemoteSSH = 4, - RemoteContainers = 5, - DevContainer = 6, + DevContainer = 5, + RemoteTunnel = 6, } public enum WorkspaceType diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces/WorkspacesHelper/VSCodeWorkspacesApi.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces/WorkspacesHelper/VSCodeWorkspacesApi.cs index 13e744e603..bd318f0896 100644 --- a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces/WorkspacesHelper/VSCodeWorkspacesApi.cs +++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces/WorkspacesHelper/VSCodeWorkspacesApi.cs @@ -19,37 +19,52 @@ namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.WorkspacesHelper { } - private VSCodeWorkspace ParseVSCodeUri(string uri, VSCodeInstance vscodeInstance, bool isWorkspace = false) + private VSCodeWorkspace ParseVSCodeUriAndAuthority(string uri, string authority, VSCodeInstance vscodeInstance, bool isWorkspace = false) { - if (uri != null && uri is string) + if (uri is null) { - string unescapeUri = Uri.UnescapeDataString(uri); - var typeWorkspace = WorkspacesHelper.ParseVSCodeUri.GetWorkspaceEnvironment(unescapeUri); - if (typeWorkspace.WorkspaceEnvironment.HasValue) - { - var folderName = Path.GetFileName(unescapeUri); - - // Check we haven't returned '' if we have a path like C:\ - if (string.IsNullOrEmpty(folderName)) - { - DirectoryInfo dirInfo = new DirectoryInfo(unescapeUri); - folderName = dirInfo.Name.TrimEnd(':'); - } - - return new VSCodeWorkspace() - { - Path = uri, - WorkspaceType = isWorkspace ? WorkspaceType.WorkspaceFile : WorkspaceType.ProjectFolder, - RelativePath = typeWorkspace.Path, - FolderName = folderName, - ExtraInfo = typeWorkspace.MachineName, - WorkspaceEnvironment = typeWorkspace.WorkspaceEnvironment.Value, - VSCodeInstance = vscodeInstance, - }; - } + return null; } - return null; + var rfc3986Uri = Rfc3986Uri.Parse(Uri.UnescapeDataString(uri)); + if (rfc3986Uri is null) + { + return null; + } + + var (workspaceEnv, machineName) = ParseVSCodeAuthority.GetWorkspaceEnvironment(authority ?? rfc3986Uri.Authority); + if (workspaceEnv is null) + { + return null; + } + + var path = rfc3986Uri.Path; + + // Remove preceding '/' from local (Windows) path + if (workspaceEnv == WorkspaceEnvironment.Local) + { + path = path[1..]; + } + + var folderName = Path.GetFileName(path); + + // Check we haven't returned '' if we have a path like C:\ + if (string.IsNullOrEmpty(folderName)) + { + DirectoryInfo dirInfo = new(path); + folderName = dirInfo.Name.TrimEnd(':'); + } + + return new VSCodeWorkspace() + { + Path = uri, + WorkspaceType = isWorkspace ? WorkspaceType.WorkspaceFile : WorkspaceType.ProjectFolder, + RelativePath = path, + FolderName = folderName, + ExtraInfo = machineName, + WorkspaceEnvironment = workspaceEnv ?? default, + VSCodeInstance = vscodeInstance, + }; } public List Workspaces @@ -100,7 +115,7 @@ namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.WorkspacesHelper { foreach (var workspaceUri in vscodeStorageFile.OpenedPathsList.Workspaces3) { - var workspace = ParseVSCodeUri(workspaceUri, vscodeInstance); + var workspace = ParseVSCodeUriAndAuthority(workspaceUri, null, vscodeInstance); if (workspace != null) { storageFileResults.Add(workspace); @@ -121,7 +136,7 @@ namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.WorkspacesHelper uri = entry.Workspace.ConfigPath; } - var workspace = ParseVSCodeUri(uri, vscodeInstance, isWorkspaceFile); + var workspace = ParseVSCodeUriAndAuthority(uri, entry.RemoteAuthority, vscodeInstance, isWorkspaceFile); if (workspace != null) { storageFileResults.Add(workspace); @@ -174,7 +189,7 @@ namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.WorkspacesHelper uri = entry.Workspace.ConfigPath; } - var workspace = ParseVSCodeUri(uri, vscodeInstance, isWorkspaceFile); + var workspace = ParseVSCodeUriAndAuthority(uri, entry.RemoteAuthority, vscodeInstance, isWorkspaceFile); if (workspace != null) { dbFileResults.Add(workspace);