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  }