提交 abf56b54 编写于 作者: D dpoeschl

Automatic text-based merging of divergent edits made to linked files during...

Automatic text-based merging of divergent edits made to linked files during Workspace.TryApplyChanges.

Changes that are easily merged because they are identical or isolated (a change span that overlaps no other change spans in any linked document) are simply updated to reflect the new text. In the case of isolated changes, - we assume that the change happened in a #if region and it is therefore safe to apply the change.

Once these straightforward changes are applied, the remaining changes which cannot be easily merged because they conflict with another edit are added to the final text in a commented form. If we start with a reference to a class C but a refactoring fully qualifies it as "A.B.C" in one linked document but as "B.C" in another, the following comment is emitted into source:

/* Unmerged change from project 'ProjectName'
Before:
C
After:
B.C
*/
A.B.C

This change also prevents the Preview Changes dialog from showing multiple copies of linked documents. (changeset 1295841)
上级 5099e06b
......@@ -17,6 +17,7 @@ public static class Features
public const string Workspace = "Workspace";
public const string Diagnostics = "Diagnostics";
public const string Formatting = "Formatting";
public const string LinkedFileDiffMerging = "LinkedFileDiffMerging";
}
public const string Environment = "Environment";
......
......@@ -208,6 +208,7 @@
<Compile Include="LanguageServices\CSharpSyntaxVersionService.cs" />
<Compile Include="LanguageServices\CSharpTypeInferenceService.cs" />
<Compile Include="LanguageServices\CSharpTypeInferenceService.TypeInferrer.cs" />
<Compile Include="LinkedFiles\CSharpLinkedFileMergeConflictCommentAdditionService.cs" />
<Compile Include="MSBuild\CSharpProjectFileLoader.cs" />
<Compile Include="MSBuild\CSharpProjectFileLoader.CSharpProjectFile.cs" />
<Compile Include="MSBuild\CSharpProjectFileLoaderFactory.cs" />
......
// Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.CodeAnalysis.Host.Mef;
namespace Microsoft.CodeAnalysis.CSharp
{
[ExportLanguageService(typeof(ILinkedFileMergeConflictCommentAdditionService), LanguageNames.CSharp)]
internal sealed class CSharpLinkedFileMergeConflictCommentAdditionService : AbstractLinkedFileMergeConflictCommentAdditionService
{
internal override string GetConflictCommentText(string header, string beforeString, string afterString)
{
if (beforeString == null && afterString == null)
{
// Whitespace only
return null;
}
else if (beforeString == null)
{
// New code
return string.Format(@"
/* {0}
{1}
{2}
*/
",
header,
WorkspacesResources.AddedHeader,
afterString);
}
else if (afterString == null)
{
// Removed code
return string.Format(@"
/* {0}
{1}
{2}
*/
",
header,
WorkspacesResources.RemovedHeader,
beforeString);
}
else
{
// Changed code
return string.Format(@"
/* {0}
{1}
{2}
{3}
{4}
*/
",
header,
WorkspacesResources.BeforeHeader,
beforeString,
WorkspacesResources.AfterHeader,
afterString);
}
}
}
}
\ No newline at end of file
......@@ -13,5 +13,6 @@ internal interface ITelemetryService : IWorkspaceService
void EndCurrentSession();
void LogRenameSession(RenameSessionInfo renameSession);
void LogEncDebugSession(EncDebuggingSessionInfo session);
void LogLinkedFileDiffMergingSession(LinkedFileDiffMergingSessionInfo session);
}
}
......@@ -91,4 +91,27 @@ internal bool IsEmpty()
return !(HadCompilationErrors || HadRudeEdits || HadValidChanges || HadValidInsignificantChanges);
}
}
internal class LinkedFileDiffMergingSessionInfo
{
public readonly List<LinkedFileGroupSessionInfo> LinkedFileGroups = new List<LinkedFileGroupSessionInfo>();
public void LogLinkedFileResult(LinkedFileGroupSessionInfo info)
{
LinkedFileGroups.Add(info);
}
}
internal class LinkedFileGroupSessionInfo
{
public int LinkedDocuments;
public int DocumentsWithChanges;
public int IsolatedDiffs;
public int IdenticalDiffs;
public int OverlappingDistinctDiffs;
public int OverlappingDistinctDiffsWithSameSpan;
public int OverlappingDistinctDiffsWithSameSpanAndSubstringRelation;
public int InsertedMergeConflictComments;
public int InsertedMergeConflictCommentsAtAdjustedLocation;
}
}
......@@ -2,6 +2,7 @@
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Internal.Log.Telemetry;
namespace Microsoft.CodeAnalysis.Internal.Log
{
......@@ -26,6 +27,10 @@ void ITelemetryService.LogRenameSession(Telemetry.RenameSessionInfo renameSessio
void ITelemetryService.LogEncDebugSession(Telemetry.EncDebuggingSessionInfo session)
{
}
void ITelemetryService.LogLinkedFileDiffMergingSession(LinkedFileDiffMergingSessionInfo session)
{
}
}
}
}
// Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis
{
internal abstract class AbstractLinkedFileMergeConflictCommentAdditionService : ILinkedFileMergeConflictCommentAdditionService
{
internal abstract string GetConflictCommentText(string header, string beforeString, string afterString);
public IEnumerable<TextChange> CreateCommentsForUnmergedChanges(SourceText originalSourceText, IEnumerable<UnmergedDocumentChanges> unmergedChanges)
{
var commentChanges = new List<TextChange>();
foreach (var documentWithChanges in unmergedChanges)
{
var partitionedChanges = PartitionChangesForDocument(documentWithChanges.UnmergedChanges, originalSourceText);
var comments = GetCommentChangesForDocument(partitionedChanges, documentWithChanges.ProjectName, originalSourceText, documentWithChanges.Text);
commentChanges.AddRange(comments);
}
return commentChanges;
}
private IEnumerable<IEnumerable<TextChange>> PartitionChangesForDocument(IEnumerable<TextChange> changes, SourceText originalSourceText)
{
var partitionedChanges = new List<IEnumerable<TextChange>>();
var currentPartition = new List<TextChange>();
currentPartition.Add(changes.First());
var currentPartitionEndLine = originalSourceText.Lines.GetLineFromPosition(changes.First().Span.End);
foreach (var change in changes.Skip(1))
{
// If changes are on adjacent lines, consider them part of the same change.
var changeStartLine = originalSourceText.Lines.GetLineFromPosition(change.Span.Start);
if (changeStartLine.LineNumber >= currentPartitionEndLine.LineNumber + 2)
{
partitionedChanges.Add(currentPartition);
currentPartition = new List<TextChange>();
}
currentPartition.Add(change);
currentPartitionEndLine = originalSourceText.Lines.GetLineFromPosition(change.Span.End);
}
if (currentPartition.Any())
{
partitionedChanges.Add(currentPartition);
}
return partitionedChanges;
}
private List<TextChange> GetCommentChangesForDocument(IEnumerable<IEnumerable<TextChange>> partitionedChanges, string projectName, SourceText oldDocumentText, SourceText newDocumentText)
{
var commentChanges = new List<TextChange>();
foreach (var changePartition in partitionedChanges)
{
var startPosition = changePartition.First().Span.Start;
var endPosition = changePartition.Last().Span.End;
var startLineStartPosition = oldDocumentText.Lines.GetLineFromPosition(startPosition).Start;
var endLineEndPosition = oldDocumentText.Lines.GetLineFromPosition(endPosition).End;
var oldText = oldDocumentText.GetSubText(TextSpan.FromBounds(startLineStartPosition, endLineEndPosition));
var adjustedChanges = changePartition.Select(c => new TextChange(TextSpan.FromBounds(c.Span.Start - startLineStartPosition, c.Span.End - startLineStartPosition), c.NewText));
var newText = oldText.WithChanges(adjustedChanges);
var warningText = GetConflictCommentText(
string.Format(WorkspacesResources.UnmergedChangeFromProject, projectName),
TrimBlankLines(oldText),
TrimBlankLines(newText));
if (warningText != null)
{
commentChanges.Add(new TextChange(TextSpan.FromBounds(startLineStartPosition, startLineStartPosition), warningText));
}
}
return commentChanges;
}
private string TrimBlankLines(SourceText text)
{
int startLine, endLine;
for (startLine = 0; startLine < text.Lines.Count; startLine++)
{
if (text.Lines[startLine].ToString().Any(c => !char.IsWhiteSpace(c)))
{
break;
}
}
for (endLine = text.Lines.Count - 1; endLine > startLine; endLine--)
{
if (text.Lines[endLine].ToString().Any(c => !char.IsWhiteSpace(c)))
{
break;
}
}
return startLine <= endLine
? text.GetSubText(TextSpan.FromBounds(text.Lines[startLine].Start, text.Lines[endLine].End)).ToString()
: null;
}
}
}
// Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis
{
internal interface ILinkedFileMergeConflictCommentAdditionService : ILanguageService
{
IEnumerable<TextChange> CreateCommentsForUnmergedChanges(SourceText originalSourceText, IEnumerable<UnmergedDocumentChanges> unmergedChanges);
}
}
\ No newline at end of file
// Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Internal.Log.Telemetry;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis
{
public partial class Solution
{
private sealed class LinkedFileDiffMergingSession
{
private Solution oldSolution;
private Solution newSolution;
private SolutionChanges solutionChanges;
public LinkedFileDiffMergingSession(Solution oldSolution, Solution newSolution, SolutionChanges solutionChanges)
{
this.oldSolution = oldSolution;
this.newSolution = newSolution;
this.solutionChanges = solutionChanges;
}
internal async Task<Solution> MergeDiffsAsync(CancellationToken cancellationToken)
{
LinkedFileDiffMergingSessionInfo sessionInfo = new LinkedFileDiffMergingSessionInfo();
var linkedDocumentGroupsWithChanges = solutionChanges
.GetProjectChanges()
.SelectMany(p => p.GetChangedDocuments())
.GroupBy(d => oldSolution.GetDocument(d).FilePath, StringComparer.InvariantCultureIgnoreCase);
var updatedSolution = newSolution;
foreach (var linkedDocumentGroup in linkedDocumentGroupsWithChanges)
{
var allLinkedDocuments = newSolution.GetDocumentIdsWithFilePath(newSolution.GetDocumentState(linkedDocumentGroup.First()).FilePath);
if (allLinkedDocuments.Length == 1)
{
continue;
}
SourceText mergedText;
if (linkedDocumentGroup.Count() > 1)
{
mergedText = (await MergeLinkedDocumentGroupAsync(linkedDocumentGroup, sessionInfo, cancellationToken).ConfigureAwait(false)).MergedSourceText;
}
else
{
mergedText = await newSolution.GetDocument(linkedDocumentGroup.Single()).GetTextAsync(cancellationToken).ConfigureAwait(false);
}
foreach (var documentId in allLinkedDocuments)
{
updatedSolution = updatedSolution.WithDocumentText(documentId, mergedText);
}
}
var telemetryService = newSolution.Workspace.Services.GetService<ITelemetryService>();
telemetryService.LogLinkedFileDiffMergingSession(sessionInfo);
return updatedSolution;
}
private async Task<LinkedFileMergeResult> MergeLinkedDocumentGroupAsync(
IEnumerable<DocumentId> linkedDocumentGroup,
LinkedFileDiffMergingSessionInfo sessionInfo,
CancellationToken cancellationToken)
{
var groupSessionInfo = new LinkedFileGroupSessionInfo();
// Automatically merge non-conflicting diffs while collecting the conflicting diffs
var appliedChanges = await newSolution.GetDocument(linkedDocumentGroup.First()).GetTextChangesAsync(oldSolution.GetDocument(linkedDocumentGroup.First())).ConfigureAwait(false);
var unmergedChanges = new List<UnmergedDocumentChanges>();
foreach (var documentId in linkedDocumentGroup.Skip(1))
{
appliedChanges = await AddDocumentMergeChangesAsync(
oldSolution.GetDocument(documentId),
newSolution.GetDocument(documentId),
appliedChanges.ToList(),
unmergedChanges,
groupSessionInfo,
cancellationToken).ConfigureAwait(false);
}
var originalDocument = oldSolution.GetDocument(linkedDocumentGroup.First());
var originalSourceText = await originalDocument.GetTextAsync().ConfigureAwait(false);
// Add comments in source explaining diffs that could not be merged
IEnumerable<TextChange> allChanges;
if (unmergedChanges.Any())
{
var mergeConflictCommentAdder = originalDocument.GetLanguageService<ILinkedFileMergeConflictCommentAdditionService>();
var commentChanges = mergeConflictCommentAdder.CreateCommentsForUnmergedChanges(originalSourceText, unmergedChanges);
allChanges = MergeChangesWithMergeFailComments(appliedChanges, commentChanges, groupSessionInfo);
}
else
{
allChanges = appliedChanges;
}
groupSessionInfo.LinkedDocuments = newSolution.GetDocumentIdsWithFilePath(originalDocument.FilePath).Length;
groupSessionInfo.DocumentsWithChanges = linkedDocumentGroup.Count();
sessionInfo.LogLinkedFileResult(groupSessionInfo);
return new LinkedFileMergeResult(originalSourceText.WithChanges(allChanges), hasMergeConflicts: unmergedChanges.Any());
}
private static async Task<List<TextChange>> AddDocumentMergeChangesAsync(
Document oldDocument,
Document newDocument,
List<TextChange> cumulativeChanges,
List<UnmergedDocumentChanges> unmergedChanges,
LinkedFileGroupSessionInfo groupSessionInfo,
CancellationToken cancellationToken)
{
var unmergedDocumentChanges = new List<TextChange>();
var successfullyMergedChanges = new List<TextChange>();
int cumulativeChangeIndex = 0;
foreach (var change in await newDocument.GetTextChangesAsync(oldDocument).ConfigureAwait(false))
{
while (cumulativeChangeIndex < cumulativeChanges.Count && cumulativeChanges[cumulativeChangeIndex].Span.End < change.Span.Start)
{
// Existing change that does not overlap with the current change in consideration
successfullyMergedChanges.Add(cumulativeChanges[cumulativeChangeIndex]);
cumulativeChangeIndex++;
groupSessionInfo.IsolatedDiffs++;
}
if (cumulativeChangeIndex < cumulativeChanges.Count)
{
var cumulativeChange = cumulativeChanges[cumulativeChangeIndex];
if (!cumulativeChange.Span.IntersectsWith(change.Span))
{
// The current change in consideration does not intersect with any existing change
successfullyMergedChanges.Add(change);
groupSessionInfo.IsolatedDiffs++;
}
else
{
if (change.Span != cumulativeChange.Span || change.NewText != cumulativeChange.NewText)
{
// The current change in consideration overlaps an existing change but
// the changes are not identical.
unmergedDocumentChanges.Add(change);
groupSessionInfo.OverlappingDistinctDiffs++;
if (change.Span == cumulativeChange.Span)
{
groupSessionInfo.OverlappingDistinctDiffsWithSameSpan++;
if (change.NewText.Contains(cumulativeChange.NewText) || cumulativeChange.NewText.Contains(change.NewText))
{
groupSessionInfo.OverlappingDistinctDiffsWithSameSpanAndSubstringRelation++;
}
}
}
else
{
// The current change in consideration is identical to an existing change
successfullyMergedChanges.Add(change);
cumulativeChangeIndex++;
groupSessionInfo.IdenticalDiffs++;
}
}
}
else
{
// The current change in consideration does not intersect with any existing change
successfullyMergedChanges.Add(change);
groupSessionInfo.IsolatedDiffs++;
}
}
while (cumulativeChangeIndex < cumulativeChanges.Count)
{
// Existing change that does not overlap with the current change in consideration
successfullyMergedChanges.Add(cumulativeChanges[cumulativeChangeIndex]);
cumulativeChangeIndex++;
groupSessionInfo.IsolatedDiffs++;
}
if (unmergedDocumentChanges.Any())
{
unmergedChanges.Add(new UnmergedDocumentChanges(
unmergedDocumentChanges.AsEnumerable(),
await oldDocument.GetTextAsync(cancellationToken).ConfigureAwait(false),
oldDocument.Project.Name));
}
return successfullyMergedChanges;
}
private IEnumerable<TextChange> MergeChangesWithMergeFailComments(IEnumerable<TextChange> mergedChanges, IEnumerable<TextChange> commentChanges, LinkedFileGroupSessionInfo groupSessionInfo)
{
var mergedChangesList = NormalizeChanges(mergedChanges).ToList();
var commentChangesList = NormalizeChanges(commentChanges).ToList();
var combinedChanges = new List<TextChange>();
var insertedMergeConflictCommentsAtAdjustedLocation = 0;
var commentChangeIndex = 0;
foreach (var mergedChange in mergedChangesList)
{
while (commentChangeIndex < commentChangesList.Count && commentChangesList[commentChangeIndex].Span.End <= mergedChange.Span.Start)
{
// Add a comment change that does not conflict with any merge change
combinedChanges.Add(commentChangesList[commentChangeIndex]);
commentChangeIndex++;
}
if (commentChangeIndex >= commentChangesList.Count || mergedChange.Span.End <= commentChangesList[commentChangeIndex].Span.Start)
{
// Add a merge change that does not conflict with any comment change
combinedChanges.Add(mergedChange);
continue;
}
// The current comment insertion location conflicts with a merge diff location. Add the comment before the diff.
var conflictingCommentInsertionLocation = new TextSpan(mergedChange.Span.Start, 0);
while (commentChangeIndex < commentChangesList.Count && commentChangesList[commentChangeIndex].Span.Start < mergedChange.Span.End)
{
combinedChanges.Add(new TextChange(conflictingCommentInsertionLocation, commentChangesList[commentChangeIndex].NewText));
commentChangeIndex++;
insertedMergeConflictCommentsAtAdjustedLocation++;
}
combinedChanges.Add(mergedChange);
}
while (commentChangeIndex < commentChangesList.Count)
{
// Add a comment change that does not conflict with any merge change
combinedChanges.Add(commentChangesList[commentChangeIndex]);
commentChangeIndex++;
}
groupSessionInfo.InsertedMergeConflictComments = commentChanges.Count();
groupSessionInfo.InsertedMergeConflictCommentsAtAdjustedLocation = insertedMergeConflictCommentsAtAdjustedLocation;
return NormalizeChanges(combinedChanges);
}
private IEnumerable<TextChange> NormalizeChanges(IEnumerable<TextChange> changes)
{
if (changes.Count() <= 1)
{
return changes;
}
changes = changes.OrderBy(c => c.Span.Start);
var normalizedChanges = new List<TextChange>();
var currentChange = changes.First();
foreach (var nextChange in changes.Skip(1))
{
if (nextChange.Span.Start == currentChange.Span.End)
{
currentChange = new TextChange(TextSpan.FromBounds(currentChange.Span.Start, nextChange.Span.End), currentChange.NewText + nextChange.NewText);
}
else
{
normalizedChanges.Add(currentChange);
currentChange = nextChange;
}
}
normalizedChanges.Add(currentChange);
return normalizedChanges;
}
}
}
}
\ No newline at end of file
// Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis
{
public partial class Solution
{
private sealed class LinkedFileMergeResult
{
public SourceText MergedSourceText { get; internal set; }
public bool HasMergeConflicts { get; private set; }
public LinkedFileMergeResult(SourceText mergedSourceText, bool hasMergeConflicts)
{
MergedSourceText = mergedSourceText;
HasMergeConflicts = hasMergeConflicts;
}
}
}
}
\ No newline at end of file
// Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
......@@ -11,8 +10,6 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.LanguageServices;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Collections.Immutable;
using Roslyn.Utilities;
......@@ -1282,6 +1279,12 @@ public Solution WithDocumentText(DocumentId documentId, SourceText text, Preserv
return newSolution;
}
internal async Task<Solution> WithMergedLinkedFileChangesAsync(Solution oldSolution, SolutionChanges? solutionChanges = null, CancellationToken cancellationToken = default(CancellationToken))
{
var session = new LinkedFileDiffMergingSession(oldSolution, this, solutionChanges ?? this.GetChanges(oldSolution));
return await session.MergeDiffsAsync(cancellationToken).ConfigureAwait(false);
}
private SolutionBranch firstBranch;
private class SolutionBranch
......
// Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis
{
internal sealed class UnmergedDocumentChanges
{
public IEnumerable<TextChange> UnmergedChanges { get; private set; }
public SourceText Text { get; private set; }
public string ProjectName { get; private set; }
public UnmergedDocumentChanges(IEnumerable<TextChange> unmergedChanges, SourceText text, string projectName)
{
UnmergedChanges = unmergedChanges;
Text = text;
ProjectName = projectName;
}
}
}
......@@ -774,6 +774,9 @@ public virtual bool TryApplyChanges(Solution newSolution)
throw new NotSupportedException(WorkspacesResources.AddingProjectsNotSupported);
}
var solutionWithLinkedFileChangesMerged = newSolution.WithMergedLinkedFileChangesAsync(oldSolution, solutionChanges, CancellationToken.None).Result;
solutionChanges = solutionWithLinkedFileChangesMerged.GetChanges(oldSolution);
// process all project changes
foreach (var projectChanges in solutionChanges.GetProjectChanges())
{
......
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -->
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\packages\Microsoft.Net.ToolsetCompilers.0.7.4070101-beta\build\Microsoft.Net.ToolsetCompilers.props" Condition="Exists('..\..\packages\Microsoft.Net.ToolsetCompilers.0.7.4070101-beta\build\Microsoft.Net.ToolsetCompilers.props')" />
<ImportGroup Label="Settings">
<Import Project="..\..\Tools\Microsoft.CodeAnalysis.Toolset.Open\Targets\VSL.Settings.targets" />
<Import Project="..\..\packages\Microsoft.Net.ToolsetCompilers.0.7.4070101-beta\build\Microsoft.Net.ToolsetCompilers.props" Condition="Exists('..\..\packages\Microsoft.Net.ToolsetCompilers.0.7.4070101-beta\build\Microsoft.Net.ToolsetCompilers.props')" />
</ImportGroup>
<PropertyGroup>
<Configuration Condition="'$(Configuration)' == ''">Debug</Configuration>
......@@ -853,6 +853,7 @@
<Compile Include="Workspace\MSBuild\SolutionFile\ProjectBlock.cs" />
<Compile Include="Workspace\MSBuild\SolutionFile\SectionBlock.cs" />
<Compile Include="Workspace\MSBuild\SolutionFile\SolutionFile.cs" />
<Compile Include="Workspace\Solution\AbstractLinkedFileMergeConflictCommentAdditionService.cs" />
<Compile Include="Workspace\Solution\BranchId.cs" />
<Compile Include="Workspace\Solution\Document.cs">
<ExcludeFromStyleCop>true</ExcludeFromStyleCop>
......@@ -863,6 +864,7 @@
<Compile Include="Workspace\Solution\DocumentState.cs" />
<Compile Include="Workspace\Solution\DocumentState.EquivalenceResult.cs" />
<Compile Include="Workspace\Solution\DocumentState_SimpleProperties.cs" />
<Compile Include="Workspace\Solution\ILinkedFileMergeConflictCommentAdditionService.cs" />
<Compile Include="Workspace\Solution\MetadataOnlyImage.cs" />
<Compile Include="Workspace\Solution\MetadataOnlyReference.cs" />
<Compile Include="Workspace\Solution\PreservationMode.cs" />
......@@ -886,6 +888,8 @@
<Compile Include="Workspace\Solution\Solution.cs">
<ExcludeFromStyleCop>true</ExcludeFromStyleCop>
</Compile>
<Compile Include="Workspace\Solution\Solution.LinkedFileDiffMergingSession.cs" />
<Compile Include="Workspace\Solution\Solution.LinkedFileMergeResult.cs" />
<Compile Include="Workspace\Solution\SolutionChanges.cs" />
<Compile Include="Workspace\Solution\SolutionId.cs" />
<Compile Include="Workspace\Solution\SolutionInfo.cs" />
......@@ -894,6 +898,7 @@
<Compile Include="Workspace\Solution\TextAndVersion.cs" />
<Compile Include="Workspace\Solution\TextLoader.cs" />
<Compile Include="Workspace\Solution\TreeAndVersion.cs" />
<Compile Include="Workspace\Solution\UnmergedDocumentChanges.cs" />
<Compile Include="Workspace\Solution\VersionStamp.cs" />
<Compile Include="Workspace\TextExtensions.cs" />
<Compile Include="Workspace\Workspace.cs" />
......@@ -948,9 +953,6 @@
<ItemGroup>
<Folder Include="Workspace\DocumentationComments\" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ImportGroup Label="Targets">
<Import Project="..\..\Tools\Microsoft.CodeAnalysis.Toolset.Open\Targets\VSL.Imports.targets" />
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
......@@ -961,6 +963,5 @@
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('$(SolutionDir)\.nuget\NuGet.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)\.nuget\NuGet.targets'))" />
<Error Condition="!Exists('..\..\packages\Microsoft.Net.ToolsetCompilers.0.7.4070101-beta\build\Microsoft.Net.ToolsetCompilers.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Net.ToolsetCompilers.0.7.4070101-beta\build\Microsoft.Net.ToolsetCompilers.props'))" />
</Target>
</Project>
\ No newline at end of file
......@@ -69,6 +69,15 @@ internal class WorkspacesResources {
}
}
/// <summary>
/// Looks up a localized string similar to Added:.
/// </summary>
internal static string AddedHeader {
get {
return ResourceManager.GetString("AddedHeader", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Adding projects not supported in ApplyChanges..
/// </summary>
......@@ -78,6 +87,15 @@ internal class WorkspacesResources {
}
}
/// <summary>
/// Looks up a localized string similar to After:.
/// </summary>
internal static string AfterHeader {
get {
return ResourceManager.GetString("AfterHeader", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} is already present..
/// </summary>
......@@ -105,6 +123,15 @@ internal class WorkspacesResources {
}
}
/// <summary>
/// Looks up a localized string similar to Before:.
/// </summary>
internal static string BeforeHeader {
get {
return ResourceManager.GetString("BeforeHeader", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Cannot generate code for unsupported operator &apos;{0}&apos;.
/// </summary>
......@@ -609,6 +636,15 @@ internal class WorkspacesResources {
}
}
/// <summary>
/// Looks up a localized string similar to Removed:.
/// </summary>
internal static string RemovedHeader {
get {
return ResourceManager.GetString("RemovedHeader", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Remove Unnecessary Imports/Usings..
/// </summary>
......@@ -672,6 +708,15 @@ internal class WorkspacesResources {
}
}
/// <summary>
/// Looks up a localized string similar to Unmerged change from project &apos;{0}&apos;.
/// </summary>
internal static string UnmergedChangeFromProject {
get {
return ResourceManager.GetString("UnmergedChangeFromProject", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unrecognized language name..
/// </summary>
......
......@@ -333,4 +333,19 @@
<data name="SolutionFileNotFound" xml:space="preserve">
<value>Solution file not found: '{0}'</value>
</data>
<data name="UnmergedChangeFromProject" xml:space="preserve">
<value>Unmerged change from project '{0}'</value>
</data>
<data name="AddedHeader" xml:space="preserve">
<value>Added:</value>
</data>
<data name="AfterHeader" xml:space="preserve">
<value>After:</value>
</data>
<data name="BeforeHeader" xml:space="preserve">
<value>Before:</value>
</data>
<data name="RemovedHeader" xml:space="preserve">
<value>Removed:</value>
</data>
</root>
\ No newline at end of file
// Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.CodeAnalysis.Test.Utilities;
using Xunit;
namespace Microsoft.CodeAnalysis.UnitTests.LinkedFileDiffMerging
{
public partial class LinkedFileDiffMergingTests
{
[Fact]
[Trait(Traits.Feature, Traits.Features.LinkedFileDiffMerging)]
public void TestChangeSignature()
{
TestLinkedFileSet(
@"public class Class1
{
void M(int x, string y, int z)
{
}
#if LIB1
void N()
{
M(2, ""A"", 1);
}
#elif LIB2
void N()
{
M(4, ""B"", 3);
}
#endif
}",
new List<string>
{
@"public class Class1
{
void M(int z, int x)
{
}
#if LIB1
void N()
{
M(1, 2);
}
#elif LIB2
void N()
{
M(4, ""B"", 3);
}
#endif
}",
@"public class Class1
{
void M(int z, int x)
{
}
#if LIB1
void N()
{
M(2, ""A"", 1);
}
#elif LIB2
void N()
{
M(3, 4);
}
#endif
}"
},
@"public class Class1
{
void M(int z, int x)
{
}
#if LIB1
void N()
{
M(1, 2);
}
#elif LIB2
void N()
{
M(3, 4);
}
#endif
}",
LanguageNames.CSharp);
}
[Fact]
[Trait(Traits.Feature, Traits.Features.LinkedFileDiffMerging)]
public void TestRename()
{
TestLinkedFileSet(
@"public class Class1
{
void M()
{
}
#if LIB1
void N()
{
M();
}
#elif LIB2
void N()
{
M();
}
#endif
}",
new List<string>
{
@"public class Class1
{
void Method()
{
}
#if LIB1
void N()
{
Method();
}
#elif LIB2
void N()
{
M();
}
#endif
}",
@"public class Class1
{
void Method()
{
}
#if LIB1
void N()
{
M();
}
#elif LIB2
void N()
{
Method();
}
#endif
}"
},
@"public class Class1
{
void Method()
{
}
#if LIB1
void N()
{
Method();
}
#elif LIB2
void N()
{
Method();
}
#endif
}",
LanguageNames.CSharp);
}
}
}
// Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.CodeAnalysis.Test.Utilities;
using Xunit;
namespace Microsoft.CodeAnalysis.UnitTests.LinkedFileDiffMerging
{
public partial class LinkedFileDiffMergingTests
{
[Fact]
[Trait(Traits.Feature, Traits.Features.LinkedFileDiffMerging)]
public void TestIdenticalChanges()
{
TestLinkedFileSet(
"x",
new List<string> { "y", "y" },
@"y",
LanguageNames.CSharp);
}
[Fact]
[Trait(Traits.Feature, Traits.Features.LinkedFileDiffMerging)]
public void TestChangesInOnlyOneFile()
{
TestLinkedFileSet(
"a b c d e",
new List<string> { "a b c d e", "a z c z e" },
@"a z c z e",
LanguageNames.CSharp);
}
[Fact]
[Trait(Traits.Feature, Traits.Features.LinkedFileDiffMerging)]
public void TestIsolatedChangesInBothFiles()
{
TestLinkedFileSet(
"a b c d e",
new List<string> { "a z c d e", "a b c z e" },
@"a z c z e",
LanguageNames.CSharp);
}
[Fact]
[Trait(Traits.Feature, Traits.Features.LinkedFileDiffMerging)]
public void TestIdenticalEditAfterIsolatedChanges()
{
TestLinkedFileSet(
"a b c d e",
new List<string> { "a zzz c xx e", "a b c xx e" },
@"a zzz c xx e",
LanguageNames.CSharp);
}
[Fact]
[Trait(Traits.Feature, Traits.Features.LinkedFileDiffMerging)]
public void TestOneConflict()
{
TestLinkedFileSet(
"a b c d e",
new List<string> { "a b y d e", "a b z d e" },
@"
/* Unmerged change from project 'ProjectName1'
Before:
a b c d e
After:
a b z d e
*/
a b y d e",
LanguageNames.CSharp);
}
[Fact]
[Trait(Traits.Feature, Traits.Features.LinkedFileDiffMerging)]
public void TestTwoConflictsOnSameLine()
{
TestLinkedFileSet(
"a b c d e",
new List<string> { "a q1 c z1 e", "a q2 c z2 e" },
@"
/* Unmerged change from project 'ProjectName1'
Before:
a b c d e
After:
a q2 c z2 e
*/
a q1 c z1 e",
LanguageNames.CSharp);
}
[Fact]
[Trait(Traits.Feature, Traits.Features.LinkedFileDiffMerging)]
public void TestTwoConflictsOnAdjacentLines()
{
TestLinkedFileSet(
@"One
Two
Three
Four",
new List<string>
{
@"One
TwoY
ThreeY
Four",
@"One
TwoZ
ThreeZ
Four"
},
@"One
/* Unmerged change from project 'ProjectName1'
Before:
Two
Three
After:
TwoZ
ThreeZ
*/
TwoY
ThreeY
Four",
LanguageNames.CSharp);
}
[Fact]
[Trait(Traits.Feature, Traits.Features.LinkedFileDiffMerging)]
public void TestTwoConflictsOnSeparatedLines()
{
TestLinkedFileSet(
@"One
Two
Three
Four
Five",
new List<string>
{
@"One
TwoY
Three
FourY
Five",
@"One
TwoZ
Three
FourZ
Five"
},
@"One
/* Unmerged change from project 'ProjectName1'
Before:
Two
After:
TwoZ
*/
TwoY
Three
/* Unmerged change from project 'ProjectName1'
Before:
Four
After:
FourZ
*/
FourY
Five",
LanguageNames.CSharp);
}
[Fact]
[Trait(Traits.Feature, Traits.Features.LinkedFileDiffMerging)]
public void TestManyLinkedFilesWithOverlappingChange()
{
TestLinkedFileSet(
@"A",
new List<string>
{
@"A",
@"B",
@"C",
@"",
},
@"
/* Unmerged change from project 'ProjectName2'
Before:
A
After:
C
*/
/* Unmerged change from project 'ProjectName3'
Removed:
A
*/
B",
LanguageNames.CSharp);
}
[Fact]
[Trait(Traits.Feature, Traits.Features.LinkedFileDiffMerging)]
public void TestCommentsAddedCodeCSharp()
{
TestLinkedFileSet(
@"",
new List<string>
{
@"A",
@"B",
},
@"
/* Unmerged change from project 'ProjectName1'
Added:
B
*/
A",
LanguageNames.CSharp);
}
[Fact]
[Trait(Traits.Feature, Traits.Features.LinkedFileDiffMerging)]
public void TestCommentsAddedCodeVB()
{
TestLinkedFileSet(
@"",
new List<string>
{
@"A",
@"B",
},
@"
' Unmerged change from project 'ProjectName1'
' Added:
' B
A",
LanguageNames.VisualBasic);
}
[Fact]
[Trait(Traits.Feature, Traits.Features.LinkedFileDiffMerging)]
public void TestCommentsRemovedCodeCSharp()
{
TestLinkedFileSet(
@"A",
new List<string>
{
@"B",
@"",
},
@"
/* Unmerged change from project 'ProjectName1'
Removed:
A
*/
B",
LanguageNames.CSharp);
}
[Fact]
[Trait(Traits.Feature, Traits.Features.LinkedFileDiffMerging)]
public void TestCommentsRemovedCodeVB()
{
TestLinkedFileSet(
@"A",
new List<string>
{
@"B",
@"",
},
@"
' Unmerged change from project 'ProjectName1'
' Removed:
' A
B",
LanguageNames.VisualBasic);
}
}
}
// Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.CodeAnalysis.Text;
using Xunit;
namespace Microsoft.CodeAnalysis.UnitTests.LinkedFileDiffMerging
{
public partial class LinkedFileDiffMergingTests
{
public void TestLinkedFileSet(string startText, List<string> updatedTexts, string expectedMergedText, string languageName)
{
using (var workspace = new CustomWorkspace())
{
var solution = new CustomWorkspace().CurrentSolution;
var startSourceText = SourceText.From(startText);
var documentIds = new List<DocumentId>();
for (int i = 0; i < updatedTexts.Count; i++)
{
var projectId = ProjectId.CreateNewId();
var documentId = DocumentId.CreateNewId(projectId);
documentIds.Add(documentId);
var projectInfo = ProjectInfo.Create(projectId, VersionStamp.Create(), "ProjectName" + i, "AssemblyName" + i, languageName);
solution = solution
.AddProject(projectInfo)
.AddDocument(documentId, "DocumentName", startSourceText, filePath: "FilePath");
}
var startingSolution = solution;
var updatedSolution = solution;
for (int i = 0; i < updatedTexts.Count; i++)
{
var text = updatedTexts[i];
if (text != startText)
{
updatedSolution = updatedSolution
.WithDocumentText(documentIds[i], SourceText.From(text));
}
}
var mergedSolution = updatedSolution.WithMergedLinkedFileChangesAsync(startingSolution).Result;
for (int i = 0; i < updatedTexts.Count; i++)
{
Assert.Equal(expectedMergedText, mergedSolution.GetDocument(documentIds[i]).GetTextAsync().Result.ToString());
}
}
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -->
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\packages\Microsoft.Net.ToolsetCompilers.0.7.4070101-beta\build\Microsoft.Net.ToolsetCompilers.props" Condition="Exists('..\..\packages\Microsoft.Net.ToolsetCompilers.0.7.4070101-beta\build\Microsoft.Net.ToolsetCompilers.props')" />
<ImportGroup Label="Settings">
<Import Project="..\..\Tools\Microsoft.CodeAnalysis.Toolset.Open\Targets\VSL.Settings.targets" />
<Import Project="..\..\packages\Microsoft.Net.ToolsetCompilers.0.7.4070101-beta\build\Microsoft.Net.ToolsetCompilers.props" Condition="Exists('..\..\packages\Microsoft.Net.ToolsetCompilers.0.7.4070101-beta\build\Microsoft.Net.ToolsetCompilers.props')" />
</ImportGroup>
<PropertyGroup>
<Nonshipping>true</Nonshipping>
......@@ -83,12 +83,13 @@
<HintPath>..\..\packages\xunit.core.2.0.0-alpha-build2576\lib\net45\xunit2.dll</HintPath>
</Reference>
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "></PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "></PropertyGroup>
<ItemGroup>
<Compile Include="AssemblyAttributes.cs" />
<Compile Include="LinkedFileDiffMerging\LinkedFileDiffMergingTests.TextMerging.cs" />
<Compile Include="LinkedFileDiffMerging\LinkedFileDiffMergingTests.Features.cs" />
<Compile Include="LinkedFileDiffMerging\LinkedFileDiffMergingTests.cs" />
<Compile Include="UtilityTest\AsyncLazyTests.cs" />
<Compile Include="UtilityTest\AsyncLazyTests.StopTheThreadPoolContext.cs" />
<Compile Include="CodeCleanup\AddMissingTokensTests.cs" />
......@@ -266,9 +267,6 @@
<ItemGroup>
<EmbeddedResource Include="TestFiles\VisualBasicProject_VisualBasicProject_UnknownProjectExtension.vbproj" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ImportGroup Label="Targets">
<Import Project="..\..\Tools\Microsoft.CodeAnalysis.Toolset.Open\Targets\VSL.Imports.targets" />
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
......@@ -279,6 +277,5 @@
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('$(SolutionDir)\.nuget\NuGet.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)\.nuget\NuGet.targets'))" />
<Error Condition="!Exists('..\..\packages\Microsoft.Net.ToolsetCompilers.0.7.4070101-beta\build\Microsoft.Net.ToolsetCompilers.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Net.ToolsetCompilers.0.7.4070101-beta\build\Microsoft.Net.ToolsetCompilers.props'))" />
</Target>
</Project>
\ No newline at end of file
......@@ -205,6 +205,7 @@
<Compile Include="LanguageServices\VisualBasicSyntaxVersionService.vb" />
<Compile Include="LanguageServices\VisualBasicTypeInferenceService.TypeInferrer.vb" />
<Compile Include="LanguageServices\VisualBasicTypeInferenceService.vb" />
<Compile Include="LinkedFiles\BasicLinkedFileMergeConflictCommentAdditionService.vb" />
<Compile Include="MSBuild\VisualBasicProjectFileLoader.vb" />
<Compile Include="MSBuild\VisualBasicProjectFileLoaderFactory.vb" />
<Compile Include="Recommendations\VisualBasicRecommendationService.vb" />
......
' Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
Imports System.Text
Imports System.Text.RegularExpressions
Imports Microsoft.CodeAnalysis.Host.Mef
Namespace Microsoft.CodeAnalysis.VisualBasic
<ExportLanguageService(GetType(ILinkedFileMergeConflictCommentAdditionService), LanguageNames.VisualBasic)>
Class BasicLinkedFileMergeConflictCommentAdditionService
Inherits AbstractLinkedFileMergeConflictCommentAdditionService
Friend Overrides Function GetConflictCommentText(header As String, beforeString As String, afterString As String) As String
If beforeString Is Nothing AndAlso afterString Is Nothing Then
Return Nothing
ElseIf beforeString Is Nothing Then
' Added code
Return String.Format("
' {0}
' {1}
{2}
",
header,
WorkspacesResources.AddedHeader,
GetCommentedText(afterString))
ElseIf afterString Is Nothing Then
' Removed code
Return String.Format("
' {0}
' {1}
{2}
",
header,
WorkspacesResources.RemovedHeader,
GetCommentedText(beforeString))
Else
Return String.Format("
' {0}
' {1}
{2}
' {3}
{4}
",
header,
WorkspacesResources.BeforeHeader,
GetCommentedText(beforeString),
WorkspacesResources.AfterHeader,
GetCommentedText(afterString))
End If
End Function
Private Function GetCommentedText(text As String) As String
Dim lines = Regex.Split(text, "\r\n|\r|\n")
If Not lines.Any() Then
Return text
End If
Dim newlines = Regex.Matches(text, "\r\n|\r|\n")
Contract.Assert(newlines.Count = lines.Count - 1)
Dim builder = New StringBuilder()
For i = 0 To lines.Count() - 2
builder.Append(String.Format("' {0}{1}", lines(i), newlines(i)))
Next
builder.Append(String.Format("' {0}", lines.Last()))
Return builder.ToString()
End Function
End Class
End Namespace
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册