361 строка
16 KiB
C#
361 строка
16 KiB
C#
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 => _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;
|
|
}
|
|
} |