Skip to content

Commit 4c11354

Browse files
committed
Add VisualStudioCodeRemoteUri.cs
1 parent 9d89848 commit 4c11354

File tree

8 files changed

+290
-121
lines changed

8 files changed

+290
-121
lines changed

WorkspaceLauncherForVSCode/Classes/Constant.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ namespace WorkspaceLauncherForVSCode.Classes;
66
public static class Constant
77
{
88
#if DEBUG
9-
public static readonly string AppName = "WorkspaceLauncherForVSCodeDev";
9+
public const string AppName = "WorkspaceLauncherForVSCodeDev";
1010
#else
11-
public static readonly string AppName = "WorkspaceLauncherForVSCode";
11+
public const string AppName = "WorkspaceLauncherForVSCode";
1212
#endif
13-
public static readonly string VscodeRemoteScheme = "vscode-remote://";
13+
public const string VscodeRemoteScheme = "vscode-remote://";
1414
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
using System;
2+
using System.Text;
3+
using System.Text.Json;
4+
using System.Net;
5+
using WorkspaceLauncherForVSCode.Enums;
6+
using WorkspaceLauncherForVSCode.Helpers;
7+
using ABI.System;
8+
9+
namespace WorkspaceLauncherForVSCode.Classes;
10+
11+
public class VisualStudioCodeRemoteUri
12+
{
13+
public const string Scheme = Constant.VscodeRemoteScheme;
14+
public string Uri { get; }
15+
public string TypeStr { get; }
16+
public VisualStudioCodeRemoteType? Type { get; }
17+
public string InfoRaw { get; }
18+
public string InfoDecoded { get; }
19+
public JsonElement? InfoJson { get; }
20+
public string Path { get; }
21+
22+
public VisualStudioCodeRemoteUri(string uri)
23+
{
24+
if (string.IsNullOrWhiteSpace(uri))
25+
throw new ArgumentException("URI cannot be null or empty.", nameof(uri));
26+
27+
if (!uri.StartsWith(Constant.VscodeRemoteScheme, StringComparison.OrdinalIgnoreCase))
28+
throw new ArgumentException("URI must start with 'vscode-remote://'");
29+
30+
Uri = uri;
31+
32+
// Remove scheme
33+
var withoutScheme = uri.Substring(Constant.VscodeRemoteScheme.Length);
34+
35+
// Detect percent-encoded '+' (e.g., Codespaces)
36+
var firstSlash = withoutScheme.IndexOf('/');
37+
var beforePath = firstSlash == -1 ? withoutScheme : withoutScheme.Substring(0, firstSlash);
38+
var path = firstSlash == -1 ? "/" : withoutScheme.Substring(firstSlash);
39+
40+
// Handle encoded `+` (for codespaces)
41+
string remoteType = null, remoteInfoRaw = null;
42+
43+
if (beforePath.Contains('+'))
44+
{
45+
var parts = beforePath.Split('+', 2);
46+
remoteType = parts[0];
47+
remoteInfoRaw = parts[1];
48+
}
49+
else if (beforePath.Contains("%2B", StringComparison.OrdinalIgnoreCase))
50+
{
51+
var decoded = WebUtility.UrlDecode(beforePath); // decode first
52+
var parts = decoded.Split('+', 2);
53+
remoteType = parts[0];
54+
remoteInfoRaw = parts[1];
55+
}
56+
else
57+
{
58+
throw new ArgumentException("Malformed vscode-remote URI: '+' not found.");
59+
}
60+
61+
TypeStr = remoteType;
62+
if (VisualStudioCodeRemoteTypeHelper.TryParse(remoteType, out var type))
63+
{
64+
Type = type;
65+
}
66+
InfoRaw = remoteInfoRaw;
67+
Path = path;
68+
69+
// Try to detect and decode hex-encoded JSON (must be even length and valid hex)
70+
InfoDecoded = TryDecodeHexJson(remoteInfoRaw, out var jsonElement)
71+
? jsonElement.ToString()
72+
: InfoRaw;
73+
74+
InfoJson = jsonElement.ValueKind != JsonValueKind.Undefined ? jsonElement : null;
75+
}
76+
77+
private static bool TryDecodeHexJson(string hex, out JsonElement element)
78+
{
79+
element = default;
80+
81+
// Rough check: valid hex, even length
82+
if (hex.Length % 2 != 0 || !IsHexString(hex))
83+
return false;
84+
85+
try
86+
{
87+
var bytes = Convert.FromHexString(hex);
88+
var json = Encoding.UTF8.GetString(bytes);
89+
90+
var doc = JsonDocument.Parse(json);
91+
element = doc.RootElement.Clone();
92+
return true;
93+
}
94+
catch
95+
{
96+
return false;
97+
}
98+
}
99+
100+
private static bool IsHexString(string s)
101+
{
102+
foreach (char c in s)
103+
{
104+
if (!System.Uri.IsHexDigit(c))
105+
return false;
106+
}
107+
return true;
108+
}
109+
110+
public override string ToString()
111+
{
112+
return $"Type: {Type}\nInfo: {InfoDecoded}\nPath: {Path}";
113+
}
114+
115+
public static bool IsVisualStudioCodeRemoteUri(string uri)
116+
{
117+
return uri.StartsWith(Constant.VscodeRemoteScheme, StringComparison.OrdinalIgnoreCase);
118+
}
119+
120+
public string GetSubtitle()
121+
{
122+
string? subtitle = null;
123+
switch (Type)
124+
{
125+
case VisualStudioCodeRemoteType.Codespaces:
126+
subtitle = GetCodespacesUrl(InfoRaw);
127+
break;
128+
case VisualStudioCodeRemoteType.DevContainer:
129+
subtitle = GetDevContainerSubtitle();
130+
break;
131+
case VisualStudioCodeRemoteType.WSL:
132+
WslPathHelper.TryGetWindowsPathFromWslUri(Uri, out var windowsPath);
133+
subtitle = windowsPath;
134+
break;
135+
default:
136+
break;
137+
}
138+
return string.IsNullOrWhiteSpace(subtitle) ? GetDecodedUri() : subtitle;
139+
}
140+
141+
public static string GetCodespacesUrl(string subdomain)
142+
{
143+
return $"{System.Uri.UriSchemeHttps}://{subdomain}.github.dev/";
144+
}
145+
146+
public string? GetDevContainerSubtitle()
147+
{
148+
if (InfoJson.HasValue && InfoJson.Value.TryGetProperty("hostPath", out JsonElement hostPathElement))
149+
{
150+
return FileUriParser.CapitalizeDriveLetter(hostPathElement.GetString() ?? string.Empty);
151+
}
152+
return null;
153+
}
154+
155+
public string GetDecodedUri() => $"{Scheme}{TypeStr}+{InfoDecoded}{Path}";
156+
}

WorkspaceLauncherForVSCode/Classes/VisualStudioCodeWorkspace.cs

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System;
44
using System.Collections.Generic;
55
using Microsoft.CommandPalette.Extensions.Toolkit;
6+
using WorkspaceLauncherForVSCode.Classes;
67
using WorkspaceLauncherForVSCode.Components;
78
using WorkspaceLauncherForVSCode.Enums;
89
using WorkspaceLauncherForVSCode.Helpers;
@@ -25,8 +26,7 @@ public class VisualStudioCodeWorkspace
2526
public VisualStudioCodeWorkspaceSource Source { get; set; }
2627
public List<string> SourcePath { get; set; } = new();
2728
public string WorkspaceName { get; set; } = "";
28-
public VisualStudioCodeRemoteType? VsCodeRemoteType { get; set; }
29-
public string? VsCodeRemoteTypeStr { get; set; }
29+
public VisualStudioCodeRemoteUri? VisualStudioCodeRemoteUri { get; set; }
3030
public string WorkspaceTypeString { get; set; } = "";
3131
public int Frequency { get; set; }
3232
public DateTime LastAccessed { get; set; }
@@ -41,12 +41,14 @@ public VisualStudioCodeWorkspace() { }
4141
internal VisualStudioCodeWorkspace(VisualStudioCodeInstance instance, string path, WorkspaceType visualStudioCodeWorkspaceType, VisualStudioCodeWorkspaceSource source, string sourcePath)
4242
{
4343
Path = path;
44-
45-
if (FileUriParser.TryParse(path, out var windowsPath, out var remoteType, out var remoteTypeStr))
44+
if (VisualStudioCodeRemoteUri.IsVisualStudioCodeRemoteUri(path))
45+
{
46+
VisualStudioCodeRemoteUri = new(path);
47+
WindowsPath = VisualStudioCodeRemoteUri?.GetSubtitle();
48+
}
49+
else if (FileUriParser.TryParse(path, out var windowsPath))
4650
{
4751
WindowsPath = windowsPath;
48-
VsCodeRemoteType = remoteType;
49-
VsCodeRemoteTypeStr = remoteTypeStr;
5052
}
5153
VSCodeInstance = instance;
5254
WorkspaceType = visualStudioCodeWorkspaceType;
@@ -121,13 +123,13 @@ public void SetVSCodeMetadata()
121123
{
122124
if (Path == null) return;
123125
var typeTags = new List<Tag>() { new Tag(WorkspaceTypeString) };
124-
if (VsCodeRemoteType != null)
126+
if (VisualStudioCodeRemoteUri != null)
125127
{
126-
typeTags.Add(new Tag(VsCodeRemoteType.ToString()));
128+
typeTags.Add(new Tag(VisualStudioCodeRemoteUri.Type.ToDisplayName()));
127129
}
128-
else if (VsCodeRemoteTypeStr != null)
130+
else if (VisualStudioCodeRemoteUri?.TypeStr != null)
129131
{
130-
typeTags.Add(new Tag(VsCodeRemoteTypeStr));
132+
typeTags.Add(new Tag(VisualStudioCodeRemoteUri.TypeStr));
131133
}
132134
}
133135

@@ -137,15 +139,5 @@ public void SetVSCodeMetadata()
137139
/// <returns>An array of details elements containing information about the workspace.</returns>
138140
public void SetVSMetadata()
139141
{
140-
if (Path == null) return;
141-
var typeTags = new List<Tag>() { new Tag(WorkspaceTypeString) };
142-
if (VsCodeRemoteType != null)
143-
{
144-
typeTags.Add(new Tag(VsCodeRemoteType.ToString()));
145-
}
146-
else if (VsCodeRemoteTypeStr != null)
147-
{
148-
typeTags.Add(new Tag(VsCodeRemoteTypeStr));
149-
}
150142
}
151143
}

WorkspaceLauncherForVSCode/Commands/OpenVisualStudioCodeCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public override CommandResult Invoke()
6161
}
6262

