Main.cs
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. 4 5 using System; 6 using System.Collections.Generic; 7 using System.ComponentModel; 8 using System.Diagnostics; 9 using System.Linq; 10 using System.Windows; 11 using System.Windows.Input; 12 13 using Community.PowerToys.Run.Plugin.VSCodeWorkspaces.Properties; 14 using Community.PowerToys.Run.Plugin.VSCodeWorkspaces.RemoteMachinesHelper; 15 using Community.PowerToys.Run.Plugin.VSCodeWorkspaces.VSCodeHelper; 16 using Community.PowerToys.Run.Plugin.VSCodeWorkspaces.WorkspacesHelper; 17 18 using Wox.Infrastructure; 19 using Wox.Plugin; 20 using Wox.Plugin.Logger; 21 22 namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces 23 { 24 public class Main : IPlugin, IPluginI18n, IContextMenu 25 { 26 private PluginInitContext _context; 27 28 public string Name => GetTranslatedPluginTitle(); 29 30 public string Description => GetTranslatedPluginDescription(); 31 32 public static string PluginID => "525995402BEF4A8CA860D92F6D108092"; 33 34 public Main() 35 { 36 VSCodeInstances.LoadVSCodeInstances(); 37 } 38 39 private readonly VSCodeWorkspacesApi _workspacesApi = new VSCodeWorkspacesApi(); 40 41 private readonly VSCodeRemoteMachinesApi _machinesApi = new VSCodeRemoteMachinesApi(); 42 43 public List<Result> Query(Query query) 44 { 45 var results = new List<Result>(); 46 47 if (query != null) 48 { 49 // Search opened workspaces 50 _workspacesApi.Workspaces.ForEach(a => 51 { 52 var title = a.WorkspaceType == WorkspaceType.ProjectFolder ? a.FolderName : a.FolderName.Replace(".code-workspace", $" ({Resources.Workspace})"); 53 54 var typeWorkspace = a.WorkspaceEnvironmentToString(); 55 if (a.WorkspaceEnvironment != WorkspaceEnvironment.Local) 56 { 57 title = $"{title}{(a.ExtraInfo != null ? $" - {a.ExtraInfo}" : string.Empty)} ({typeWorkspace})"; 58 } 59 60 var tooltip = new ToolTipData(title, $"{(a.WorkspaceType == WorkspaceType.WorkspaceFile ? Resources.Workspace : Resources.ProjectFolder)}{(a.WorkspaceEnvironment != WorkspaceEnvironment.Local ? $" {Resources.In} {typeWorkspace}" : string.Empty)}: {SystemPath.RealPath(a.RelativePath)}"); 61 62 results.Add(new Result 63 { 64 Title = title, 65 SubTitle = $"{(a.WorkspaceType == WorkspaceType.WorkspaceFile ? Resources.Workspace : Resources.ProjectFolder)}{(a.WorkspaceEnvironment != WorkspaceEnvironment.Local ? $" {Resources.In} {typeWorkspace}" : string.Empty)}: {SystemPath.RealPath(a.RelativePath)}", 66 Icon = a.VSCodeInstance.WorkspaceIcon, 67 ToolTipData = tooltip, 68 Action = c => 69 { 70 bool hide; 71 try 72 { 73 var process = new ProcessStartInfo 74 { 75 FileName = a.VSCodeInstance.ExecutablePath, 76 UseShellExecute = true, 77 Arguments = a.WorkspaceType == WorkspaceType.ProjectFolder ? $"--folder-uri {a.Path}" : $"--file-uri {a.Path}", 78 WindowStyle = ProcessWindowStyle.Hidden, 79 }; 80 Process.Start(process); 81 82 hide = true; 83 } 84 catch (Win32Exception ex) 85 { 86 HandleError("Can't Open this file", ex, showMsg: true); 87 hide = false; 88 } 89 90 return hide; 91 }, 92 ContextData = a, 93 }); 94 }); 95 96 // Search opened remote machines 97 _machinesApi.Machines.ForEach(a => 98 { 99 var title = $"{a.Host}"; 100 101 if (a.User != null && a.User != string.Empty && a.HostName != null && a.HostName != string.Empty) 102 { 103 title += $" [{a.User}@{a.HostName}]"; 104 } 105 106 var tooltip = new ToolTipData(title, Resources.SSHRemoteMachine); 107 108 results.Add(new Result 109 { 110 Title = title, 111 SubTitle = Resources.SSHRemoteMachine, 112 Icon = a.VSCodeInstance.RemoteIcon, 113 ToolTipData = tooltip, 114 Action = c => 115 { 116 bool hide; 117 try 118 { 119 var process = new ProcessStartInfo() 120 { 121 FileName = a.VSCodeInstance.ExecutablePath, 122 UseShellExecute = true, 123 Arguments = $"--new-window --enable-proposed-api ms-vscode-remote.remote-ssh --remote ssh-remote+{((char)34) + a.Host + ((char)34)}", 124 WindowStyle = ProcessWindowStyle.Hidden, 125 }; 126 Process.Start(process); 127 128 hide = true; 129 } 130 catch (Win32Exception ex) 131 { 132 HandleError("Can't Open this file", ex, showMsg: true); 133 hide = false; 134 } 135 136 return hide; 137 }, 138 ContextData = a, 139 }); 140 }); 141 } 142 143 results = results.Where(a => a.Title.Contains(query.Search, StringComparison.InvariantCultureIgnoreCase)).ToList(); 144 145 results.ForEach(x => 146 { 147 if (x.Score == 0) 148 { 149 x.Score = 100; 150 } 151 152 // intersect the title with the query 153 var intersection = Convert.ToInt32(x.Title.ToLowerInvariant().Intersect(query.Search.ToLowerInvariant()).Count() * query.Search.Length); 154 var differenceWithQuery = Convert.ToInt32((x.Title.Length - intersection) * query.Search.Length * 0.7); 155 x.Score = x.Score - differenceWithQuery + intersection; 156 157 // if is a remote machine give it 12 extra points 158 if (x.ContextData is VSCodeRemoteMachine) 159 { 160 x.Score = Convert.ToInt32(x.Score + (intersection * 2)); 161 } 162 }); 163 164 results = results.OrderByDescending(x => x.Score).ToList(); 165 if (query.Search == string.Empty || query.Search.Replace(" ", string.Empty) == string.Empty) 166 { 167 results = results.OrderBy(x => x.Title).ToList(); 168 } 169 170 return results; 171 } 172 173 public void Init(PluginInitContext context) 174 { 175 _context = context; 176 } 177 178 public List<ContextMenuResult> LoadContextMenus(Result selectedResult) 179 { 180 if (selectedResult?.ContextData is not VSCodeWorkspace workspace) 181 { 182 return new List<ContextMenuResult>(); 183 } 184 185 string realPath = SystemPath.RealPath(workspace.RelativePath); 186 187 return new List<ContextMenuResult> 188 { 189 new ContextMenuResult 190 { 191 PluginName = Name, 192 Title = $"{Resources.CopyPath} (Ctrl+C)", 193 Glyph = "\xE8C8", // Copy 194 FontFamily = "Segoe Fluent Icons,Segoe MDL2 Assets", 195 AcceleratorKey = Key.C, 196 AcceleratorModifiers = ModifierKeys.Control, 197 Action = context => CopyToClipboard(realPath), 198 }, 199 new ContextMenuResult 200 { 201 PluginName = Name, 202 Title = $"{Resources.OpenInExplorer} (Ctrl+Shift+F)", 203 Glyph = "\xEC50", // File Explorer 204 FontFamily = "Segoe Fluent Icons,Segoe MDL2 Assets", 205 AcceleratorKey = Key.F, 206 AcceleratorModifiers = ModifierKeys.Control | ModifierKeys.Shift, 207 Action = context => OpenInExplorer(realPath), 208 }, 209 new ContextMenuResult 210 { 211 PluginName = Name, 212 Title = $"{Resources.OpenInConsole} (Ctrl+Shift+C)", 213 Glyph = "\xE756", // Command Prompt 214 FontFamily = "Segoe Fluent Icons,Segoe MDL2 Assets", 215 AcceleratorKey = Key.C, 216 AcceleratorModifiers = ModifierKeys.Control | ModifierKeys.Shift, 217 Action = context => OpenInConsole(realPath), 218 }, 219 }; 220 } 221 222 private bool CopyToClipboard(string path) 223 { 224 try 225 { 226 Clipboard.SetText(path); 227 return true; 228 } 229 catch (Exception ex) 230 { 231 HandleError("Can't copy to clipboard", ex, showMsg: true); 232 return false; 233 } 234 } 235 236 private bool OpenInConsole(string path) 237 { 238 try 239 { 240 Helper.OpenInConsole(path); 241 return true; 242 } 243 catch (Exception ex) 244 { 245 HandleError($"Unable to open the specified path in the console: {path}", ex, showMsg: true); 246 return false; 247 } 248 } 249 250 private bool OpenInExplorer(string path) 251 { 252 if (!Helper.OpenInShell("explorer.exe", $"\"{path}\"")) 253 { 254 HandleError($"Failed to open folder in Explorer at path: {path}", showMsg: true); 255 return false; 256 } 257 258 return true; 259 } 260 261 private void HandleError(string msg, Exception exception = null, bool showMsg = false) 262 { 263 if (exception != null) 264 { 265 Log.Exception(msg, exception, exception.GetType()); 266 } 267 else 268 { 269 Log.Error(msg, typeof(VSCodeWorkspaces.Main)); 270 } 271 272 if (showMsg) 273 { 274 _context.API.ShowMsg( 275 $"Plugin: {_context.CurrentPluginMetadata.Name}", 276 msg); 277 } 278 } 279 280 public string GetTranslatedPluginTitle() 281 { 282 return Resources.PluginTitle; 283 } 284 285 public string GetTranslatedPluginDescription() 286 { 287 return Resources.PluginDescription; 288 } 289 } 290 }