-
Notifications
You must be signed in to change notification settings - Fork 222
/
Copy pathCodeConversion.cs
346 lines (295 loc) · 18.3 KB
/
CodeConversion.cs
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
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using CodeConv.Shared.Util;
using ICSharpCode.CodeConverter;
using ICSharpCode.CodeConverter.Shared;
using ICSharpCode.CodeConverter.VB;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServices;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Threading;
using IAsyncServiceProvider = Microsoft.VisualStudio.Shell.IAsyncServiceProvider;
using Project = EnvDTE.Project;
using Task = System.Threading.Tasks.Task;
namespace ICSharpCode.CodeConverter.VsExtension
{
internal class CodeConversion
{
public Func<Task<ConverterOptionsPage>> GetOptions { get; }
private readonly IAsyncServiceProvider _serviceProvider;
private readonly JoinableTaskFactory _joinableTaskFactory;
private readonly VisualStudioWorkspace _visualStudioWorkspace;
private static readonly string Intro = Environment.NewLine + Environment.NewLine + new string(Enumerable.Repeat('-', 80).ToArray()) + Environment.NewLine;
private readonly OutputWindow _outputWindow;
private readonly Cancellation _packageCancellation;
private string SolutionDir => Path.GetDirectoryName(_visualStudioWorkspace.CurrentSolution.FilePath);
public static async Task<CodeConversion> CreateAsync(CodeConverterPackage serviceProvider, VisualStudioWorkspace visualStudioWorkspace, Func<Task<ConverterOptionsPage>> getOptions)
{
var options = await getOptions();
AppDomain.CurrentDomain.UseVersionAgnosticAssemblyResolution(options.BypassAssemblyLoadingErrors);
return new CodeConversion(serviceProvider, serviceProvider.JoinableTaskFactory, serviceProvider.PackageCancellation, visualStudioWorkspace,
getOptions, await OutputWindow.CreateAsync());
}
public CodeConversion(IAsyncServiceProvider serviceProvider,
JoinableTaskFactory joinableTaskFactory, Cancellation packageCancellation, VisualStudioWorkspace visualStudioWorkspace,
Func<Task<ConverterOptionsPage>> getOptions, OutputWindow outputWindow)
{
JoinableTaskFactorySingleton.Instance = joinableTaskFactory;
GetOptions = getOptions;
_serviceProvider = serviceProvider;
_joinableTaskFactory = joinableTaskFactory;
_visualStudioWorkspace = visualStudioWorkspace;
_outputWindow = outputWindow;
_packageCancellation = packageCancellation;
}
public async Task ConvertProjectsAsync<TLanguageConversion>(IReadOnlyCollection<Project> selectedProjects, CancellationToken cancellationToken) where TLanguageConversion : ILanguageConversion, new()
{
try {
await EnsureBuiltAsync();
await _joinableTaskFactory.RunAsync(async () => {
var convertedFiles = ConvertProjectUnhandled<TLanguageConversion>(selectedProjects, cancellationToken);
await WriteConvertedFilesAndShowSummaryAsync(convertedFiles);
});
} catch (OperationCanceledException) {
if (!_packageCancellation.CancelAll.IsCancellationRequested) {
await _outputWindow.WriteToOutputWindowAsync(Environment.NewLine + "Previous conversion cancelled", forceShow: true);
}
}
}
public async Task ConvertDocumentAsync<TLanguageConversion>(string documentFilePath, Span selected, CancellationToken cancellationToken) where TLanguageConversion : ILanguageConversion, new()
{
try {
await EnsureBuiltAsync();
var conversionResult = await _joinableTaskFactory.RunAsync(async () => {
var result = await ConvertDocumentUnhandledAsync<TLanguageConversion>(documentFilePath, selected, cancellationToken);
await WriteConvertedFilesAndShowSummaryAsync(new[] { result }.ToAsyncEnumerable());
return result;
});
if ((await GetOptions()).CopyResultToClipboardForSingleDocument) {
await SetClipboardTextOnUiThreadAsync(conversionResult.ConvertedCode ?? conversionResult.GetExceptionsAsString());
await _outputWindow.WriteToOutputWindowAsync(Environment.NewLine + "Conversion result copied to clipboard.");
await VisualStudioInteraction.ShowMessageBoxAsync(_serviceProvider, "Conversion result copied to clipboard.", $"Conversion result copied to clipboard. {conversionResult.GetExceptionsAsString()}", false);
}
} catch (OperationCanceledException) {
if (!_packageCancellation.CancelAll.IsCancellationRequested) {
await _outputWindow.WriteToOutputWindowAsync(Environment.NewLine + "Previous conversion cancelled", forceShow: true);
}
}
}
/// <remarks>
/// https://github.com/icsharpcode/CodeConverter/issues/592
/// https://github.com/dotnet/roslyn/issues/6615
/// </remarks>
private async Task EnsureBuiltAsync()
{
await VisualStudioInteraction.EnsureBuiltAsync(m => _outputWindow.WriteToOutputWindowAsync(m));
}
private static async Task SetClipboardTextOnUiThreadAsync(string conversionResultConvertedCode)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
Clipboard.SetText(conversionResultConvertedCode);
await TaskScheduler.Default;
}
private async Task WriteConvertedFilesAndShowSummaryAsync(IAsyncEnumerable<ConversionResult> convertedFiles)
{
await _outputWindow.WriteToOutputWindowAsync(Intro, forceShow: true);
var files = new List<string>();
var filesToOverwrite = new List<ConversionResult>();
var errors = new List<string>();
string longestFilePath = null;
var longestFileLength = -1;
await foreach (var convertedFile in convertedFiles) {
if (convertedFile.SourcePathOrNull == null) continue;
if (WillOverwriteSource(convertedFile)) {
filesToOverwrite.Add(convertedFile);
continue;
}
var exceptionsAsString = convertedFile.GetExceptionsAsString();
if (!string.IsNullOrWhiteSpace(exceptionsAsString)) {
errors.Add(exceptionsAsString);
}
if (convertedFile.Success) {
files.Add(convertedFile.TargetPathOrNull);
if (convertedFile.ConvertedCode.Length > longestFileLength) {
longestFileLength = convertedFile.ConvertedCode.Length;
longestFilePath = convertedFile.TargetPathOrNull;
}
convertedFile.WriteToFile();
}
}
await FinalizeConversionAsync(files, errors, longestFilePath, filesToOverwrite);
}
private async Task FinalizeConversionAsync(List<string> files, List<string> errors, string longestFilePath, List<ConversionResult> filesToOverwrite)
{
var options = await GetOptions();
var pathsToOverwrite = filesToOverwrite.Select(f => PathRelativeToSolutionDir(f.SourcePathOrNull));
var shouldOverwriteSolutionAndProjectFiles =
filesToOverwrite.Any() &&
(options.AlwaysOverwriteFiles || await UserHasConfirmedOverwriteAsync(files, errors, pathsToOverwrite.ToList()));
if (shouldOverwriteSolutionAndProjectFiles)
{
var titleMessage = options.CreateBackups ? "Creating backups and overwriting files:" : "Overwriting files:" + "";
await _outputWindow.WriteToOutputWindowAsync(Environment.NewLine + titleMessage);
foreach (var fileToOverwrite in filesToOverwrite)
{
if (options.CreateBackups) File.Copy(fileToOverwrite.SourcePathOrNull, fileToOverwrite.SourcePathOrNull + ".bak", true);
fileToOverwrite.WriteToFile();
var targetPathRelativeToSolutionDir = PathRelativeToSolutionDir(fileToOverwrite.TargetPathOrNull);
await _outputWindow.WriteToOutputWindowAsync(Environment.NewLine + $"* {targetPathRelativeToSolutionDir}");
}
files = files.Concat(filesToOverwrite.Select(f => f.SourcePathOrNull)).ToList();
} else if (longestFilePath != null) {
await (await VisualStudioInteraction.OpenFileAsync(new FileInfo(longestFilePath))).SelectAllAsync();
}
var conversionSummary = await GetConversionSummaryAsync(files, errors);
await _outputWindow.WriteToOutputWindowAsync(conversionSummary, false, true);
}
private async Task<bool> UserHasConfirmedOverwriteAsync(List<string> files, List<string> errors, IReadOnlyCollection<string> pathsToOverwrite)
{
var maxExamples = 30; // Avoid a huge unreadable dialog going off the screen
var exampleText = pathsToOverwrite.Count > maxExamples ? $". First {maxExamples} examples" : "";
await _outputWindow.WriteToOutputWindowAsync(Environment.NewLine + "Awaiting user confirmation for overwrite....", forceShow: true);
bool shouldOverwrite = await VisualStudioInteraction.ShowMessageBoxAsync(_serviceProvider,
"Overwrite solution and referencing projects?",
$@"The current solution file and any referencing projects will be overwritten to reference the new project(s){exampleText}:
* {string.Join(Environment.NewLine + "* ", pathsToOverwrite.Take(maxExamples))}
The old contents will be copied to 'currentFilename.bak'.
Please 'Reload All' when Visual Studio prompts you.", true, files.Count > errors.Count);
await _outputWindow.WriteToOutputWindowAsync(shouldOverwrite ? "confirmed" : "declined");
return shouldOverwrite;
}
private static bool WillOverwriteSource(ConversionResult convertedFile)
{
return string.Equals(convertedFile.SourcePathOrNull, convertedFile.TargetPathOrNull, StringComparison.OrdinalIgnoreCase);
}
private string PathRelativeToSolutionDir(string path)
{
return path.Replace(SolutionDir, "")
.Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}
private async Task<string> GetConversionSummaryAsync(IReadOnlyCollection<string> files, IReadOnlyCollection<string> errors)
{
var oneLine = "Code conversion failed";
var successSummary = "";
if (files.Any()) {
oneLine = "Code conversion completed";
successSummary = $"{files.Count} files have been written to disk.";
}
if (errors.Any()) {
oneLine += $" with {errors.Count} error" + (errors.Count == 1 ? "" : "s");
}
if (files.Count > errors.Count * 2) {
successSummary += Environment.NewLine + "Please report issues at https://github.com/icsharpcode/CodeConverter/issues and consider rating at https://marketplace.visualstudio.com/items?itemName=SharpDevelopTeam.CodeConverter#review-details";
} else {
successSummary += Environment.NewLine + "Please report issues at https://github.com/icsharpcode/CodeConverter/issues";
}
await VisualStudioInteraction.WriteStatusBarTextAsync(_serviceProvider, oneLine + " - see output window");
return Environment.NewLine + Environment.NewLine
+ oneLine
+ Environment.NewLine + successSummary
+ Environment.NewLine;
}
private async Task<ConversionResult> ConvertDocumentUnhandledAsync<TLanguageConversion>(string documentPath, Span selected, CancellationToken cancellationToken) where TLanguageConversion : ILanguageConversion, new()
{
await _outputWindow.WriteToOutputWindowAsync($"Converting {documentPath}...", true, true);
//TODO Figure out when there are multiple document ids for a single file path
var documentId = _visualStudioWorkspace.CurrentSolution.GetDocumentIdsWithFilePath(documentPath).SingleOrDefault();
if (documentId == null) {
//If document doesn't belong to any project
await _outputWindow.WriteToOutputWindowAsync("File is not part of a compiling project, using best effort text conversion (less accurate).");
return await ConvertFileTextAsync<TLanguageConversion>(documentPath, selected, cancellationToken);
}
var document = _visualStudioWorkspace.CurrentSolution.GetDocument(documentId);
var selectedTextSpan = new TextSpan(selected.Start, selected.Length);
return await ProjectConversion.ConvertSingleAsync<TLanguageConversion>(document, new SingleConversionOptions {SelectedTextSpan = selectedTextSpan, AbandonOptionalTasksAfter = await GetAbandonOptionalTasksAfterAsync()}, CreateOutputWindowProgress(), cancellationToken);
}
private async Task<ConversionResult> ConvertFileTextAsync<TLanguageConversion>(string documentPath,
Span selected, CancellationToken cancellationToken)
where TLanguageConversion : ILanguageConversion, new()
{
var documentText = File.ReadAllText(documentPath);
if (selected.Length > 0 && documentText.Length >= selected.End) {
documentText = documentText.Substring(selected.Start, selected.Length);
}
var convertTextOnly = await ConvertTextAsync<TLanguageConversion>(documentText, cancellationToken, documentPath);
convertTextOnly.SourcePathOrNull = documentPath;
return convertTextOnly;
}
private async IAsyncEnumerable<ConversionResult> ConvertProjectUnhandled<TLanguageConversion>(IReadOnlyCollection<Project> selectedProjects, [EnumeratorCancellation] CancellationToken cancellationToken)
where TLanguageConversion : ILanguageConversion, new()
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
if (selectedProjects.Count > 1) {
await _outputWindow.WriteToOutputWindowAsync($"Converting {selectedProjects.Count} projects...", true, true);
}
var projectsByPath =
_visualStudioWorkspace.CurrentSolution.Projects.ToLookup(p => p.FilePath, p => p);
#pragma warning disable VSTHRD010 // Invoke single-threaded types on Main thread - ToList ensures this happens within the same thread just switched to above
var projects = selectedProjects.Select(p => projectsByPath[p.FullName].First()).ToList();
#pragma warning restore VSTHRD010 // Invoke single-threaded types on Main thread
await TaskScheduler.Default;
var conversionOptions = new ConversionOptions(){AbandonOptionalTasksAfter = await GetAbandonOptionalTasksAfterAsync()};
var solutionConverter = SolutionConverter.CreateFor<TLanguageConversion>(projects, progress: CreateOutputWindowProgress(), cancellationToken: cancellationToken, conversionOptions: conversionOptions);
await foreach (var result in solutionConverter.Convert()) yield return result;
}
private async Task<TimeSpan> GetAbandonOptionalTasksAfterAsync() => TimeSpan.FromMinutes((await GetOptions()).FormattingTimeout);
private Progress<ConversionProgress> CreateOutputWindowProgress()
{
return new Progress<ConversionProgress>(s => {
_outputWindow.WriteToOutputWindowAsync(s.ToString()).ForgetNoThrow();
});
}
public static bool IsCSFileName(string fileName)
{
return fileName.EndsWith(".cs", StringComparison.OrdinalIgnoreCase);
}
/// <remarks>https://github.com/dotnet/roslyn/blob/91571a3bb038e05e7bf2ab87510273a1017faed0/src/VisualStudio/VisualBasic/Impl/LanguageService/VisualBasicPackage.vb#L45-L52</remarks>
public static bool IsVBFileName(string fileName)
{
switch (Path.GetExtension(fileName).ToLower()) {
case ".vb":
case ".bas":
case ".cls":
case ".ctl":
case ".dob":
case ".dsr":
case ".frm":
case ".pag":
return true;
}
return false;
}
public async Task PasteAsAsync<TLanguageConversion>(CancellationToken cancellationToken) where TLanguageConversion : ILanguageConversion, new()
{
var caretPosition = await VisualStudioInteraction.GetCaretPositionAsync(_serviceProvider);
_outputWindow.WriteToOutputWindowAsync("Converting clipboard text...", true, true).Forget();
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
string text = Clipboard.GetText();
var convertTextOnly = await _joinableTaskFactory.RunAsync(async () =>
await ConvertTextAsync<TLanguageConversion>(text, cancellationToken)
);
await caretPosition.InsertAsync(convertTextOnly.ConvertedCode);
}
private async Task<ConversionResult> ConvertTextAsync<TLanguageConversion>(string text,
CancellationToken cancellationToken, string documentPath = null) where TLanguageConversion : ILanguageConversion, new()
{
return await ProjectConversion.ConvertTextAsync<TLanguageConversion>(text,
await CreateTextConversionOptionsAsync(documentPath),
cancellationToken: cancellationToken,
progress: CreateOutputWindowProgress());
}
private async Task<TextConversionOptions> CreateTextConversionOptionsAsync(string documentPath = null)
{
return new TextConversionOptions(DefaultReferences.NetStandard2, documentPath) {
AbandonOptionalTasksAfter = await GetAbandonOptionalTasksAfterAsync()
};
}
}
}