6363
var pathToValidate = Workspace.WindowsPath ?? Workspace.Path;
64-
if (Workspace.VsCodeRemoteType == null)
64+
if (Workspace.VisualStudioCodeRemoteUri == null)
6565
{
6666
var pathInvalidResult = CommandHelpers.IsPathValid(pathToValidate);
6767
if (pathInvalidResult != null)

WorkspaceLauncherForVSCode/Enums/VisualStudioCodeRemoteType.cs

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,36 +13,4 @@ public enum VisualStudioCodeRemoteType
1313
AttachedContainer,
1414
SSHRemote,
1515
}
16-
17-
public static class VisualStudioCodeRemoteHelper
18-
{
19-
private static readonly Dictionary<string, VisualStudioCodeRemoteType> _map = new(StringComparer.OrdinalIgnoreCase)
20-
{
21-
{ "wsl", VisualStudioCodeRemoteType.WSL },
22-
{ "dev-container", VisualStudioCodeRemoteType.DevContainer },
23-
{ "codespaces", VisualStudioCodeRemoteType.Codespaces },
24-
{ "attached-container", VisualStudioCodeRemoteType.AttachedContainer },
25-
{ "ssh-remote", VisualStudioCodeRemoteType.SSHRemote }
26-
};
27-
28-
public static bool TryParse(string input, out VisualStudioCodeRemoteType env)
29-
=> _map.TryGetValue(input, out env);
30-
31-
public static string ToDisplayName(this VisualStudioCodeRemoteType? input)
32-
{
33-
if (input == null)
34-
{
35-
return string.Empty;
36-
}
37-
return input switch
38-
{
39-
VisualStudioCodeRemoteType.WSL => "WSL",
40-
VisualStudioCodeRemoteType.DevContainer => "Dev Container",
41-
VisualStudioCodeRemoteType.Codespaces => "Codespaces",
42-
VisualStudioCodeRemoteType.AttachedContainer => "Attached Container",
43-
VisualStudioCodeRemoteType.SSHRemote => "SSH Remote",
44-
_ => input.ToString()
45-
};
46-
}
47-
}
4816
}

0 commit comments

Comments
 (0)