internal void ApplyConflictResolutionEdits(IInlineRenameReplacementInfo conflictResolution, LinkedFileMergeSessionResult mergeResult, IEnumerable <Document> documents, CancellationToken cancellationToken) { AssertIsForeground(); if (!AreAllReferenceSpansMappable()) { // don't dynamically update the reference spans for documents with unmappable projections return; } using (new SelectionTracking(this)) { // 1. Undo any previous edits and update the buffer to resulting document after conflict resolution _session.UndoManager.UndoTemporaryEdits(_subjectBuffer, disconnect: false); var newDocument = mergeResult.MergedSolution.GetDocument(documents.First().Id); var originalDocument = _baseDocuments.Single(d => d.Id == newDocument.Id); var changes = GetTextChangesFromTextDifferencingServiceAsync(originalDocument, newDocument, cancellationToken).WaitAndGetResult(cancellationToken); // TODO: why does the following line hang when uncommented? // newDocument.GetTextChangesAsync(this.baseDocuments.Single(d => d.Id == newDocument.Id), cancellationToken).WaitAndGetResult(cancellationToken).Reverse(); _session.UndoManager.CreateConflictResolutionUndoTransaction(_subjectBuffer, () => { using var edit = _subjectBuffer.CreateEdit(EditOptions.DefaultMinimalChange, null, s_propagateSpansEditTag); foreach (var change in changes) { edit.Replace(change.Span.Start, change.Span.Length, change.NewText); } edit.ApplyAndLogExceptions(); }); // 2. We want to update referenceSpanToLinkedRenameSpanMap where spans were affected by conflict resolution. // We also need to add the remaining document edits to conflictResolutionRenameTrackingSpans // so they get classified/tagged correctly in the editor. _conflictResolutionRenameTrackingSpans.Clear(); var documentReplacements = documents .Select(document => (document, conflictResolution.GetReplacements(document.Id).Where(r => GetRenameSpanKind(r.Kind) != RenameSpanKind.None).ToImmutableArray())) .ToImmutableArray(); var firstDocumentReplacements = documentReplacements.FirstOrDefault(d => !d.Item2.IsEmpty); var bufferContainsLinkedDocuments = documentReplacements.Length > 1 && firstDocumentReplacements.document != null; var linkedDocumentsMightConflict = bufferContainsLinkedDocuments; if (linkedDocumentsMightConflict) { // When changes are made and linked documents are involved, some of the linked documents may // have changes that differ from others. When these changes conflict (both differ and overlap), // the inline rename UI reveals the conflicts. However, the merge process for finding these // conflicts is slow, so we want to avoid it when possible. This code block attempts to set // linkedDocumentsMightConflict back to false, eliminating the need to merge the changes as part // of the conflict detection process. Currently we only special case one scenario: ignoring // documents that have no changes at all, we check if all linked documents have exactly the same // set of changes. // 1. Check if all documents have the same replacement spans (or no replacements) var spansMatch = true; foreach (var(document, replacements) in documentReplacements) { if (document == firstDocumentReplacements.document || replacements.IsEmpty) { continue; } if (replacements.Length != firstDocumentReplacements.Item2.Length) { spansMatch = false; break; } for (var i = 0; i < replacements.Length; i++) { if (!replacements[i].Equals(firstDocumentReplacements.Item2[i])) { spansMatch = false; break; } } if (!spansMatch) { break; } } // 2. If spans match, check content if (spansMatch) { linkedDocumentsMightConflict = false; // Only need to check the new span's content var firstDocumentNewText = conflictResolution.NewSolution.GetDocument(firstDocumentReplacements.document.Id).GetTextAsync(cancellationToken).WaitAndGetResult(cancellationToken); var firstDocumentNewSpanText = firstDocumentReplacements.Item2.SelectAsArray(replacement => firstDocumentNewText.ToString(replacement.NewSpan)); foreach (var(document, replacements) in documentReplacements) { if (document == firstDocumentReplacements.document || replacements.IsEmpty) { continue; } var documentNewText = conflictResolution.NewSolution.GetDocument(document.Id).GetTextAsync(cancellationToken).WaitAndGetResult(cancellationToken); for (var i = 0; i < replacements.Length; i++) { if (documentNewText.ToString(replacements[i].NewSpan) != firstDocumentNewSpanText[i]) { // Have to use the slower merge process linkedDocumentsMightConflict = true; break; } } if (linkedDocumentsMightConflict) { break; } } } } foreach (var document in documents) { var relevantReplacements = conflictResolution.GetReplacements(document.Id).Where(r => GetRenameSpanKind(r.Kind) != RenameSpanKind.None); if (!relevantReplacements.Any()) { continue; } var mergedReplacements = linkedDocumentsMightConflict ? GetMergedReplacementInfos( relevantReplacements, conflictResolution.NewSolution.GetDocument(document.Id), mergeResult.MergedSolution.GetDocument(document.Id), cancellationToken) : relevantReplacements; // Show merge conflicts comments as unresolvable conflicts, and do not // show any other rename-related spans that overlap a merge conflict comment. var mergeConflictComments = mergeResult.MergeConflictCommentSpans.ContainsKey(document.Id) ? mergeResult.MergeConflictCommentSpans[document.Id] : SpecializedCollections.EmptyEnumerable <TextSpan>(); foreach (var conflict in mergeConflictComments) { // TODO: Add these to the unresolvable conflict counts in the dashboard _conflictResolutionRenameTrackingSpans.Add(new RenameTrackingSpan( _subjectBuffer.CurrentSnapshot.CreateTrackingSpan(conflict.ToSpan(), SpanTrackingMode.EdgeInclusive, TrackingFidelityMode.Forward), RenameSpanKind.UnresolvedConflict)); } foreach (var replacement in mergedReplacements) { var kind = GetRenameSpanKind(replacement.Kind); if (_referenceSpanToLinkedRenameSpanMap.ContainsKey(replacement.OriginalSpan) && kind != RenameSpanKind.Complexified) { var linkedRenameSpan = _session._renameInfo.GetConflictEditSpan( new InlineRenameLocation(newDocument, replacement.NewSpan), GetWithoutAttributeSuffix(_session.ReplacementText, document.GetLanguageService <LanguageServices.ISyntaxFactsService>().IsCaseSensitive), cancellationToken); if (linkedRenameSpan.HasValue) { if (!mergeConflictComments.Any(s => replacement.NewSpan.IntersectsWith(s))) { _referenceSpanToLinkedRenameSpanMap[replacement.OriginalSpan] = new RenameTrackingSpan( _subjectBuffer.CurrentSnapshot.CreateTrackingSpan( linkedRenameSpan.Value.ToSpan(), SpanTrackingMode.EdgeInclusive, TrackingFidelityMode.Forward), kind); } } else { // We might not have a renameable span if an alias conflict completely changed the text _referenceSpanToLinkedRenameSpanMap[replacement.OriginalSpan] = new RenameTrackingSpan( _referenceSpanToLinkedRenameSpanMap[replacement.OriginalSpan].TrackingSpan, RenameSpanKind.None); if (_activeSpan.HasValue && _activeSpan.Value.IntersectsWith(replacement.OriginalSpan)) { _activeSpan = null; } } } else { if (!mergeConflictComments.Any(s => replacement.NewSpan.IntersectsWith(s))) { _conflictResolutionRenameTrackingSpans.Add(new RenameTrackingSpan( _subjectBuffer.CurrentSnapshot.CreateTrackingSpan(replacement.NewSpan.ToSpan(), SpanTrackingMode.EdgeInclusive, TrackingFidelityMode.Forward), kind)); } } } if (!linkedDocumentsMightConflict) { break; } } UpdateReadOnlyRegions(); // 3. Reset the undo state and notify the taggers. this.ApplyReplacementText(updateSelection: false); RaiseSpansChanged(); } }