-
Notifications
You must be signed in to change notification settings - Fork 235
Expand file tree
/
Copy pathVisualStudioInteraction.cs
More file actions
366 lines (311 loc) · 16.5 KB
/
VisualStudioInteraction.cs
File metadata and controls
366 lines (311 loc) · 16.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using EnvDTE;
using EnvDTE80;
using ICSharpCode.CodeConverter.Common;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Threading;
using Constants = EnvDTE.Constants;
using IAsyncServiceProvider = Microsoft.VisualStudio.Shell.IAsyncServiceProvider;
using Task = System.Threading.Tasks.Task;
using Window = EnvDTE.Window;
namespace ICSharpCode.CodeConverter.VsExtension;
/// <remarks>
/// All public methods switch to the main thread, do their work, then switch back to the thread pool
/// Private methods may also do so for convenience to suppress the analyzer warning
/// </remarks>
internal static class VisualStudioInteraction
{
private static DTE2 _dte;
/// <remarks>All calls and usages must be from the main thread</remarks>
internal static DTE2 Dte {
get {
ThreadHelper.ThrowIfNotOnUIThread();
return _dte ??= Package.GetGlobalService(typeof(DTE)) as DTE2;
}
}
private static CancellationToken _cancelAllToken;
private static readonly Version LowestSupportedVersion = new(16, 10, 0, 0);
private static readonly Version FullVsVersion = GetFullVsVersion();
private static readonly string Title = "Code converter " + new AssemblyName(typeof(ProjectConversion).Assembly.FullName).Version.ToString(3) + " - Visual Studio " + (FullVsVersion?.ToString() ?? "unknown version");
private static readonly int WeeksUpdatesStoppedFor = (int) (DateTime.Now - new DateTime(2022, 04, 11)).TotalDays / 7;
private static Version GetFullVsVersion()
{
string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "devenv.exe");
if (File.Exists(path)) {
var fvi = FileVersionInfo.GetVersionInfo(path);
return new Version(fvi.ProductMajorPart, fvi.ProductMinorPart, fvi.ProductBuildPart,
fvi.ProductPrivatePart);
}
return null;
}
internal static void Initialize(Cancellation packageCancellation)
{
_cancelAllToken = packageCancellation.CancelAll;
}
public static async Task<List<string>> GetSelectedItemsPathAsync(Func<string, bool> fileFilter)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(_cancelAllToken);
const string folderKind = "{6BB5F8EF-4483-11D3-8BCF-00C04F8EC28C}";
const string fileKind = "{6BB5F8EE-4483-11D3-8BCF-00C04F8EC28C}";
var allSelectedFiles = new List<string>();
var projectItems = GetSelectedSolutionExplorerItems<ProjectItem>().ToList();
while (projectItems.Count > 0) {
#pragma warning disable VSTHRD010 // Invoke single-threaded types on Main thread
var folders = projectItems.Where(t => string.Equals(t.Kind, folderKind, StringComparison.OrdinalIgnoreCase));
var files = projectItems.Where(t => string.Equals(t.Kind, fileKind, StringComparison.OrdinalIgnoreCase)).ToList();
var filesPath = files.Select(t => t.Properties.Item("FullPath").Value as string).Where(fileFilter);
allSelectedFiles.AddRange(filesPath);
projectItems = folders.Concat(files).SelectMany(t => t.ProjectItems?.OfType<ProjectItem>() ?? Enumerable.Empty<ProjectItem>()).ToList();
#pragma warning restore VSTHRD010 // Invoke single-threaded types on Main thread
}
await TaskScheduler.Default;
return allSelectedFiles;
}
public static async Task<Window> OpenFileAsync(FileInfo fileInfo)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(_cancelAllToken);
var window = Dte.ItemOperations.OpenFile(fileInfo.FullName, Constants.vsViewKindTextView);
await TaskScheduler.Default;
return window;
}
public static async Task SelectAllAsync(this Window window)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(_cancelAllToken);
((TextSelection)window?.Document?.Selection)?.SelectAll(); // https://github.com/icsharpcode/CodeConverter/issues/770
await TaskScheduler.Default;
}
public static async Task<IReadOnlyCollection<Project>> GetSelectedProjectsAsync(string projectExtension)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(_cancelAllToken);
#pragma warning disable VSTHRD010 // Invoke single-threaded types on Main thread
var projects = GetSelectedSolutionExplorerItems<Solution>().SelectMany(s => s.GetAllProjects())
.Concat(GetSelectedSolutionExplorerItems<Project>().SelectMany(p => p.GetProjects()))
.Concat(GetSelectedSolutionExplorerItems<ProjectItem>().Where(p => p.SubProject != null).SelectMany(p => p.SubProject.GetProjects()))
.Where(project => project.FullName.EndsWith(projectExtension, StringComparison.InvariantCultureIgnoreCase))
.Distinct().ToList();
#pragma warning restore VSTHRD010 // Invoke single-threaded types on Main thread
await TaskScheduler.Default;
return projects;
}
public static async Task<ITextDocument> GetTextDocumentAsync(this IWpfTextViewHost viewHost)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(_cancelAllToken);
viewHost.TextView.TextDataModel.DocumentBuffer.Properties.TryGetProperty(typeof(ITextDocument), out ITextDocument textDocument);
await TaskScheduler.Default;
return textDocument;
}
public static async Task ShowExceptionAsync(this AsyncPackage asyncPackage, Exception ex)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(_cancelAllToken);
if (_cancelAllToken.IsCancellationRequested) {
return;
}
string mainMessage = ex.ToString();
var messageSuffix = "";
if (FullVsVersion < LowestSupportedVersion) {
messageSuffix = $"{Environment.NewLine}This extension only receives updates for VS {LowestSupportedVersion}+, you are currently using {FullVsVersion}";
}
if (ex is FileNotFoundException fnf && !string.IsNullOrEmpty(fnf.FusionLog)) {
try {
var options = await asyncPackage.GetDialogPageAsync<ConverterOptionsPage>();
if (!options.BypassAssemblyLoadingErrors) {
options.BypassAssemblyLoadingErrors = true;
options.SaveSettingsToStorage();
mainMessage =
$"Assembly load issue detected. Tools->Options->Code Converter->BypassAssemblyLoadingErrors has now been automatically activated, please try again (but report this issue either way).{Environment.NewLine}";
}
} catch {
mainMessage =
$"Assembly load issue detected. Try activating Tools->Options->Code Converter->Bypass assembly loading errors and try again (but report this issue either way).{Environment.NewLine}";
}
mainMessage += $"{ex.Message}{Environment.NewLine}{GetShortStackTrace(ex)}";
messageSuffix += $"{Environment.NewLine}{fnf.FusionLog}{Environment.NewLine}";
}
MessageBox.Show($"An error has occurred during conversion - press Ctrl+C to copy the details: {mainMessage}{messageSuffix}",
Title, MessageBoxButton.OK, MessageBoxImage.Error);
}
private static string GetShortStackTrace(Exception ex)
{
var lines = ex.StackTrace.Split(Environment.NewLine.ToCharArray()).Where(l => !string.IsNullOrWhiteSpace(l)).ToList();
if (lines.Count < 5) return string.Join(Environment.NewLine, lines);
var summaryLines = lines.TakeWhile(l => !l.Contains("ICSharpCode.")).ToList();
summaryLines.Add(lines.ElementAt(summaryLines.Count));
summaryLines.Add(" ...");
summaryLines.Add(lines.Last());
return string.Join(Environment.NewLine, summaryLines);
}
/// <returns>true iff the user answers "OK"</returns>
public static async Task<bool> ShowMessageBoxAsync(string title, string msg, bool showCancelButton, bool defaultOk = true)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(_cancelAllToken);
if (_cancelAllToken.IsCancellationRequested) return false;
var userAnswer = MessageBox.Show(msg, title,
showCancelButton ? MessageBoxButton.OKCancel : MessageBoxButton.OK,
MessageBoxImage.Information,
defaultOk || !showCancelButton ? MessageBoxResult.OK : MessageBoxResult.Cancel);
return userAnswer == MessageBoxResult.OK;
}
public static async Task ShowMessageBoxAsync(string msg) => await ShowMessageBoxAsync(Title, msg, showCancelButton: false);
public static async Task EnsureBuiltAsync(IReadOnlyCollection<Project> projects, Func<string, Task> writeMessageAsync)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(_cancelAllToken);
var build = Dte.Solution.SolutionBuild;
if (build.BuildState == vsBuildState.vsBuildStateInProgress) {
throw new InvalidOperationException("Build in progress, please wait for it to complete before conversion.");
}
if (GetUpdateWarningsOrNull() is {} warning) await writeMessageAsync(warning);
if (projects.Count == 1 && build.ActiveConfiguration?.Name is { } configuration && projects.Single().UniqueName is {} uniqueName) {
await writeMessageAsync($"Building project '{uniqueName}' prior to conversion for maximum accuracy...");
build.BuildProject(configuration, uniqueName, true);
} else {
await writeMessageAsync("Building solution prior to conversion for maximum accuracy...");
build.Build(true);
}
await TaskScheduler.Default;
}
public static string GetUpdateWarningsOrNull()
{
if (FullVsVersion < LowestSupportedVersion && WeeksUpdatesStoppedFor > 1)
{
return
$"Deprecated: Code Converter no longer receives updates for Visual Studio {FullVsVersion}. Please update to the latest version of Visual Studio ({LowestSupportedVersion} at minimum).{Environment.NewLine}" +
$"See the {WeeksUpdatesStoppedFor} weeks of improvements you're missing at https://github.com/icsharpcode/CodeConverter/blob/master/CHANGELOG.md";
}
return null;
}
public static async Task WriteStatusBarTextAsync(IAsyncServiceProvider serviceProvider, string text)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(_cancelAllToken);
var statusBar = await serviceProvider.GetServiceAsync<SVsStatusbar, IVsStatusbar>();
if (statusBar == null)
return;
statusBar.IsFrozen(out int frozen);
if (frozen != 0) {
statusBar.FreezeOutput(0);
}
statusBar.SetText(text);
statusBar.FreezeOutput(1);
await TaskScheduler.Default;
}
public static async Task<Span?> GetFirstSelectedSpanInCurrentViewAsync(IAsyncServiceProvider serviceProvider,
Func<string, bool> predicate, bool mustHaveFocus)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(_cancelAllToken);
var span = await FirstSelectedSpanInCurrentViewPrivateAsync(serviceProvider, predicate, mustHaveFocus);
await TaskScheduler.Default;
return span;
}
public static async Task<(string FilePath, Span? Selection)> GetCurrentFilenameAndSelectionAsync(
IAsyncServiceProvider asyncServiceProvider, Func<string, bool> predicate, bool mustHaveFocus)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(_cancelAllToken);
var span = await GetFirstSelectedSpanInCurrentViewAsync(asyncServiceProvider, predicate, mustHaveFocus);
var currentViewHostAsync =
await GetCurrentViewHostAsync(asyncServiceProvider, predicate, mustHaveFocus);
if (currentViewHostAsync == null) return (null, null);
using (var textDocumentAsync = await currentViewHostAsync.GetTextDocumentAsync())
{
var result = (textDocumentAsync?.FilePath, span);
await TaskScheduler.Default;
return result;
}
}
public static async Task<CaretPosition> GetCaretPositionAsync(IAsyncServiceProvider serviceProvider)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(_cancelAllToken);
var viewHost = await GetCurrentViewHostAsync(serviceProvider,false);
ITextEdit edit = viewHost.TextView.TextBuffer.CreateEdit();
var caretPositionAsync = new CaretPosition(edit, viewHost.TextView.Caret.Position.BufferPosition.Position);
await TaskScheduler.Default;
return caretPositionAsync;
}
public static async Task<Project> GetFirstProjectContainingAsync(string documentFilePath)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(_cancelAllToken);
var containingProject = Dte.Solution.FindProjectItem(documentFilePath)?.ContainingProject;
await TaskScheduler.Default;
return containingProject;
}
internal class CaretPosition
{
private readonly ITextEdit _textEdit;
private readonly int _position;
public CaretPosition(ITextEdit textEdit, int position)
{
_textEdit = textEdit;
_position = position;
}
public async Task InsertAsync(string text)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(_cancelAllToken);
_textEdit.Insert(_position, text);
_textEdit.Apply();
_textEdit.Dispose();
}
}
private static IEnumerable<T> GetSelectedSolutionExplorerItems<T>() where T: class
{
ThreadHelper.ThrowIfNotOnUIThread();
var selectedObjects = (IEnumerable<object>) Dte.ToolWindows.SolutionExplorer.SelectedItems;
var selectedItems = selectedObjects.Cast<UIHierarchyItem>().ToList();
return ObjectOfType<T>(selectedItems);
}
private static IEnumerable<T> ObjectOfType<T>(IReadOnlyCollection<UIHierarchyItem> selectedItems) where T : class
{
#pragma warning disable VSTHRD010 // Invoke single-threaded types on Main thread
var returnType = typeof(T);
return selectedItems.Select(item => item.Object).Where(returnType.IsInstanceOfType).Cast<T>();
#pragma warning restore VSTHRD010 // Invoke single-threaded types on Main thread
}
private static async Task<IWpfTextViewHost> GetCurrentViewHostAsync(IAsyncServiceProvider serviceProvider, bool mustHaveFocus)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(_cancelAllToken);
var txtMgr = await serviceProvider.GetServiceAsync<SVsTextManager, IVsTextManager>();
if (txtMgr == null) {
return null;
}
txtMgr.GetActiveView(mustHaveFocus ? 1 : 0, null, out IVsTextView vTextView);
// ReSharper disable once SuspiciousTypeConversion.Global - COM Object
if (!(vTextView is IVsUserData userData)) {
return null;
}
Guid guidViewHost = DefGuidList.guidIWpfTextViewHost;
userData.GetData(ref guidViewHost, out var holder);
return holder as IWpfTextViewHost;
}
private static async Task<Span?> FirstSelectedSpanInCurrentViewPrivateAsync(
IAsyncServiceProvider serviceProvider,
Func<string, bool> predicate, bool mustHaveFocus)
{
var selection = await GetSelectionInCurrentViewAsync(serviceProvider, predicate, mustHaveFocus);
return selection?.SelectedSpans.First().Span;
}
private static async Task<ITextSelection> GetSelectionInCurrentViewAsync(IAsyncServiceProvider serviceProvider,
Func<string, bool> predicate, bool mustHaveFocus)
{
var viewHost = await GetCurrentViewHostAsync(serviceProvider, predicate, mustHaveFocus);
return viewHost?.TextView.Selection;
}
private static async Task<IWpfTextViewHost> GetCurrentViewHostAsync(IAsyncServiceProvider serviceProvider,
Func<string, bool> predicate, bool mustHaveFocus)
{
var viewHost = await GetCurrentViewHostAsync(serviceProvider, mustHaveFocus);
if (viewHost == null)
return null;
var textDocument = await viewHost.GetTextDocumentAsync();
return textDocument != null && predicate(textDocument.FilePath) ? viewHost : null;
}
}