diff --git a/README.md b/README.md index 76be8f4fff..dcc20622b8 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Usage: dotnet-format [options] Options: + -f, --folder The folder to operate on. Cannot be used with the `--workspace` option. -w, --workspace The solution or project file to operate on. If a file is not specified, the command will search the current directory for one. -v, --verbosity Set the verbosity level. Allowed values are q[uiet], m[inimal], n[ormal], d[etailed], and @@ -56,6 +57,7 @@ Add `format` after `dotnet` and before the command arguments that you want to ru | Examples | Description | | -------------------------------------------------------- |---------------------------------------------------------------------------------------------- | | dotnet **format** | Formats the project or solution in the current directory. | +| dotnet **format** -f <folder> | Formats a particular folder and subfolders. | dotnet **format** -w <workspace> | Formats a specific project or solution. | | dotnet **format** -v diag | Formats with very verbose logging. | | dotnet **format** --files Programs.cs,Utility\Logging.cs | Formats the files Program.cs and Utility\Logging.cs | diff --git a/azure-pipelines-integration.yml b/azure-pipelines-integration.yml index a379472e4a..003e3010cf 100644 --- a/azure-pipelines-integration.yml +++ b/azure-pipelines-integration.yml @@ -19,35 +19,35 @@ jobs: format: _repo: "https://github.com/dotnet/format" _repoName: "dotnet/format" - _sha: "b6db94c361b1ecd0558fc7429335ec01133773ed" + _sha: "4ff73616469a2aaa5ff07d11b7f6951eca83d76e" roslyn: _repo: "https://github.com/dotnet/roslyn" _repoName: "dotnet/roslyn" - _sha: "665dc224dcfefb66dce215ca0204363172b611ad" + _sha: "1bd191ea896b19e3b06a152f815f3a998e87049a" cli: _repo: "https://github.com/dotnet/cli" _repoName: "dotnet/cli" - _sha: "8d0f2a593ec98ccc1ef2141452d7dccadc34ac40" + _sha: "45f1c5f7f3275ff483c804e7c90bf61731a83e97" project-system: _repo: "https://github.com/dotnet/project-system" _repoName: "dotnet/project-system" - _sha: "80580ad27c5544f00f4abb9db930e698ce305285" + _sha: "fc3b12e47adaad6e4813dc600acf190156fecc24" msbuild: _repo: "https://github.com/Microsoft/msbuild" _repoName: "Microsoft/msbuild" - _sha: "d54d2e0180b99b7773f0fef2cdb03168f134d9aa" + _sha: "e92633a735b0e23e2cbf818d02ba89dd7a426f89" Blazor: _repo: "https://github.com/aspnet/Blazor" _repoName: "aspnet/Blazor" - _sha: "3addb6169afcae1b5c6a64de4a7cd02bd270ffed" + _sha: "7eeab316fa122b69a9bd777c93dcc78bc6a68905" EntityFrameworkCore: _repo: "https://github.com/aspnet/EntityFrameworkCore" _repoName: "aspnet/EntityFrameworkCore" - _sha: "090f385ffe4f2cd1a3e6b794523bdf574862c298" + _sha: "94f5bdf081f9a774f5d7f01bfa279bac26dab303" EntityFramework6: _repo: "https://github.com/aspnet/EntityFramework6" _repoName: "aspnet/EntityFramework6" - _sha: "936befcd88a4b58be6f35038248cd5337aca4967" + _sha: "9099ad2f6e5936ecdf549f88394b5a59fa75c869" timeoutInMinutes: 20 steps: - script: eng\integration-test.cmd -repo '$(_repo)' -sha '$(_sha)' -testPath '$(Build.SourcesDirectory)\temp' diff --git a/eng/format-verifier.ps1 b/eng/format-verifier.ps1 index cbe2a082ef..97378779d1 100644 --- a/eng/format-verifier.ps1 +++ b/eng/format-verifier.ps1 @@ -5,17 +5,7 @@ Param( [string]$testPath ) -function Clone-Repo([string]$repo, [string]$sha, [string]$repoPath) { - $currentLocation = Get-Location - - git.exe clone $repo $repoPath - - Set-Location $repoPath - - git.exe checkout $sha - - Set-Location $currentLocation -} +$currentLocation = Get-Location if (!(Test-Path $testPath)) { New-Item -ItemType Directory -Force -Path $testPath | Out-Null @@ -27,19 +17,37 @@ try { $repoPath = Join-Path $testPath $folderName Write-Output "$(Get-Date) - Cloning $repoName." - Clone-Repo $repo $sha $repoPath + git.exe clone $repo $repoPath + + Set-Location $repoPath + + git.exe checkout $sha Write-Output "$(Get-Date) - Finding solutions." - $solutions = Get-ChildItem -Path $repoPath -Filter *.sln -Recurse -Depth 2 | Select-Object -ExpandProperty FullName | Where-Object { $_ -match '.sln$' } + $solutions = Get-ChildItem -Filter *.sln -Recurse -Depth 2 | Select-Object -ExpandProperty FullName | Where-Object { $_ -match '.sln$' } + + # We invoke build.ps1 ourselves because running `restore.cmd` invokes the build.ps1 + # in a child process which means added .NET Core SDKs aren't visible to this process. + if (Test-Path '.\eng\Build.ps1') { + Write-Output "$(Get-Date) - Running Build.ps1 -restore" + .\eng\Build.ps1 -restore + } + elseif (Test-Path '.\eng\common\Build.ps1') { + Write-Output "$(Get-Date) - Running Build.ps1 -restore" + .\eng\common\Build.ps1 -restore + } foreach ($solution in $solutions) { + $solutionPath = Split-Path $solution $solutionFile = Split-Path $solution -leaf + Set-Location $solutionPath + Write-Output "$(Get-Date) - $solutionFile - Restoring" dotnet.exe restore $solution Write-Output "$(Get-Date) - $solutionFile - Formatting" - $output = dotnet.exe run -p .\src\dotnet-format.csproj -c Release -- -w $solution -v d --dry-run | Out-String + $output = dotnet.exe run -p "$currentLocation\src\dotnet-format.csproj" -c Release -- -w $solution -v d --dry-run | Out-String Write-Output $output.TrimEnd() if ($LastExitCode -ne 0) { @@ -59,6 +67,8 @@ catch { exit -1 } finally { + Set-Location $currentLocation + Remove-Item $repoPath -Force -Recurse Write-Output "$(Get-Date) - Deleted $repoName." } diff --git a/src/CodeFormatter.cs b/src/CodeFormatter.cs index 2ab3299981..33bbf55c94 100644 --- a/src/CodeFormatter.cs +++ b/src/CodeFormatter.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Tools.Utilities; using Microsoft.CodeAnalysis.Tools.Formatters; +using Microsoft.CodeAnalysis.Tools.Workspaces; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.CodingConventions; @@ -32,7 +33,7 @@ public static async Task FormatWorkspaceAsync( ILogger logger, CancellationToken cancellationToken) { - var (workspaceFilePath, isSolution, logLevel, saveFormattedFiles, _, filesToFormat) = options; + var (workspaceFilePath, workspaceType, logLevel, saveFormattedFiles, _, filesToFormat) = options; var logWorkspaceWarnings = logLevel == LogLevel.Trace; logger.LogInformation(string.Format(Resources.Formatting_code_files_in_workspace_0, workspaceFilePath)); @@ -42,7 +43,7 @@ public static async Task FormatWorkspaceAsync( var workspaceStopwatch = Stopwatch.StartNew(); using (var workspace = await OpenWorkspaceAsync( - workspaceFilePath, isSolution, logWorkspaceWarnings, logger, cancellationToken).ConfigureAwait(false)) + workspaceFilePath, workspaceType, filesToFormat, logWorkspaceWarnings, logger, cancellationToken).ConfigureAwait(false)) { if (workspace is null) { @@ -52,7 +53,7 @@ public static async Task FormatWorkspaceAsync( var loadWorkspaceMS = workspaceStopwatch.ElapsedMilliseconds; logger.LogTrace(Resources.Complete_in_0_ms, workspaceStopwatch.ElapsedMilliseconds); - var projectPath = isSolution ? string.Empty : workspaceFilePath; + var projectPath = workspaceType == WorkspaceType.Project ? workspaceFilePath : string.Empty; var solution = workspace.CurrentSolution; logger.LogTrace(Resources.Determining_formattable_files); @@ -101,8 +102,26 @@ public static async Task FormatWorkspaceAsync( } private static async Task OpenWorkspaceAsync( + string workspacePath, + WorkspaceType workspaceType, + ImmutableHashSet filesToFormat, + bool logWorkspaceWarnings, + ILogger logger, + CancellationToken cancellationToken) + { + if (workspaceType == WorkspaceType.Folder) + { + var folderWorkspace = FolderWorkspace.Create(); + await folderWorkspace.OpenFolder(workspacePath, filesToFormat, cancellationToken); + return folderWorkspace; + } + + return await OpenMSBuildWorkspaceAsync(workspacePath, workspaceType, logWorkspaceWarnings, logger, cancellationToken); + } + + private static async Task OpenMSBuildWorkspaceAsync( string solutionOrProjectPath, - bool isSolution, + WorkspaceType workspaceType, bool logWorkspaceWarnings, ILogger logger, CancellationToken cancellationToken) @@ -119,10 +138,8 @@ private static async Task OpenWorkspaceAsync( }; var workspace = MSBuildWorkspace.Create(properties); - workspace.WorkspaceFailed += LogWorkspaceWarnings; - var projectPath = string.Empty; - if (isSolution) + if (workspaceType == WorkspaceType.Solution) { await workspace.OpenSolutionAsync(solutionOrProjectPath, cancellationToken: cancellationToken).ConfigureAwait(false); } @@ -131,7 +148,6 @@ private static async Task OpenWorkspaceAsync( try { await workspace.OpenProjectAsync(solutionOrProjectPath, cancellationToken: cancellationToken).ConfigureAwait(false); - projectPath = solutionOrProjectPath; } catch (InvalidOperationException) { @@ -141,25 +157,33 @@ private static async Task OpenWorkspaceAsync( } } - workspace.WorkspaceFailed -= LogWorkspaceWarnings; + LogWorkspaceDiagnostics(logger, logWorkspaceWarnings, workspace.Diagnostics); return workspace; + } - void LogWorkspaceWarnings(object sender, WorkspaceDiagnosticEventArgs args) + private static void LogWorkspaceDiagnostics(ILogger logger, bool logWorkspaceWarnings, ImmutableList diagnostics) + { + if (!logWorkspaceWarnings) { - if (args.Diagnostic.Kind == WorkspaceDiagnosticKind.Failure) + if (diagnostics.Count > 0) { - return; + logger.LogWarning(Resources.Warnings_were_encountered_while_loading_the_workspace_Set_the_verbosity_option_to_the_diagnostic_level_to_log_warnings); } - if (!logWorkspaceWarnings) + return; + } + + foreach (var diagnostic in diagnostics) + { + if (diagnostic.Kind == WorkspaceDiagnosticKind.Failure) { - logger.LogWarning(Resources.Warnings_were_encountered_while_loading_the_workspace_Set_the_verbosity_option_to_the_diagnostic_level_to_log_warnings); - ((MSBuildWorkspace)sender).WorkspaceFailed -= LogWorkspaceWarnings; - return; + logger.LogError(diagnostic.Message); + } + else + { + logger.LogWarning(diagnostic.Message); } - - logger.LogWarning(args.Diagnostic.Message); } } @@ -212,8 +236,8 @@ private static async Task RunCodeFormattersAsync( fileCount += project.DocumentIds.Count; // Get project documents and options with .editorconfig settings applied. - var getProjectDocuments = project.DocumentIds.Select(documentId => Task.Run(async () => await GetDocumentAndOptions( - project, documentId, filesToFormat, codingConventionsManager, optionsApplier, cancellationToken).ConfigureAwait(false), cancellationToken)); + var getProjectDocuments = project.DocumentIds.Select(documentId => GetDocumentAndOptions( + project, documentId, filesToFormat, codingConventionsManager, optionsApplier, cancellationToken)); getDocumentsAndOptions.AddRange(getProjectDocuments); } @@ -248,7 +272,7 @@ private static async Task RunCodeFormattersAsync( return (fileCount, formattableFiles.ToImmutableArray()); } - private static async Task<(Document, OptionSet, ICodingConventionsSnapshot, bool)> GetDocumentAndOptions( + private static async Task<(Document, OptionSet, ICodingConventionsSnapshot, bool)> GetDocumentAndOptions( Project project, DocumentId documentId, ImmutableHashSet filesToFormat, diff --git a/src/FormatOptions.cs b/src/FormatOptions.cs index 348ba40039..0c4a27a358 100644 --- a/src/FormatOptions.cs +++ b/src/FormatOptions.cs @@ -1,4 +1,6 @@ -using System.Collections.Immutable; +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Immutable; using Microsoft.Extensions.Logging; namespace Microsoft.CodeAnalysis.Tools @@ -6,7 +8,7 @@ namespace Microsoft.CodeAnalysis.Tools internal class FormatOptions { public string WorkspaceFilePath { get; } - public bool IsSolution { get; } + public WorkspaceType WorkspaceType { get; } public LogLevel LogLevel { get; } public bool SaveFormattedFiles { get; } public bool ChangesAreErrors { get; } @@ -14,14 +16,14 @@ internal class FormatOptions public FormatOptions( string workspaceFilePath, - bool isSolution, + WorkspaceType workspaceType, LogLevel logLevel, bool saveFormattedFiles, bool changesAreErrors, ImmutableHashSet filesToFormat) { WorkspaceFilePath = workspaceFilePath; - IsSolution = isSolution; + WorkspaceType = workspaceType; LogLevel = logLevel; SaveFormattedFiles = saveFormattedFiles; ChangesAreErrors = changesAreErrors; @@ -30,14 +32,14 @@ public FormatOptions( public void Deconstruct( out string workspaceFilePath, - out bool isSolution, + out WorkspaceType workspaceType, out LogLevel logLevel, out bool saveFormattedFiles, out bool changesAreErrors, out ImmutableHashSet filesToFormat) { workspaceFilePath = WorkspaceFilePath; - isSolution = IsSolution; + workspaceType = WorkspaceType; logLevel = LogLevel; saveFormattedFiles = SaveFormattedFiles; changesAreErrors = ChangesAreErrors; diff --git a/src/Formatters/DocumentFormatter.cs b/src/Formatters/DocumentFormatter.cs index c2c0a5f6bf..279ba2fc69 100644 --- a/src/Formatters/DocumentFormatter.cs +++ b/src/Formatters/DocumentFormatter.cs @@ -78,8 +78,6 @@ protected abstract Task FormatFileAsync( ILogger logger, CancellationToken cancellationToken) { - logger.LogTrace(Resources.Formatting_code_file_0, Path.GetFileName(document.FilePath)); - var originalSourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); var formattedSourceText = await FormatFileAsync(document, originalSourceText, options, codingConventions, formatOptions, logger, cancellationToken).ConfigureAwait(false); @@ -113,7 +111,7 @@ private async Task ApplyFileChangesAsync( continue; } - if (!formatOptions.SaveFormattedFiles) + if (!formatOptions.SaveFormattedFiles || formatOptions.LogLevel == LogLevel.Trace) { // Log formatting changes as errors when we are doing a dry-run. LogFormattingChanges(formatOptions.WorkspaceFilePath, document.FilePath, originalText, formattedText, formatOptions.ChangesAreErrors, logger); diff --git a/src/Program.cs b/src/Program.cs index 61dee16a04..270b231b9e 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -30,6 +30,7 @@ private static async Task Main(string[] args) .RegisterWithDotnetSuggest() .UseParseErrorReporting() .UseExceptionHandler() + .AddOption(new Option(new[] { "-f", "--folder" }, Resources.The_folder_to_operate_on_Cannot_be_used_with_the_workspace_option, new Argument(() => null))) .AddOption(new Option(new[] { "-w", "--workspace" }, Resources.The_solution_or_project_file_to_operate_on_If_a_file_is_not_specified_the_command_will_search_the_current_directory_for_one, new Argument(() => null))) .AddOption(new Option(new[] { "-v", "--verbosity" }, Resources.Set_the_verbosity_level_Allowed_values_are_quiet_minimal_normal_detailed_and_diagnostic, new Argument() { Arity = ArgumentArity.ExactlyOne }.FromAmong(_verbosityLevels))) .AddOption(new Option(new[] { "--dry-run" }, Resources.Format_files_but_do_not_save_changes_to_disk, new Argument())) @@ -41,7 +42,7 @@ private static async Task Main(string[] args) return await parser.InvokeAsync(args).ConfigureAwait(false); } - public static async Task Run(string workspace, string verbosity, bool dryRun, bool check, string files, IConsole console = null) + public static async Task Run(string folder, string workspace, string verbosity, bool dryRun, bool check, string files, IConsole console = null) { // Setup logging. var serviceCollection = new ServiceCollection(); @@ -65,16 +66,41 @@ public static async Task Run(string workspace, string verbosity, bool dryRu { currentDirectory = Environment.CurrentDirectory; - var workingDirectory = Directory.GetCurrentDirectory(); - var (isSolution, workspacePath) = MSBuildWorkspaceFinder.FindWorkspace(workingDirectory, workspace); + string workspaceDirectory; + string workspacePath; + WorkspaceType workspaceType; - // To ensure we get the version of MSBuild packaged with the dotnet SDK used by the - // workspace, use its directory as our working directory which will take into account - // a global.json if present. - var workspaceDirectory = Path.GetDirectoryName(workspacePath); - Environment.CurrentDirectory = workingDirectory; + if (!string.IsNullOrEmpty(folder) && !string.IsNullOrEmpty(workspace)) + { + logger.LogWarning(Resources.Cannot_specify_both_folder_and_workspace_options); + return 1; + } + + if (!string.IsNullOrEmpty(folder)) + { + folder = Path.GetFullPath(folder, Environment.CurrentDirectory); + workspacePath = folder; + workspaceDirectory = workspacePath; + workspaceType = WorkspaceType.Folder; + } + else + { + var (isSolution, workspaceFilePath) = MSBuildWorkspaceFinder.FindWorkspace(currentDirectory, workspace); + + workspacePath = workspaceFilePath; + workspaceType = isSolution + ? WorkspaceType.Solution + : WorkspaceType.Project; - var filesToFormat = GetFilesToFormat(files); + // To ensure we get the version of MSBuild packaged with the dotnet SDK used by the + // workspace, use its directory as our working directory which will take into account + // a global.json if present. + workspaceDirectory = Path.GetDirectoryName(workspacePath); + } + + Environment.CurrentDirectory = workspaceDirectory; + + var filesToFormat = GetFilesToFormat(files, folder); // Since we are running as a dotnet tool we should be able to find an instance of // MSBuild in a .NET Core SDK. @@ -90,7 +116,7 @@ public static async Task Run(string workspace, string verbosity, bool dryRu var formatOptions = new FormatOptions( workspacePath, - isSolution, + workspaceType, logLevel, saveFormattedFiles: !dryRun, changesAreErrors: check, @@ -164,16 +190,26 @@ private static void ConfigureServices(ServiceCollection serviceCollection, ICons /// /// Converts a comma-separated list of relative file paths to a hashmap of full file paths. /// - internal static ImmutableHashSet GetFilesToFormat(string files) + internal static ImmutableHashSet GetFilesToFormat(string files, string folder) { if (string.IsNullOrEmpty(files)) { return ImmutableHashSet.Create(); } - return files.Split(',') - .Select(path => Path.GetFullPath(path, Environment.CurrentDirectory)) - .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); + if (string.IsNullOrEmpty(folder)) + { + return files.Split(',') + .Select(path => Path.GetFullPath(path, Environment.CurrentDirectory)) + .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); + } + else + { + return files.Split(',') + .Select(path => Path.GetFullPath(path, Environment.CurrentDirectory)) + .Where(path => path.StartsWith(folder)) + .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); + } } } } diff --git a/src/Resources.resx b/src/Resources.resx index 2a86be8ca3..a7331817aa 100644 --- a/src/Resources.resx +++ b/src/Resources.resx @@ -204,4 +204,10 @@ Fix file encoding. + + The folder to operate on. Cannot be used with the `--workspace` option. + + + Cannot specify both folder and workspace options. + \ No newline at end of file diff --git a/src/WorkspaceType.cs b/src/WorkspaceType.cs new file mode 100644 index 0000000000..2069a22d84 --- /dev/null +++ b/src/WorkspaceType.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +namespace Microsoft.CodeAnalysis.Tools +{ + internal enum WorkspaceType + { + Folder, + Project, + Solution + } +} diff --git a/src/Workspaces/FolderWorkspace.cs b/src/Workspaces/FolderWorkspace.cs new file mode 100644 index 0000000000..1f8a068ac8 --- /dev/null +++ b/src/Workspaces/FolderWorkspace.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Tools.Workspaces +{ + internal sealed partial class FolderWorkspace : Workspace + { + private static readonly Encoding DefaultEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + + private FolderWorkspace(HostServices hostServices) + : base(hostServices, "Folder") + { + } + + public static FolderWorkspace Create() + { + return Create(MSBuildMefHostServices.DefaultServices); + } + + public static FolderWorkspace Create(HostServices hostServices) + { + return new FolderWorkspace(hostServices); + } + + public async Task OpenFolder(string folderPath, ImmutableHashSet filesToInclude, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(folderPath) || !Directory.Exists(folderPath)) + { + throw new ArgumentException($"Folder '{folderPath}' does not exist.", nameof(folderPath)); + } + + ClearSolution(); + + var solutionInfo = await FolderSolutionLoader.LoadSolutionInfoAsync(folderPath, filesToInclude, cancellationToken).ConfigureAwait(false); + + OnSolutionAdded(solutionInfo); + + return CurrentSolution; + } + + public override bool CanApplyChange(ApplyChangesKind feature) + { + // Whitespace formatting should only produce document changes; no other types of changes are supported. + return feature == ApplyChangesKind.ChangeDocument; + } + + protected override void ApplyDocumentTextChanged(DocumentId documentId, SourceText text) + { + var document = this.CurrentSolution.GetDocument(documentId); + if (document != null) + { + SaveDocumentText(documentId, document.FilePath, text, text.Encoding); + OnDocumentTextChanged(documentId, text, PreservationMode.PreserveValue); + } + } + + private void SaveDocumentText(DocumentId id, string fullPath, SourceText newText, Encoding encoding) + { + try + { + using (var writer = new StreamWriter(fullPath, append: false, encoding)) + { + newText.Write(writer); + } + } + catch (IOException exception) + { + OnWorkspaceFailed(new DocumentDiagnostic(WorkspaceDiagnosticKind.Failure, exception.Message, id)); + } + } + } +} diff --git a/src/Workspaces/FolderWorkspace_CSharpProjectLoader.cs b/src/Workspaces/FolderWorkspace_CSharpProjectLoader.cs new file mode 100644 index 0000000000..de19b035f1 --- /dev/null +++ b/src/Workspaces/FolderWorkspace_CSharpProjectLoader.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +namespace Microsoft.CodeAnalysis.Tools.Workspaces +{ + internal sealed partial class FolderWorkspace : Workspace + { + private sealed class CSharpProjectLoader : ProjectLoader + { + public override string Language => LanguageNames.CSharp; + public override string FileExtension => ".cs"; + } + } +} diff --git a/src/Workspaces/FolderWorkspace_FolderSolutionLoader.cs b/src/Workspaces/FolderWorkspace_FolderSolutionLoader.cs new file mode 100644 index 0000000000..db2a54a57b --- /dev/null +++ b/src/Workspaces/FolderWorkspace_FolderSolutionLoader.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.Tools.Workspaces +{ + internal sealed partial class FolderWorkspace : Workspace + { + private static class FolderSolutionLoader + { + private static ImmutableArray ProjectLoaders + => ImmutableArray.Create(new CSharpProjectLoader(), new VisualBasicProjectLoader()); + + public static async Task LoadSolutionInfoAsync(string folderPath, ImmutableHashSet filesToInclude, CancellationToken cancellationToken) + { + var absoluteFolderPath = Path.IsPathFullyQualified(folderPath) + ? folderPath + : Path.GetFullPath(folderPath, Directory.GetCurrentDirectory()); + + var projectInfos = ImmutableArray.CreateBuilder(ProjectLoaders.Length); + + // Create projects for each of the supported languages. + foreach (var loader in ProjectLoaders) + { + var projectInfo = await loader.LoadProjectInfoAsync(folderPath, filesToInclude, cancellationToken); + if (projectInfo is null) + { + continue; + } + + projectInfos.Add(projectInfo); + } + + // Construct workspace from loaded project infos. + return SolutionInfo.Create( + SolutionId.CreateNewId(debugName: absoluteFolderPath), + version: default, + absoluteFolderPath, + projectInfos); + } + } + } +} diff --git a/src/Workspaces/FolderWorkspace_ProjectLoader.cs b/src/Workspaces/FolderWorkspace_ProjectLoader.cs new file mode 100644 index 0000000000..8ed30b1d75 --- /dev/null +++ b/src/Workspaces/FolderWorkspace_ProjectLoader.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.Tools.Workspaces +{ + internal sealed partial class FolderWorkspace : Workspace + { + private abstract class ProjectLoader + { + public abstract string Language { get; } + public abstract string FileExtension { get; } + public virtual string ProjectName => $"{Language}{FileExtension}proj"; + + public virtual async Task LoadProjectInfoAsync(string folderPath, ImmutableHashSet filesToInclude, CancellationToken cancellationToken) + { + var projectId = ProjectId.CreateNewId(debugName: folderPath); + + var documents = await LoadDocumentInfosAsync(projectId, folderPath, FileExtension, filesToInclude); + if (documents.IsDefaultOrEmpty) + { + return null; + } + + return ProjectInfo.Create( + projectId, + version: default, + name: ProjectName, + assemblyName: folderPath, + Language, + filePath: folderPath, + documents: documents); + } + + private static Task> LoadDocumentInfosAsync(ProjectId projectId, string folderPath, string fileExtension, ImmutableHashSet filesToInclude) + { + return Task.Run(() => + { + string[] filePaths; + if (filesToInclude.Count == 0) + { + filePaths = Directory.GetFiles(folderPath, $"*{fileExtension}", SearchOption.AllDirectories); + } + else + { + filePaths = filesToInclude.Where( + filePath => filePath.EndsWith(fileExtension) && File.Exists(filePath)).ToArray(); + } + + if (filePaths.Length == 0) + { + return ImmutableArray.Empty; + } + + var documentInfos = ImmutableArray.CreateBuilder(filePaths.Length); + + foreach (var filePath in filePaths) + { + var documentInfo = DocumentInfo.Create( + DocumentId.CreateNewId(projectId, debugName: filePath), + name: Path.GetFileName(filePath), + loader: new FileTextLoader(filePath, DefaultEncoding), + filePath: filePath); + + documentInfos.Add(documentInfo); + } + + return documentInfos.ToImmutableArray(); + }); + } + } + } +} diff --git a/src/Workspaces/FolderWorkspace_VisualBasicProjectLoader.cs b/src/Workspaces/FolderWorkspace_VisualBasicProjectLoader.cs new file mode 100644 index 0000000000..1f50e45af6 --- /dev/null +++ b/src/Workspaces/FolderWorkspace_VisualBasicProjectLoader.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +namespace Microsoft.CodeAnalysis.Tools.Workspaces +{ + internal sealed partial class FolderWorkspace : Workspace + { + private sealed class VisualBasicProjectLoader : ProjectLoader + { + public override string Language => LanguageNames.VisualBasic; + public override string FileExtension => ".vb"; + } + } +} diff --git a/src/xlf/Resources.cs.xlf b/src/xlf/Resources.cs.xlf index 48fc81ae18..1ec437e41c 100644 --- a/src/xlf/Resources.cs.xlf +++ b/src/xlf/Resources.cs.xlf @@ -17,6 +17,11 @@ {0} obsahuje jak soubor projektu MSBuild, tak soubor řešení. Určete, který soubor chcete použít, pomocí parametru --workspace. + + Cannot specify both folder and workspace options. + Cannot specify both folder and workspace options. + + Complete in {0}ms. Complete in {0}ms. @@ -122,6 +127,11 @@ Soubor {0} zřejmě není platný soubor projektu nebo řešení. + + The folder to operate on. Cannot be used with the `--workspace` option. + The folder to operate on. Cannot be used with the `--workspace` option. + + The project file '{0}' does not exist. Soubor projektu {0} neexistuje. diff --git a/src/xlf/Resources.de.xlf b/src/xlf/Resources.de.xlf index 75716acc73..b356ce8fd4 100644 --- a/src/xlf/Resources.de.xlf +++ b/src/xlf/Resources.de.xlf @@ -17,6 +17,11 @@ In "{0}" wurden eine MSBuild-Projektdatei und eine Projektmappe gefunden. Geben Sie mit der Option "--workspace" an, welche verwendet werden soll. + + Cannot specify both folder and workspace options. + Cannot specify both folder and workspace options. + + Complete in {0}ms. Complete in {0}ms. @@ -122,6 +127,11 @@ Die Datei "{0}" ist weder ein gültiges Projekt noch eine Projektmappendatei. + + The folder to operate on. Cannot be used with the `--workspace` option. + The folder to operate on. Cannot be used with the `--workspace` option. + + The project file '{0}' does not exist. Die Projektdatei "{0}" ist nicht vorhanden. diff --git a/src/xlf/Resources.es.xlf b/src/xlf/Resources.es.xlf index c292b95efb..8b37b5fcd2 100644 --- a/src/xlf/Resources.es.xlf +++ b/src/xlf/Resources.es.xlf @@ -17,6 +17,11 @@ Se encontró un archivo de proyecto y un archivo de solución MSBuild en "{0}". Especifique cuál debe usarse con la opción --workspace. + + Cannot specify both folder and workspace options. + Cannot specify both folder and workspace options. + + Complete in {0}ms. Complete in {0}ms. @@ -122,6 +127,11 @@ El archivo "{0}" no parece ser un proyecto o archivo de solución válido. + + The folder to operate on. Cannot be used with the `--workspace` option. + The folder to operate on. Cannot be used with the `--workspace` option. + + The project file '{0}' does not exist. El archivo de proyecto "{0}" no existe. diff --git a/src/xlf/Resources.fr.xlf b/src/xlf/Resources.fr.xlf index 528b9962ee..5345117728 100644 --- a/src/xlf/Resources.fr.xlf +++ b/src/xlf/Resources.fr.xlf @@ -17,6 +17,11 @@ Un fichier projet et un fichier solution MSBuild ont été trouvés dans '{0}'. Spécifiez celui à utiliser avec l'option --workspace. + + Cannot specify both folder and workspace options. + Cannot specify both folder and workspace options. + + Complete in {0}ms. Complete in {0}ms. @@ -122,6 +127,11 @@ Le fichier '{0}' ne semble pas être un fichier projet ou solution valide. + + The folder to operate on. Cannot be used with the `--workspace` option. + The folder to operate on. Cannot be used with the `--workspace` option. + + The project file '{0}' does not exist. Le fichier projet '{0}' n'existe pas. diff --git a/src/xlf/Resources.it.xlf b/src/xlf/Resources.it.xlf index d5ec868f7c..d472a97f7f 100644 --- a/src/xlf/Resources.it.xlf +++ b/src/xlf/Resources.it.xlf @@ -17,6 +17,11 @@ In '{0}' sono stati trovati sia un file di progetto che un file di soluzione MSBuild. Per specificare quello desiderato, usare l'opzione --workspace. + + Cannot specify both folder and workspace options. + Cannot specify both folder and workspace options. + + Complete in {0}ms. Complete in {0}ms. @@ -122,6 +127,11 @@ Il file '{0}' non sembra essere un file di progetto o di soluzione valido. + + The folder to operate on. Cannot be used with the `--workspace` option. + The folder to operate on. Cannot be used with the `--workspace` option. + + The project file '{0}' does not exist. Il file di progetto '{0}' non esiste. diff --git a/src/xlf/Resources.ja.xlf b/src/xlf/Resources.ja.xlf index 509ee5ad9a..53f5fe83e0 100644 --- a/src/xlf/Resources.ja.xlf +++ b/src/xlf/Resources.ja.xlf @@ -17,6 +17,11 @@ MSBuild プロジェクト ファイルとソリューション ファイルが '{0}' で見つかりました。使用するファイルを --workspace オプションで指定してください。 + + Cannot specify both folder and workspace options. + Cannot specify both folder and workspace options. + + Complete in {0}ms. Complete in {0}ms. @@ -122,6 +127,11 @@ ファイル '{0}' が、有効なプロジェクト ファイルまたはソリューション ファイルではない可能性があります。 + + The folder to operate on. Cannot be used with the `--workspace` option. + The folder to operate on. Cannot be used with the `--workspace` option. + + The project file '{0}' does not exist. プロジェクト ファイル '{0}' が存在しません。 diff --git a/src/xlf/Resources.ko.xlf b/src/xlf/Resources.ko.xlf index 4ba7dc9fad..a7ef729016 100644 --- a/src/xlf/Resources.ko.xlf +++ b/src/xlf/Resources.ko.xlf @@ -17,6 +17,11 @@ '{0}'에 MSBuild 프로젝트 파일 및 솔루션 파일이 모두 있습니다. --workspace 옵션에 사용할 파일을 지정하세요. + + Cannot specify both folder and workspace options. + Cannot specify both folder and workspace options. + + Complete in {0}ms. Complete in {0}ms. @@ -122,6 +127,11 @@ '{0}' 파일은 유효한 프로젝트 또는 솔루션 파일이 아닌 것 같습니다. + + The folder to operate on. Cannot be used with the `--workspace` option. + The folder to operate on. Cannot be used with the `--workspace` option. + + The project file '{0}' does not exist. 프로젝트 파일 '{0}'이(가) 없습니다. diff --git a/src/xlf/Resources.pl.xlf b/src/xlf/Resources.pl.xlf index 653d8745ac..069c24b4fe 100644 --- a/src/xlf/Resources.pl.xlf +++ b/src/xlf/Resources.pl.xlf @@ -17,6 +17,11 @@ W elemencie „{0}” znaleziono zarówno plik rozwiązania, jak i plik projektu MSBuild. Określ plik do użycia za pomocą opcji --workspace. + + Cannot specify both folder and workspace options. + Cannot specify both folder and workspace options. + + Complete in {0}ms. Complete in {0}ms. @@ -122,6 +127,11 @@ Plik „{0}” prawdopodobnie nie jest prawidłowym plikiem projektu lub rozwiązania. + + The folder to operate on. Cannot be used with the `--workspace` option. + The folder to operate on. Cannot be used with the `--workspace` option. + + The project file '{0}' does not exist. Plik projektu „{0}” nie istnieje. diff --git a/src/xlf/Resources.pt-BR.xlf b/src/xlf/Resources.pt-BR.xlf index 47ee54d398..26e4a79595 100644 --- a/src/xlf/Resources.pt-BR.xlf +++ b/src/xlf/Resources.pt-BR.xlf @@ -17,6 +17,11 @@ Foram encontrados um arquivo de solução e um arquivo de projeto MSBuild em '{0}'. Especifique qual usar com a opção --espaço de trabalho. + + Cannot specify both folder and workspace options. + Cannot specify both folder and workspace options. + + Complete in {0}ms. Complete in {0}ms. @@ -122,6 +127,11 @@ O arquivo '{0}' parece não ser um projeto válido ou o arquivo de solução. + + The folder to operate on. Cannot be used with the `--workspace` option. + The folder to operate on. Cannot be used with the `--workspace` option. + + The project file '{0}' does not exist. O arquivo de projeto '{0}' não existe. diff --git a/src/xlf/Resources.ru.xlf b/src/xlf/Resources.ru.xlf index dcf0dfa05a..5192bf1779 100644 --- a/src/xlf/Resources.ru.xlf +++ b/src/xlf/Resources.ru.xlf @@ -17,6 +17,11 @@ В "{0}" обнаружены как файл проекта, так и файл решения MSBuild. Укажите используемый файл с помощью параметра --workspace. + + Cannot specify both folder and workspace options. + Cannot specify both folder and workspace options. + + Complete in {0}ms. Complete in {0}ms. @@ -122,6 +127,11 @@ Файл "{0}" не является допустимым файлом проекта или решения. + + The folder to operate on. Cannot be used with the `--workspace` option. + The folder to operate on. Cannot be used with the `--workspace` option. + + The project file '{0}' does not exist. Файл проекта "{0}" не существует. diff --git a/src/xlf/Resources.tr.xlf b/src/xlf/Resources.tr.xlf index fb42711278..fcf937673d 100644 --- a/src/xlf/Resources.tr.xlf +++ b/src/xlf/Resources.tr.xlf @@ -17,6 +17,11 @@ Hem bir MSBuild proje dosyası ve çözüm dosyası '{0}' içinde bulundu. Hangi--çalışma alanı seçeneği ile kullanmak için belirtin. + + Cannot specify both folder and workspace options. + Cannot specify both folder and workspace options. + + Complete in {0}ms. Complete in {0}ms. @@ -122,6 +127,11 @@ '{0}' dosyası geçerli proje veya çözüm dosyası gibi görünmüyor. + + The folder to operate on. Cannot be used with the `--workspace` option. + The folder to operate on. Cannot be used with the `--workspace` option. + + The project file '{0}' does not exist. Proje dosyası '{0}' yok. diff --git a/src/xlf/Resources.zh-Hans.xlf b/src/xlf/Resources.zh-Hans.xlf index 4f19172e6e..c978ddb51c 100644 --- a/src/xlf/Resources.zh-Hans.xlf +++ b/src/xlf/Resources.zh-Hans.xlf @@ -17,6 +17,11 @@ 在“{0}”中同时找到 MSBuild 项目文件和解决方案文件。请指定要将哪一个文件用于 --workspace 选项。 + + Cannot specify both folder and workspace options. + Cannot specify both folder and workspace options. + + Complete in {0}ms. Complete in {0}ms. @@ -122,6 +127,11 @@ 文件“{0}”似乎不是有效的项目或解决方案文件。 + + The folder to operate on. Cannot be used with the `--workspace` option. + The folder to operate on. Cannot be used with the `--workspace` option. + + The project file '{0}' does not exist. 项目文件“{0}” 不存在。 diff --git a/src/xlf/Resources.zh-Hant.xlf b/src/xlf/Resources.zh-Hant.xlf index 5f60ebdcc6..05476acc16 100644 --- a/src/xlf/Resources.zh-Hant.xlf +++ b/src/xlf/Resources.zh-Hant.xlf @@ -17,6 +17,11 @@ 在 '{0}' 中同時找到 MSBuild 專案檔和解決方案檔。請指定要搭配 --workspace 選項使用的檔案。 + + Cannot specify both folder and workspace options. + Cannot specify both folder and workspace options. + + Complete in {0}ms. Complete in {0}ms. @@ -122,6 +127,11 @@ 檔案 '{0}' 似乎不是有效的專案或解決方案檔。 + + The folder to operate on. Cannot be used with the `--workspace` option. + The folder to operate on. Cannot be used with the `--workspace` option. + + The project file '{0}' does not exist. 專案檔 '{0}' 不存在。 diff --git a/tests/CodeFormatterTests.cs b/tests/CodeFormatterTests.cs index cb4b32962f..fc61b20614 100644 --- a/tests/CodeFormatterTests.cs +++ b/tests/CodeFormatterTests.cs @@ -83,6 +83,31 @@ await TestFormatWorkspaceAsync( expectedFileCount: 5); } + + [Fact] + public async Task FilesFormattedInUnformattedProjectFolder() + { + // Since the code files are beneath the project folder, files are found and formatted. + await TestFormatWorkspaceAsync( + Path.GetDirectoryName(UnformattedProjectFilePath), + EmptyFilesToFormat, + expectedExitCode: 0, + expectedFilesFormatted: 2, + expectedFileCount: 3); + } + + [Fact] + public async Task NoFilesFormattedInUnformattedSolutionFolder() + { + // Since the code files are outside the solution folder, no files are found or formatted. + await TestFormatWorkspaceAsync( + Path.GetDirectoryName(UnformattedSolutionFilePath), + EmptyFilesToFormat, + expectedExitCode: 0, + expectedFilesFormatted: 0, + expectedFileCount: 0); + } + [Fact] public async Task FSharpProjectsDoNotCreateException() { @@ -210,16 +235,28 @@ public async Task FormatLocationsNotLoggedInFormattedProject() Assert.Empty(formatLocations); } - public async Task TestFormatWorkspaceAsync(string solutionOrProjectPath, IEnumerable files, int expectedExitCode, int expectedFilesFormatted, int expectedFileCount) + public async Task TestFormatWorkspaceAsync(string workspaceFilePath, IEnumerable files, int expectedExitCode, int expectedFilesFormatted, int expectedFileCount) { - var workspacePath = Path.GetFullPath(solutionOrProjectPath); - var isSolution = workspacePath.EndsWith(".sln"); + var workspacePath = Path.GetFullPath(workspaceFilePath); + + WorkspaceType workspaceType; + if (Directory.Exists(workspacePath)) + { + workspaceType = WorkspaceType.Folder; + } + else + { + workspaceType = workspacePath.EndsWith(".sln") + ? WorkspaceType.Solution + : WorkspaceType.Project; + } + var filesToFormat = files.Select(Path.GetFullPath).ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); var logger = new TestLogger(); var formatOptions = new FormatOptions( workspacePath, - isSolution, + workspaceType, LogLevel.Trace, saveFormattedFiles: false, changesAreErrors: false, diff --git a/tests/Formatters/AbstractFormatterTests.cs b/tests/Formatters/AbstractFormatterTests.cs index 00c9c0ff2c..436aa720ce 100644 --- a/tests/Formatters/AbstractFormatterTests.cs +++ b/tests/Formatters/AbstractFormatterTests.cs @@ -87,7 +87,7 @@ private protected async Task TestAsync(string testCode, string expec var document = project.Documents.Single(); var formatOptions = new FormatOptions( workspaceFilePath: project.FilePath, - isSolution: false, + workspaceType: WorkspaceType.Folder, logLevel: LogLevel.Trace, saveFormattedFiles: false, changesAreErrors: false, diff --git a/tests/ProgramTests.cs b/tests/ProgramTests.cs index 6ba32f6867..2eb7ea2495 100644 --- a/tests/ProgramTests.cs +++ b/tests/ProgramTests.cs @@ -38,10 +38,10 @@ public void ExitCodeIsSameWithoutCheck() public void FilesFormattedDirectorySeparatorInsensitive() { var filePath = $"other_items{Path.DirectorySeparatorChar}OtherClass.cs"; - var files = Program.GetFilesToFormat(filePath); + var files = Program.GetFilesToFormat(filePath, folder: null); var filePathAlt = $"other_items{Path.AltDirectorySeparatorChar}OtherClass.cs"; - var filesAlt = Program.GetFilesToFormat(filePathAlt); + var filesAlt = Program.GetFilesToFormat(filePathAlt, folder: null); Assert.True(files.IsSubsetOf(filesAlt)); }