// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. // // This file contain implementations details that are subject to change without notice. // Use at your own risk. // namespace Microsoft.VisualStudio.Text.Projection.Implementation { // todo: denormalizations that are performed may possibly be removed by exploiting OldPosition // information in TextChange. Investigate this. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Text; using Microsoft.VisualStudio.Utilities; using Microsoft.VisualStudio.Text.Implementation; using Microsoft.VisualStudio.Text.Utilities; using Microsoft.VisualStudio.Text.Differencing; using Strings = Microsoft.VisualStudio.Text.Implementation.Strings; internal sealed partial class ProjectionBuffer : BaseProjectionBuffer, IProjectionBuffer { #region ProjectionEdit Class private class ProjectionEdit : Edit, ISubordinateTextEdit { private ProjectionBuffer projectionBuffer; private bool subordinate; public ProjectionEdit(ProjectionBuffer projectionBuffer, ITextSnapshot originSnapshot, EditOptions options, int? reiteratedVersionNumber, object editTag) : base(projectionBuffer, originSnapshot, options, reiteratedVersionNumber, editTag) { this.projectionBuffer = projectionBuffer; this.subordinate = true; } public ITextBuffer TextBuffer { get { return this.projectionBuffer; } } // this is the master edit path -- initiated from outside protected override ITextSnapshot PerformApply() { CheckActive(); this.applied = true; this.subordinate = false; ITextSnapshot result = this.baseBuffer.currentSnapshot; if (this.changes.Count > 0) { this.projectionBuffer.group.PerformMasterEdit(this.projectionBuffer, this, this.options, this.editTag); if (!this.Canceled) { result = this.baseBuffer.currentSnapshot; } } else { // vacuous edit this.baseBuffer.editInProgress = false; } return result; } public void PreApply() { // called for all non-vacuous edits if (this.changes.Count > 0) { this.projectionBuffer.ComputeSourceEdits(this.changes); } } public void FinalApply() // TODO: make FinalApply return event raisers, eliminate FinishEdit() { // called for all non-vacuous edits if (this.changes.Count > 0 || this.projectionBuffer.pendingContentChangedEventArgs.Count > 0) { this.projectionBuffer.group.CancelIndependentEdit(this.projectionBuffer); // just in case IList eventRaisers = this.projectionBuffer.InterpretSourceChanges(this.options, /*this.reiteratedVersionNumber,*/ this.editTag); this.projectionBuffer.group.EnqueueEvents(eventRaisers, this.baseBuffer); // raise immediate events foreach (var raiser in eventRaisers) { raiser.RaiseEvent(this.baseBuffer, true); } } this.projectionBuffer.editInProgress = false; if (this.subordinate) { this.baseBuffer.group.FinishEdit(); } } public override void CancelApplication() { if (!this.canceled) { base.CancelApplication(); this.projectionBuffer.editApplicationInProgress = false; this.projectionBuffer.pendingContentChangedEventArgs.Clear(); } } } #endregion #region SourceBufferSet Class /// /// Tracks the Source TextBuffers of this ProjectionBuffer. There is exactly one SourceBufferSet /// per ProjectionBufferImpl. /// private class SourceBufferSet { private class BufferTracker { public ITextBuffer _buffer; public int _spanCount; public BufferTracker(ITextBuffer buffer) { _buffer = buffer; _spanCount = 1; } } // presumption: the number of source buffers is small private bool _inTransaction; private List _sourceBufferTrackers = new List(); private FrugalList _addedBuffers; private FrugalList _removedBuffers; private int Find(ITextBuffer buffer) { for (int i = 0; i < _sourceBufferTrackers.Count; ++i) { if (buffer == _sourceBufferTrackers[i]._buffer) { return i; } } return -1; } public List SourceBuffers { get { List sourceBuffers = new List(); foreach (BufferTracker bt in _sourceBufferTrackers) { if (!sourceBuffers.Contains(bt._buffer)) { sourceBuffers.Add(bt._buffer); } } return sourceBuffers; } } public void StartTransaction() { Debug.Assert(!_inTransaction); _addedBuffers = new FrugalList(); _removedBuffers = new FrugalList(); _inTransaction = true; } public void FinishTransaction(out FrugalList addedBuffers, out FrugalList removedBuffers) { Debug.Assert(_inTransaction); // If a buffer was removed and then added, eliminate it from both lists. // Since these lists should be extremely short, nothing fancy here. FrugalList comingAndGoingBuffers = new FrugalList(); foreach (ITextBuffer buffer in _addedBuffers) { if (_removedBuffers.Contains(buffer)) { comingAndGoingBuffers.Add(buffer); } } foreach (ITextBuffer buffer in comingAndGoingBuffers) { _addedBuffers.Remove(buffer); _removedBuffers.Remove(buffer); } addedBuffers = _addedBuffers; removedBuffers = _removedBuffers; _addedBuffers = null; _removedBuffers = null; _inTransaction = false; } public void AddSpan(ITrackingSpan span) { Debug.Assert(_inTransaction); int i = Find(span.TextBuffer); if (i < 0) { _sourceBufferTrackers.Add(new BufferTracker(span.TextBuffer)); _addedBuffers.Add(span.TextBuffer); } else { _sourceBufferTrackers[i]._spanCount++; } } public void RemoveSpan(ITrackingSpan span) { Debug.Assert(_inTransaction); int i = Find(span.TextBuffer); Debug.Assert(i >= 0); if (--_sourceBufferTrackers[i]._spanCount == 0) { _sourceBufferTrackers.RemoveAt(i); _removedBuffers.Add(span.TextBuffer); } } } public override IList SourceBuffers { // this is problematic, but we need it until the buffer graph implementation catches up to the new world get { return this.sourceBufferSet.SourceBuffers; } } #endregion #region Private State private ProjectionBufferOptions bufferOptions; private IDifferenceService differenceService; private List sourceSpans = new List(); private SourceBufferSet sourceBufferSet = new SourceBufferSet(); private ProjectionSnapshot currentProjectionSnapshot; private IInternalTextBufferFactory textBufferFactory; internal ITextBuffer literalBuffer; private IReadOnlyRegion literalBufferRor; private List eventHooks = new List(); #endregion #region Construction public ProjectionBuffer(IInternalTextBufferFactory textBufferFactory, IProjectionEditResolver resolver, IContentType contentType, IList initialSourceSpans, IDifferenceService differenceService, ITextDifferencingService textDifferencingService, ProjectionBufferOptions options, GuardedOperations guardedOperations) : base(resolver, contentType, textDifferencingService, guardedOperations) { // Parameters are validated outside Debug.Assert(initialSourceSpans != null); this.textBufferFactory = textBufferFactory; this.differenceService = differenceService; this.bufferOptions = options; SpanManager spanManager = new SpanManager(this, 0, 0, initialSourceSpans, false, false); spanManager.PerformChecks(); spanManager.ProcessLiteralSpans(); int spanPos = 0; this.sourceBufferSet.StartTransaction(); int initialLength = 0; List snapshotSpans = new List(); foreach (ITrackingSpan initialTrackingSpan in spanManager.SpansToInsert) { AddSpan(spanPos++, initialTrackingSpan); SnapshotSpan snapSpan = new SnapshotSpan(initialTrackingSpan.TextBuffer.CurrentSnapshot, initialTrackingSpan.GetSpan(initialTrackingSpan.TextBuffer.CurrentSnapshot)); initialLength += snapSpan.Length; snapshotSpans.Add(snapSpan); } StringRebuilder newBuilder = StringRebuilder.Empty; for (int i = 0; (i < snapshotSpans.Count); ++i) newBuilder = newBuilder.Append(BufferFactoryService.StringRebuilderFromSnapshotSpan(snapshotSpans[i])); this.builder = newBuilder; this.currentVersion.SetLength(initialLength); // this is a bit hacky FrugalList addedBuffers; FrugalList removedBuffers; this.sourceBufferSet.FinishTransaction(out addedBuffers, out removedBuffers); // listen to changes to source buffers bool firstAddedBuffer = true; BufferGroup chosenGroup = null; foreach (ITextBuffer addedBuffer in addedBuffers) { BaseBuffer baseAddedBuffer = (BaseBuffer)addedBuffer; if (firstAddedBuffer) { firstAddedBuffer = false; chosenGroup = baseAddedBuffer.group; chosenGroup.AddMember(this); } else { chosenGroup.Swallow(baseAddedBuffer.group); } if ((baseAddedBuffer != this.literalBuffer) || ((this.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) != 0)) { this.eventHooks.Add(new WeakEventHook(this, baseAddedBuffer)); } } this.group = chosenGroup ?? new BufferGroup(this); this.currentProjectionSnapshot = new ProjectionSnapshot(this, base.currentVersion, this.builder, snapshotSpans); this.currentSnapshot = this.currentProjectionSnapshot; } #endregion #region Span Editing and Management private class SourceSpansChangedEventRaiser : ITextEventRaiser { ProjectionSourceSpansChangedEventArgs args; public SourceSpansChangedEventRaiser(ProjectionSourceSpansChangedEventArgs args) { this.args = args; } public void RaiseEvent(BaseBuffer baseBuffer, bool immediate) { ProjectionBuffer projBuffer = (ProjectionBuffer)baseBuffer; ProjectionSourceBuffersChangedEventArgs bufferArgs = args as ProjectionSourceBuffersChangedEventArgs; if (bufferArgs != null) { EventHandler bufferHandlers = immediate ? projBuffer.SourceBuffersChangedImmediate : projBuffer.SourceBuffersChanged; if (bufferHandlers != null) { bufferHandlers(baseBuffer, bufferArgs); } } EventHandler spanHandlers = immediate ? projBuffer.SourceSpansChangedImmediate : projBuffer.SourceSpansChanged; if (spanHandlers != null) { spanHandlers(baseBuffer, args); } // now raise the text content changed event baseBuffer.RawRaiseEvent(args, immediate); } public bool HasPostEvent { get { return true; } } } /// /// Perform validity checking and normalization of source spans that are to be inserted. Checks /// for spans that are null, of the wrong type (neither string nor ITrackingSpan), would induce /// projection buffer cycles, have the wrong tracking mode, or induce projection overlaps. /// Also converts string literals to ITrackingSpans over the literal source buffer. /// private class SpanManager { public int Position { get; private set; } public int SpansToDelete { get; private set; } public List RawSpansToInsert { get; private set; } // The set of text buffers that have been previously visited in a cyclic dependency check private Dictionary visitedBufferSet = new Dictionary(); private ProjectionBuffer projBuffer; private List spansToInsert; private LiteralBufferHelper lit; private bool checkForCycles; public SpanManager(ProjectionBuffer projBuffer, int position, int spansToDelete, IList rawSpansToInsert, bool checkForCycles, bool groupEdit) { this.projBuffer = projBuffer; this.checkForCycles = checkForCycles; this.lit = new LiteralBufferHelper(projBuffer, (this.projBuffer.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) == 0, groupEdit); this.Position = position; this.SpansToDelete = spansToDelete; this.RawSpansToInsert = new List(rawSpansToInsert); } public List SpansToInsert { get { if (this.spansToInsert == null) { this.lit.FinishEdit(); this.spansToInsert = new List(); foreach (object rawSpan in this.RawSpansToInsert) { ITrackingSpan trackingSpan = rawSpan as ITrackingSpan; if (trackingSpan == null) { trackingSpan = this.projBuffer.literalBuffer.CurrentSnapshot.CreateTrackingSpan((Span)rawSpan, SpanTrackingMode.EdgeExclusive, TrackingFidelityMode.Forward); } this.spansToInsert.Add(trackingSpan); } } return this.spansToInsert; } } private void CheckForSourceBufferCycle(ITextBuffer buffer) { // TODO shouldn't we load up the visitedbufferset with existing source buffers first? at least for perf. if (!this.visitedBufferSet.ContainsKey(buffer)) { if (buffer == this.projBuffer) { throw new ArgumentException(Strings.SourceBufferCycle); } this.visitedBufferSet.Add(buffer, true); IProjectionBuffer p = buffer as ProjectionBuffer; if (p != null) { foreach (ITextBuffer sourceBuffer in p.SourceBuffers) { CheckForSourceBufferCycle(sourceBuffer); } } } } private static void CheckTrackingMode(ITrackingSpan spanToInsert) { if (spanToInsert.TrackingMode != SpanTrackingMode.EdgeExclusive && spanToInsert.TrackingMode != SpanTrackingMode.Custom) { ITextSnapshot snap = spanToInsert.TextBuffer.CurrentSnapshot; Span span = spanToInsert.GetSpan(snap); if (spanToInsert.TrackingMode == SpanTrackingMode.EdgeInclusive) { if (span.Start > 0 || span.End < snap.Length) { throw new ArgumentException(Strings.InvalidEdgeInclusiveSourceSpan); } } else if (spanToInsert.TrackingMode == SpanTrackingMode.EdgePositive) { if (span.End < snap.Length) { throw new ArgumentException(Strings.InvalidEdgePositiveSourceSpan); } } else if (span.Start > 0) { throw new ArgumentException(Strings.InvalidEdgeNegativeSourceSpan); } } } private IEnumerable GetProposedSpans() { for (int s = 0; s < this.Position; ++s) { yield return projBuffer.sourceSpans[s]; } for (int s = this.Position + this.SpansToDelete; s < projBuffer.sourceSpans.Count; ++s) { yield return projBuffer.sourceSpans[s]; } for (int s = 0; s < this.RawSpansToInsert.Count; ++s) { ITrackingSpan ts = this.RawSpansToInsert[s] as ITrackingSpan; if (ts != null) { yield return ts; } } } /// /// Build a list of ultimate sources for the given span, looking through all projections /// /// /// private IList BaseSourceSpans(SnapshotSpan span) { List result = new List(); if (span.Snapshot.TextBuffer is IProjectionBuffer) { foreach (SnapshotSpan s in ((IProjectionSnapshot)span.Snapshot).MapToSourceSnapshots(span)) { result.AddRange(BaseSourceSpans(s)); } } else { result.Add(span); } return result; } private void CheckOverlap() { Dictionary> bufferToSpans = new Dictionary>(); foreach (ITrackingSpan proposedSpan in GetProposedSpans()) { // Look through all projections IList baseSpans = BaseSourceSpans(proposedSpan.GetSpan(proposedSpan.TextBuffer.CurrentSnapshot)); // Group by source buffer foreach (SnapshotSpan baseSpan in baseSpans) { List spans; ITextBuffer buffer = baseSpan.Snapshot.TextBuffer; if (!bufferToSpans.TryGetValue(buffer, out spans)) { spans = new List(); bufferToSpans.Add(buffer, spans); } spans.Add(baseSpan); } } foreach (KeyValuePair> bufferSpans in bufferToSpans) { if (bufferSpans.Value.Count > 1) { // sort and check for overlap. // sort by start position, except if two spans start at the same position sort by // end position so that a null span comes first bufferSpans.Value.Sort(delegate(Span x, Span y) { return x.Start == y.Start ? x.End - y.End : x.Start - y.Start; }); for (int s = 1; s < bufferSpans.Value.Count; ++s) { if (bufferSpans.Value[s].Start < bufferSpans.Value[s - 1].End) { throw new ArgumentException(Strings.OverlappingSourceSpans); } } } } } public void PerformChecks() { foreach (object spanToInsert in this.RawSpansToInsert) { Requires.NotNull(spanToInsert, nameof(spanToInsert)); ITrackingSpan trackingSpanToInsert = spanToInsert as ITrackingSpan; if (trackingSpanToInsert != null) { if (checkForCycles) { try { CheckForSourceBufferCycle(trackingSpanToInsert.TextBuffer); } catch (ArgumentException) { throw new ArgumentException(Strings.SourceBufferCycle); } } if ((this.projBuffer.bufferOptions & ProjectionBufferOptions.PermissiveEdgeInclusiveSourceSpans) == 0) { CheckTrackingMode(trackingSpanToInsert); } } else { if (!(spanToInsert is string)) { throw new ArgumentException(Strings.NeitherSpanNorString); } } } CheckOverlap(); } public void ProcessLiteralSpans() { // must do deletions first! for (int d = this.Position; d < this.Position + this.SpansToDelete; ++d) { ITrackingSpan sourceSpan = this.projBuffer.sourceSpans[d]; if (sourceSpan.TextBuffer == this.projBuffer.literalBuffer) { this.lit.Delete(sourceSpan); } } for (int r = 0; r < this.RawSpansToInsert.Count; ++r) { object rawSpan = this.RawSpansToInsert[r]; string literalSpan = rawSpan as string; if (literalSpan != null) { // change the string into a Span indicating what the bounds of // the span will be when it is ready this.RawSpansToInsert[r] = this.lit.Append(literalSpan); } } } } private class LiteralBufferHelper { private ProjectionBuffer projBuffer; bool performedEdit; bool readOnly; bool groupEdit; int totalInsertions; int totalDeletions; int insertionPoint; public LiteralBufferHelper(ProjectionBuffer projBuffer, bool readOnly, bool groupEdit) { this.projBuffer = projBuffer; this.readOnly = readOnly; this.groupEdit = groupEdit; if (this.projBuffer.literalBuffer != null) { this.insertionPoint = this.projBuffer.literalBuffer.CurrentSnapshot.Length; } } private void PrepareEdit() { if (this.projBuffer.literalBuffer == null) { this.projBuffer.literalBuffer = projBuffer.textBufferFactory.CreateTextBuffer(string.Empty, projBuffer.textBufferFactory.InertContentType, readOnly); this.insertionPoint = 0; } else if (this.projBuffer.literalBufferRor != null) { using (IReadOnlyRegionEdit rorEdit = this.projBuffer.literalBuffer.CreateReadOnlyRegionEdit()) { rorEdit.RemoveReadOnlyRegion(this.projBuffer.literalBufferRor); rorEdit.Apply(); } this.projBuffer.literalBufferRor = null; } this.performedEdit = true; } public Span Append(string text) { PrepareEdit(); Span result; if (this.groupEdit) { ITextEdit edit = this.projBuffer.group.GetEdit((BaseBuffer)this.projBuffer.literalBuffer, EditOptions.None); edit.Insert(this.insertionPoint, text); result = new Span(this.insertionPoint + this.totalInsertions - this.totalDeletions, text.Length); this.totalInsertions += text.Length; } else { ITextSnapshot literalSnap = projBuffer.literalBuffer.CurrentSnapshot; int length = literalSnap.Length; literalSnap = projBuffer.literalBuffer.Insert(length, text); result = new Span(length, text.Length); } return result; } public void Delete(ITrackingSpan trackingSpan) { PrepareEdit(); Span span = trackingSpan.GetSpan(projBuffer.literalBuffer.CurrentSnapshot); if (this.groupEdit) { ITextEdit edit = this.projBuffer.group.GetEdit((BaseBuffer)this.projBuffer.literalBuffer, EditOptions.None); edit.Delete(span); totalDeletions += span.Length; } else { projBuffer.literalBuffer.Delete(span); } } public void FinishEdit() { if (this.performedEdit && this.readOnly) { if (this.projBuffer.literalBuffer != null) { Debug.Assert(this.projBuffer.literalBufferRor == null); using (IReadOnlyRegionEdit rorEdit = this.projBuffer.literalBuffer.CreateReadOnlyRegionEdit()) { this.projBuffer.literalBufferRor = rorEdit.CreateReadOnlyRegion(new Span(0, rorEdit.Snapshot.Length), SpanTrackingMode.EdgeInclusive, EdgeInsertionMode.Deny); rorEdit.Apply(); } } } } } private class SpanEdit : TextBufferBaseEdit, ISubordinateTextEdit { private ProjectionBuffer projBuffer; private EditOptions editOptions = EditOptions.None; private object tag = null; private SpanManager spanManager; public SpanEdit(ProjectionBuffer projBuffer) : base(projBuffer) { this.projBuffer = projBuffer; } public ITextBuffer TextBuffer { get { return this.projBuffer; } } public IProjectionSnapshot ReplaceSpans(int position, int spansToReplace, IList spansToInsert, EditOptions options, object editTag) { if (position < 0 || position > this.projBuffer.sourceSpans.Count) { throw new ArgumentOutOfRangeException(nameof(position)); } if (spansToReplace < 0 || position + spansToReplace > this.projBuffer.sourceSpans.Count) { throw new ArgumentOutOfRangeException(nameof(spansToReplace)); } if (spansToInsert == null) { throw new ArgumentNullException(nameof(spansToInsert)); } this.spanManager = new SpanManager(this.projBuffer, position, spansToReplace, spansToInsert, true, (this.projBuffer.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) != 0); this.editOptions = options; this.tag = editTag; this.spanManager.PerformChecks(); // we are committed! this.applied = true; if (this.spanManager.SpansToDelete > 0 || this.spanManager.RawSpansToInsert.Count > 0) { // non-vacuous this.projBuffer.group.PerformMasterEdit(this.projBuffer, this, this.editOptions, this.tag); } this.baseBuffer.group.FinishEdit(); this.baseBuffer.editInProgress = false; return this.projBuffer.currentProjectionSnapshot; } public void PreApply() { this.projBuffer.editApplicationInProgress = true; this.spanManager.ProcessLiteralSpans(); } public void FinalApply() { ProjectionSourceSpansChangedEventArgs args = this.projBuffer.ApplySpanChanges(this.spanManager.Position, this.spanManager.SpansToDelete, this.spanManager.SpansToInsert, this.editOptions, this.tag); SourceSpansChangedEventRaiser raiser = new SourceSpansChangedEventRaiser(args); this.baseBuffer.group.EnqueueEvents(raiser, this.baseBuffer); raiser.RaiseEvent(this.baseBuffer, true); this.baseBuffer.editInProgress = false; this.projBuffer.editApplicationInProgress = false; if ((this.projBuffer.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) != 0) { // the only pending changes should be changes to our literal buffer Debug.Assert(this.projBuffer.pendingContentChangedEventArgs.Count <= 1); if (this.projBuffer.pendingContentChangedEventArgs.Count == 1) { Debug.Assert(this.projBuffer.pendingContentChangedEventArgs[0].Before.TextBuffer == this.projBuffer.literalBuffer); } // forget about changes to our literal buffer; we've already incorporated them this.projBuffer.pendingContentChangedEventArgs.Clear(); } else { Debug.Assert(this.projBuffer.pendingContentChangedEventArgs.Count == 0); } } public bool CheckForCancellation(Action cancelAction) { return true; } public override string ToString() { StringBuilder insertions = new StringBuilder(); for (int t = 0; t < this.spanManager.RawSpansToInsert.Count; ++t) { ITrackingSpan insertion = this.spanManager.RawSpansToInsert[t] as ITrackingSpan; if (insertion != null) { insertions.Append(insertion.ToString()); } else { insertions.Append("(Literal)"); } if (t < this.spanManager.RawSpansToInsert.Count - 1) { insertions.Append(","); } } return string.Format(System.Globalization.CultureInfo.CurrentCulture, "pos: {0}, delete: {1}, insert: {2}", this.spanManager.Position, this.spanManager.SpansToDelete, insertions.ToString()); } public void RecordMasterChangeOffset(int masterChangeOffset) { throw new InvalidOperationException("Projection span edits shouldn't have change offsets."); } } private ProjectionSourceSpansChangedEventArgs ApplySpanChanges(int position, int spansToDelete, IList spansToInsert, EditOptions options, object editTag) { ProjectionSnapshot beforeSnapshot = this.currentProjectionSnapshot; List deletedSpans = new List(); List insertedSnapSpans = new List(); List deletedSnapSpans = new List(); this.sourceBufferSet.StartTransaction(); for (int i = position + spansToDelete - 1; i >= position; --i) { ITrackingSpan removedSpan = RemoveSpan(i); deletedSpans.Insert(0, removedSpan); // preserve order! deletedSnapSpans.Insert(0, this.currentProjectionSnapshot.GetSourceSpan(i)); } int insertPosition = position; foreach (ITrackingSpan span in spansToInsert) { AddSpan(insertPosition++, span); insertedSnapSpans.Add(span.GetSpan(span.TextBuffer.CurrentSnapshot)); } FrugalList addedBuffers; FrugalList removedBuffers; this.sourceBufferSet.FinishTransaction(out addedBuffers, out removedBuffers); // todo make this transactional in case it fails here, or else check thread affinity earlier // todo combine with later loop (can it move up here?) foreach (ITextBuffer addedBuffer in addedBuffers) { BaseBuffer baseAddedBuffer = (BaseBuffer)addedBuffer; this.group.Swallow(baseAddedBuffer.group); } int textPosition = 0; for (int i = 0; i < position; ++i) { textPosition += this.currentProjectionSnapshot.GetSourceSpan(i).Length; } INormalizedTextChangeCollection normalizedChanges; if (options.ComputeMinimalChange) { normalizedChanges = ComputeTextChangesByStringDiffing(options.DifferenceOptions, textPosition, deletedSnapSpans, insertedSnapSpans); } else { normalizedChanges = ComputeTextChangesBySpanDiffing(textPosition, deletedSnapSpans, insertedSnapSpans); } SetCurrentVersionAndSnapshot(normalizedChanges); ProjectionSourceSpansChangedEventArgs args = null; if (addedBuffers.Count > 0 || removedBuffers.Count > 0) { // Adjust buffer change listening foreach (ITextBuffer addedBuffer in addedBuffers) { if (addedBuffer != this.literalBuffer || (this.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) != 0) { BaseBuffer baseAddedBuffer = (BaseBuffer)addedBuffer; this.eventHooks.Add(new WeakEventHook(this, baseAddedBuffer)); } } foreach (ITextBuffer removedBuffer in removedBuffers) { if (removedBuffer != this.literalBuffer || (this.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) != 0) { BaseBuffer baseRemovedBuffer = (BaseBuffer)removedBuffer; for (int i = 0; (i < this.eventHooks.Count); ++i) { var hook = this.eventHooks[i]; if (hook.SourceBuffer == baseRemovedBuffer) { hook.UnsubscribeFromSourceBuffer(); this.eventHooks.RemoveAt(i); break; } } } } args = new ProjectionSourceBuffersChangedEventArgs (beforeSnapshot, this.currentProjectionSnapshot, spansToInsert, deletedSpans, position, addedBuffers, removedBuffers, options, editTag); } else { args = new ProjectionSourceSpansChangedEventArgs (beforeSnapshot, this.currentProjectionSnapshot, spansToInsert, deletedSpans, position, options, editTag); } return args; } private INormalizedTextChangeCollection ComputeTextChangesByStringDiffing(StringDifferenceOptions differenceOptions, int textPosition, List deletedSnapSpans, List insertedSnapSpans) { StringBuilder oldText = new StringBuilder(); foreach (SnapshotSpan deletedSnapSpan in deletedSnapSpans) { oldText.Append(deletedSnapSpan.GetText()); } StringBuilder newText = new StringBuilder(); foreach (SnapshotSpan insertedSnapSpan in insertedSnapSpans) { newText.Append(insertedSnapSpan.GetText()); } List changes = new List(); if (oldText.Length > 0 || newText.Length > 0) { changes.Add(TextChange.Create(textPosition, oldText.ToString(), newText.ToString(), this.currentProjectionSnapshot)); } return NormalizedTextChangeCollection.Create(changes, differenceOptions, this.textDifferencingService); } private INormalizedTextChangeCollection ComputeTextChangesBySpanDiffing(int textPosition, List deletedSnapSpans, List insertedSnapSpans) { ProjectionSpanDiffer differ = new ProjectionSpanDiffer(this.differenceService, deletedSnapSpans.AsReadOnly(), insertedSnapSpans.AsReadOnly()); return new ProjectionSpanToNormalizedChangeConverter(differ, textPosition, this.currentProjectionSnapshot).NormalizedChanges; } private void AddSpan(int position, ITrackingSpan sourceSpan) { this.sourceSpans.Insert(position, sourceSpan); this.sourceBufferSet.AddSpan(sourceSpan); } private ITrackingSpan RemoveSpan(int position) { ITrackingSpan result = this.sourceSpans[position]; this.sourceSpans.RemoveAt(position); this.sourceBufferSet.RemoveSpan(result); return result; } public IProjectionSnapshot InsertSpan(int position, ITrackingSpan spanToInsert) { return ReplaceSpans(position, 0, new List(1) { spanToInsert }, EditOptions.None, null); } public IProjectionSnapshot InsertSpan(int position, string literalSpanToInsert) { return ReplaceSpans(position, 0, new List(1) { literalSpanToInsert }, EditOptions.None, null); } public IProjectionSnapshot InsertSpans(int position, IList spansToInsert) { return ReplaceSpans(position, 0, spansToInsert, EditOptions.None, null); } public IProjectionSnapshot DeleteSpans(int position, int spansToDelete) { return ReplaceSpans(position, spansToDelete, new List(0), EditOptions.None, null); } public IProjectionSnapshot ReplaceSpans(int position, int spansToReplace, IList spansToInsert, EditOptions options, object editTag) { using (SpanEdit spedit = new SpanEdit(this)) { return spedit.ReplaceSpans(position, spansToReplace, spansToInsert, options, editTag); } } #endregion #region Edit Creation public override ITextEdit CreateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag) { return new ProjectionEdit(this, this.currentProjectionSnapshot, options, reiteratedVersionNumber, editTag); } protected internal override ISubordinateTextEdit CreateSubordinateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag) { return new ProjectionEdit(this, this.currentProjectionSnapshot, options, reiteratedVersionNumber, editTag); } #endregion #region Event Handling /// /// Respond to a content change in a source buffer by mapping the change events to the coordinate system of /// this projection buffer. /// internal override void OnSourceTextChanged(object sender, TextContentChangedEventArgs e) { this.pendingContentChangedEventArgs.Add(e); if (!this.editApplicationInProgress) { // We had better be a member of the same group as the buffer that we just heard from Debug.Assert(this.group.MembersContains(e.After.TextBuffer)); // Let the buffer group decide when to issue events; this allows us to collect changes from multiple // source buffers into a single snapshot, and (more important) to avoid inconsistencies this.group.ScheduleIndependentEdit(this); } } #endregion #region Source Change Interpretation public override BaseBuffer.ITextEventRaiser PropagateSourceChanges(EditOptions options, object editTag) { IList eventRaisers = InterpretSourceChanges(options, /*this.reiteratedVersionNumber,*/ editTag); Debug.Assert(eventRaisers.Count == 1); eventRaisers[0].RaiseEvent(this, true); return eventRaisers[0]; } /// /// TextChanges that are to be accomplished by a change to source spans (because of EdgeExclusive anomalies). /// private class SpanAdjustment { public TextChange LeadingChange; // Text change induced by span growth/shrinkage on the leading edge public TextChange TrailingChange; // Text change induced by span growth/shrinkage on the trailing edge } private IList InterpretSourceChanges(EditOptions options, object editTag) { // now that all source edits have been applied, we can interpret the events they raised List eventRaisers = new List(); SortedDictionary spanPreAdjustments = new SortedDictionary(); SortedDictionary spanPostAdjustments = new SortedDictionary(); INormalizedTextChangeCollection normalizedChanges = ComputeProjectedChanges(spanPreAdjustments, spanPostAdjustments); // Shrink source spans for each span pre-adjustment. Each one generates a new projection version/snapshot. if (BufferGroup.Tracing) { Debug.WriteLine("Span Preadjustments"); } foreach (KeyValuePair positionAndAdjustment in spanPreAdjustments) { eventRaisers.Add(PerformSpanAdjustments(positionAndAdjustment.Key, positionAndAdjustment.Value, true, editTag)); } if (BufferGroup.Tracing) { Debug.WriteLine("Main Snapshot"); } //if (normalizedChanges.Count > 0) // TODO: optimize this snapshot away if Count == 0 { ITextSnapshot originSnapshot = this.currentProjectionSnapshot; base.SetCurrentVersionAndSnapshot(normalizedChanges); eventRaisers.Add(new TextContentChangedEventRaiser(originSnapshot, this.currentProjectionSnapshot, options, editTag)); } // Generate additional transaction(s) that alter EdgeExclusive source spans as needed if (BufferGroup.Tracing) { Debug.WriteLine("Span Postadjustments"); } foreach (KeyValuePair positionAndAdjustment in spanPostAdjustments) { eventRaisers.Add(PerformSpanAdjustments(positionAndAdjustment.Key, positionAndAdjustment.Value, false, editTag)); } this.editApplicationInProgress = false; return eventRaisers; } private SourceSpansChangedEventRaiser PerformSpanAdjustments(int spanPosition, SpanAdjustment adjustment, bool beforeBaseSnapshot, object editTag) { IProjectionSnapshot originSnapshot = this.currentProjectionSnapshot; List newSourceSpans = new List(originSnapshot.GetSourceSpans()); this.sourceBufferSet.StartTransaction(); ITrackingSpan deletedSpan = RemoveSpan(spanPosition); SnapshotSpan sourceOriginSpan = originSnapshot.GetSourceSpans(spanPosition, 1)[0]; int start = sourceOriginSpan.Start; int end = sourceOriginSpan.End; List changes = new List(); if (adjustment.LeadingChange != null) { changes.Add(adjustment.LeadingChange); if (beforeBaseSnapshot) { start += adjustment.LeadingChange.OldLength; } else { start -= adjustment.LeadingChange.NewLength; } } if (adjustment.TrailingChange != null) { changes.Add(adjustment.TrailingChange); if (beforeBaseSnapshot) { end -= adjustment.TrailingChange.OldLength; } else { end += adjustment.TrailingChange.NewLength; } } ITrackingSpan insertedSpan = sourceOriginSpan.Snapshot.CreateTrackingSpan(Span.FromBounds(start, end), deletedSpan.TrackingMode); AddSpan(spanPosition, insertedSpan); newSourceSpans[spanPosition] = new SnapshotSpan(sourceOriginSpan.Snapshot, Span.FromBounds(start, end)); FrugalList addedBuffers; FrugalList removedBuffers; this.sourceBufferSet.FinishTransaction(out addedBuffers, out removedBuffers); INormalizedTextChangeCollection normalizedChanges = NormalizedTextChangeCollection.Create(changes); this.currentVersion = this.currentVersion.CreateNext(normalizedChanges, newLength: -1, reiteratedVersionNumber: -1); this.builder = this.ApplyChangesToStringRebuilder(normalizedChanges, this.builder); if (beforeBaseSnapshot) { this.currentProjectionSnapshot = TakeStaticSnapshot(newSourceSpans); } else { this.currentSnapshot = TakeSnapshot(); } ProjectionSourceSpansChangedEventArgs args = new ProjectionSourceSpansChangedEventArgs(originSnapshot, this.currentProjectionSnapshot, new ITrackingSpan[] { insertedSpan }, new ITrackingSpan[] { deletedSpan }, spanPosition, EditOptions.None, editTag); return new SourceSpansChangedEventRaiser(args); } private void DeleteFromSource(SnapshotSpan deletionSpan) { ITextBuffer sourceBuffer = deletionSpan.Snapshot.TextBuffer; ITextEdit xedit = this.group.GetEdit((BaseBuffer)sourceBuffer); xedit.Delete(deletionSpan); } private void ReplaceInSource(SnapshotSpan deletionSpan, string insertionText, int masterChangeOffset) { ITextBuffer sourceBuffer = deletionSpan.Snapshot.TextBuffer; ITextEdit xedit = this.group.GetEdit((BaseBuffer)sourceBuffer); xedit.Replace(deletionSpan, insertionText); ((ISubordinateTextEdit)xedit).RecordMasterChangeOffset(masterChangeOffset); // This above cast is safe because the buffer group stores edits of type ISubordinateTextEdit // and casts them to ITextEdit before returning them. } private void InsertInSource(SnapshotPoint sourceInsertionPoint, string text, int masterChangeOffset) { ITextBuffer sourceBuffer = sourceInsertionPoint.Snapshot.TextBuffer; ITextEdit xedit = this.group.GetEdit((BaseBuffer)sourceBuffer); xedit.Insert(sourceInsertionPoint.Position, text); ((ISubordinateTextEdit)xedit).RecordMasterChangeOffset(masterChangeOffset); // This above cast is safe because the buffer group stores edits of type ISubordinateTextEdit // and casts them to ITextEdit before returning them. } /// /// Generate the set of normalized text changes to the projection buffer that are induced by the current set of /// pending source changes. /// /// Adjustments to be manifested as shrinkage of EdgeExclusive source spans before adopting new source snapshot(s). /// Adjustments to be manifested as growth of EdgeExclusive source spans adopting new source snapshot(s). /// private INormalizedTextChangeCollection ComputeProjectedChanges(SortedDictionary spanPreAdjustments, SortedDictionary spanPostAdjustments) { DumpPendingContentChangedEventArgs(); List>> pendingSourceChanges = PreparePendingChanges(); List projectedChanges = new List(); DumpPendingChanges(pendingSourceChanges); // these are the points in ordinary text buffers at which we have performed span adjustments // to account for edge-exclusive anomalies. We track them so that we never repeat the adjustment // for the same change. HashSet urPoints = new HashSet(); // Process the pending changes in the reverse of the order in which we received them. This means // that the 'nearest' buffers are processed first. for (int p = pendingSourceChanges.Count - 1; p >= 0; --p) { var pair = pendingSourceChanges[p]; List sourceChanges = pair.Item2; int accumulatedDelta = 0; for (int sc = 0; sc < sourceChanges.Count - 1; ++sc) // skip the sentinel { InterpretSourceBufferChange(pair.Item1, sourceChanges[sc], projectedChanges, urPoints, spanPreAdjustments, spanPostAdjustments, accumulatedDelta); accumulatedDelta += sourceChanges[sc].Delta; } } if (this.editApplicationInProgress && (spanPreAdjustments.Count > 0 || spanPostAdjustments.Count > 0)) { // We may be generating several distinct versions, so we have to do some position normalization. // This only occurs in edits originating on this buffer, not independent changes to source buffers. CorrectSpanAdjustments(projectedChanges, spanPreAdjustments, spanPostAdjustments); } return NormalizedTextChangeCollection.Create(projectedChanges); } private List>> PreparePendingChanges() { // Changes to source buffers are interpreted against the state of the projection buffer // at the beginning of the transaction. However, events raised by source buffers have // been normalized so that coordinates of later versions are expressed in terms of the // immediately preceding version, not the version at the beginning of the transaction. // Therefore we denormalize the changes before considering them here. This has effect only // when more than one new version has been created for a particular source buffer. List>> pendingSourceChanges = new List>>(); foreach (TextContentChangedEventArgs args in this.pendingContentChangedEventArgs) { ITextBuffer sourceBuffer = args.Before.TextBuffer; // Get the list of pending changes associated with the source buffer. In the case of an // independent change to the source buffer, this list is always empty. For dependent changes, // the list can be nonempty if there are multiple routes to the source buffer. // In the usual case there will be only one change event for a source buffer so this // list will not be found. List bufferChanges; var pair = pendingSourceChanges.Find((p) => (p.Item1 == sourceBuffer)); if (pair == null) { bufferChanges = new List(1) { new TextChange(int.MaxValue, StringRebuilder.Empty, StringRebuilder.Empty, LineBreakBoundaryConditions.None) }; pendingSourceChanges.Add(new Tuple>(sourceBuffer, bufferChanges)); } else { bufferChanges = pair.Item2; } NormalizedTextChangeCollection.Denormalize(args.Changes, bufferChanges); } this.pendingContentChangedEventArgs.Clear(); return pendingSourceChanges; } private static FrugalList> Load(SortedDictionary adjustments) { FrugalList> result = new FrugalList>(); foreach (SpanAdjustment adjustment in adjustments.Values) { int fudge = 0; if (adjustment.LeadingChange != null) { result.Add(new Tuple(adjustment.LeadingChange, 0)); fudge = adjustment.LeadingChange.Delta; } if (adjustment.TrailingChange != null) { result.Add(new Tuple(adjustment.TrailingChange, fudge)); } } return result; } private static void CorrectSpanAdjustments(List ordinaryChanges, SortedDictionary spanPreAdjustments, SortedDictionary spanPostAdjustments) { TextChange[] sortedOrdinary = TextUtilities.StableSort(ordinaryChanges, (left, right) => (left.OldPosition - right.OldPosition)); FrugalList ordinary = new FrugalList(sortedOrdinary); FrugalList> preAdjustments = Load(spanPreAdjustments); FrugalList> postAdjustments = Load(spanPostAdjustments); int count = ordinary.Count + preAdjustments.Count + postAdjustments.Count; TextChange sentinel = new TextChange(int.MaxValue, StringRebuilder.Empty, StringRebuilder.Empty, LineBreakBoundaryConditions.None); ordinary.Add(sentinel); preAdjustments.Add(new Tuple(sentinel, 0)); postAdjustments.Add(new Tuple(sentinel, 0)); int ordDelta = 0; int preDelta = 0; int postDelta = 0; int ordIndex = 0; int preIndex = 0; int postIndex = 0; int ordPos = ordinary[0].OldPosition; int prePos = preAdjustments[0].Item1.OldPosition; int postPos = postAdjustments[0].Item1.OldPosition; for (int c = 0; (c < count); ++c) { if (postPos <= prePos && postPos <= ordPos) { // post either comes first or is tied. Advance it one step since its adjustments won't affect any of the other adjustments // (they don't depend on postDelta). postAdjustments[postIndex].Item1.OldPosition += preDelta + ordDelta + postDelta - postAdjustments[postIndex].Item2; postAdjustments[postIndex].Item1.NewPosition = postAdjustments[postIndex].Item1.OldPosition; postDelta += postAdjustments[postIndex].Item1.Delta; postPos = postAdjustments[++postIndex].Item1.OldPosition; } else if (ordPos <= prePos) { // The ordinary change comes before or is tied with pre and comes before post. Advance it one step since its adjustment won't // affect pre's adjustment. // postPos comes after ordPos or prePos. // if ordPos is before prePos, then it must also come before postPos. Debug.Assert(ordPos < postPos); ordinary[ordIndex].OldPosition += preDelta; ordinary[ordIndex].NewPosition = ordinary[ordIndex].OldPosition; ordDelta += ordinary[ordIndex].Delta; ordPos = ordinary[++ordIndex].OldPosition; } else { Debug.Assert(prePos < ordPos); Debug.Assert(prePos < postPos); preAdjustments[preIndex].Item1.OldPosition += preDelta - preAdjustments[preIndex].Item2; preAdjustments[preIndex].Item1.NewPosition = preAdjustments[preIndex].Item1.OldPosition; preDelta += preAdjustments[preIndex].Item1.Delta; prePos = preAdjustments[++preIndex].Item1.OldPosition; } } Debug.Assert(ordIndex == ordinary.Count - 1); Debug.Assert(preIndex == preAdjustments.Count - 1); Debug.Assert(postIndex == postAdjustments.Count - 1); } /// /// Figure out how a source buffer change affects the projection buffer. /// /// The source buffer. /// The TextChange received from the source buffer. /// List of changes to projection buffer to be augmented if /// applies to the source buffer and is not otherwised covered by a span adjustment. /// /// Adjustments to be manifested as shrinkage of EdgeExclusive source spans before adopting new source snapshot(s). /// Adjustments to be manifested as growth of EdgeExclusive source spans adopting new source snapshot(s). private void InterpretSourceBufferChange(ITextBuffer changedBuffer, ITextChange change, List projectedChanges, HashSet urPoints, SortedDictionary spanPreAdjustments, SortedDictionary spanPostAdjustments, int accumulatedDelta) { ProjectionSnapshot priorSnapshot = this.currentProjectionSnapshot; int sourceChangePosition = change.NewPosition; Span deletionSpan = new Span(sourceChangePosition, change.OldLength); int insertionCount = change.NewLength; int cumulativeLength = 0; int spanPosition = 0; ITextSnapshot afterSourceSnapshot = changedBuffer.CurrentSnapshot; // todo: consider whether need to use a more precise snapshot. I don't think so, but give it more thought. // this is used only for mapping to urPoints. // This algorithm does a linear search of source spans in forward order. foreach (ITrackingSpan sourceSpan in this.sourceSpans) { SnapshotSpan priorRawSpan = priorSnapshot.GetSourceSpan(spanPosition); // Note: if we switch back to not generating a new snapshot of the projection buffer on every source // buffer change, then here we have to be careful to map the priorRawSpan to the current snapshot of the source buffer, // since it might be coming from an old snapshot (see e.g. Edit00 unit test) if (sourceSpan.TextBuffer == changedBuffer) { SpanTrackingMode mode = sourceSpan.TrackingMode; // is there an easy way to handle custom spans here? Span? deletedHere = deletionSpan.Overlap(priorRawSpan); // n.b.: Null span does not overlap with anything if (deletedHere.HasValue && deletedHere.Value.Length > 0) { // part or all of the source span was deleted by the change // compute the position at which the change takes place in the projection buffer // with respect to its current snapshot int projectedPosition = cumulativeLength + deletedHere.Value.Start - priorRawSpan.Start; Debug.Assert(projectedPosition >= 0 && projectedPosition <= priorSnapshot.Length); StringRebuilder deletedText = TextChange.ChangeOldSubText(change, Math.Max(priorRawSpan.Start.Position - deletionSpan.Start, 0), deletedHere.Value.Length); StringRebuilder insertedText = StringRebuilder.Empty; SnapshotSpan adjustedPriorRawSpan = new SnapshotSpan(priorRawSpan.Snapshot, priorRawSpan.Start, priorRawSpan.Length - deletedText.Length); if (sourceSpan.TrackingMode != SpanTrackingMode.EdgeInclusive && sourceSpan.TrackingMode != SpanTrackingMode.Custom && this.editInProgress) { // the tricky cases. If the deletion touches the edge of the source span, we first explicitly // shrink the span to effect the deletion. If the change is later undone, the source span // will be grown explicitly to encompass the restored text (otherwise we would lose it since // the source span is EdgeExclusive and won't grow on its own). if ((sourceSpan.TrackingMode != SpanTrackingMode.EdgeNegative) && (deletedHere.Value.Start == priorRawSpan.Start)) { // A prefix of the source span (or the whole thing) is to be shrunk to effect the deletion. SpanAdjustment adjust = GetAdjustment(spanPreAdjustments, spanPosition); // Create the text change that will be induced by the span adjustment Debug.Assert(adjust.LeadingChange == null); // there can only be one leading change for a particular span adjust.LeadingChange = TextChange.Create(projectedPosition, deletedText, string.Empty, this.currentProjectionSnapshot); Debug.Assert(adjust.LeadingChange.OldEnd <= priorSnapshot.Length); deletedText = StringRebuilder.Empty; } else if ((sourceSpan.TrackingMode != SpanTrackingMode.EdgePositive) && (deletedHere.Value.End == priorRawSpan.End)) { // A suffix of the source span is to be shrunk to effect the deletion. SpanAdjustment adjust = GetAdjustment(spanPreAdjustments, spanPosition); // Create the text change that will be induced by the span adjustment Debug.Assert(adjust.TrailingChange == null); adjust.TrailingChange = TextChange.Create(projectedPosition, deletedText, string.Empty, this.currentProjectionSnapshot); Debug.Assert(adjust.TrailingChange.OldEnd <= priorSnapshot.Length); deletedText = StringRebuilder.Empty; } } if (change.NewLength > 0) // change includes an insertion { insertedText = InsertionLiesInSpan(afterSourceSnapshot, projectedPosition, spanPosition, adjustedPriorRawSpan, deletionSpan, sourceChangePosition, mode, change, urPoints, spanPostAdjustments, accumulatedDelta); if (insertedText.Length > 0) { // replacement string is inserted here. There can be more than one insertion // per change if custom tracking spans are involved. insertionCount = change.NewLength - insertedText.Length; } } if (deletedText.Length > 0 || insertedText.Length > 0) { TextChange interpretedChange = TextChange.Create(projectedPosition, deletedText, insertedText, this.currentProjectionSnapshot); Debug.Assert(interpretedChange.OldEnd <= priorSnapshot.Length); projectedChanges.Add(interpretedChange); } } else if (insertionCount > 0) { int projectedPosition = cumulativeLength + Math.Max(sourceChangePosition - priorRawSpan.Start, 0); // if the insertion is part of a replacement and the source span in question is edge inclusive, a sourceChangePosition to the // left of the current source span may actually end up being interesting, in which case it would be at the beginning of the span. // If those conditions don't obtain, InsertionLiesInSpan will return false and nobody will be the wiser. int hack = spanPostAdjustments == null ? 0 : spanPostAdjustments.Count; StringRebuilder insertedText = InsertionLiesInSpan(afterSourceSnapshot, projectedPosition, spanPosition, priorRawSpan, deletionSpan, sourceChangePosition, mode, change, urPoints, spanPostAdjustments, accumulatedDelta); if (insertedText.Length > 0) { // a pure insertion into the source span TextChange interpretedChange = TextChange.Create(projectedPosition, string.Empty, insertedText, this.currentProjectionSnapshot); projectedChanges.Add(interpretedChange); } if (spanPostAdjustments != null && spanPostAdjustments.Count != hack) // ur points should have eliminated the need for the hack { insertionCount = 0; } } } cumulativeLength += priorRawSpan.Length; spanPosition++; } } private StringRebuilder InsertionLiesInSpan(ITextSnapshot afterSourceSnapshot, int projectedPosition, int spanPosition, SnapshotSpan rawSpan, Span deletionSpan, int sourcePosition, SpanTrackingMode mode, ITextChange incomingChange, HashSet urPoints, SortedDictionary spanAdjustments, int accumulatedDelta) { int renormalizedSourcePosition = sourcePosition + accumulatedDelta; if (mode == SpanTrackingMode.Custom) { return InsertionLiesInCustomSpan(afterSourceSnapshot, spanPosition, incomingChange, urPoints, accumulatedDelta); } bool contains = rawSpan.Contains(sourcePosition); if (mode == SpanTrackingMode.EdgeInclusive) { if ((this.bufferOptions & ProjectionBufferOptions.PermissiveEdgeInclusiveSourceSpans) != 0) { return InsertionLiesInPermissiveInclusiveSpan (afterSourceSnapshot, rawSpan, deletionSpan, sourcePosition, renormalizedSourcePosition, incomingChange, urPoints); } else { return contains || sourcePosition == rawSpan.End ? TextChange.NewStringRebuilder(incomingChange) : StringRebuilder.Empty; } } else { if (!this.editInProgress) { // Edit originated in the source buffer; we don't do any implicit growing // of spans bool included; if (mode == SpanTrackingMode.EdgeNegative) { included = contains; } else if (mode == SpanTrackingMode.EdgePositive) { included = (sourcePosition != rawSpan.Start) && (contains || sourcePosition == rawSpan.End); } else { included = contains && sourcePosition != rawSpan.Start; } return included ? TextChange.NewStringRebuilder(incomingChange) : StringRebuilder.Empty; } else { if (sourcePosition == rawSpan.Start && (mode != SpanTrackingMode.EdgeNegative)) { SnapshotPoint? urPoint = MappingHelper.MapDownToFirstMatchNoTrack(new SnapshotPoint(afterSourceSnapshot, renormalizedSourcePosition), (buffer) => (buffer is TextBuffer), PositionAffinity.Successor); Debug.Assert(urPoint.HasValue); if (urPoints.Add(urPoint.Value)) { if (BufferGroup.Tracing) { Debug.WriteLine("UR-Point [exclusive:start]" + urPoint.Value.ToString()); } // Insertion at exclusive left edge of source span: we need to grow the source span on the left SpanAdjustment adjust = GetAdjustment(spanAdjustments, spanPosition); // Create the text change that will be induced by the span adjustment Debug.Assert(adjust.LeadingChange == null); adjust.LeadingChange = TextChange.Create(projectedPosition, StringRebuilder.Empty, TextChange.NewStringRebuilder(incomingChange), this.currentProjectionSnapshot); } return StringRebuilder.Empty; // this insertion either already happened or happens on a subsequent transaction, not this one } else if (sourcePosition == rawSpan.End && (mode != SpanTrackingMode.EdgePositive)) { SnapshotPoint? urPoint = MappingHelper.MapDownToFirstMatchNoTrack(new SnapshotPoint(afterSourceSnapshot, renormalizedSourcePosition), (buffer) => (buffer is TextBuffer), PositionAffinity.Predecessor); Debug.Assert(urPoint.HasValue); if (urPoints.Add(urPoint.Value)) { if (BufferGroup.Tracing) { Debug.WriteLine("UR-Point [exclusive:end]" + urPoint.Value.ToString()); } // Insertion at exclusive right edge of source span: we need to grow the source span on the right SpanAdjustment adjust = GetAdjustment(spanAdjustments, spanPosition); // Create the text change that will be induced by the span adjustment Debug.Assert(adjust.TrailingChange == null); adjust.TrailingChange = TextChange.Create(projectedPosition, StringRebuilder.Empty, TextChange.NewStringRebuilder(incomingChange), this.currentProjectionSnapshot); } return StringRebuilder.Empty; // this insertion either already happened or happens on a subsequent transaction, not this one } return (contains || (mode == SpanTrackingMode.EdgePositive && sourcePosition == rawSpan.End)) ? TextChange.NewStringRebuilder(incomingChange) : StringRebuilder.Empty; } } } #pragma warning disable CA1801 // Review unused parameters private StringRebuilder InsertionLiesInCustomSpan(ITextSnapshot afterSourceSnapshot, int spanPosition, ITextChange incomingChange, HashSet urPoints, int accumulatedDelta) { #pragma warning disable CA1801 // Review unused parameters // just evaluate the new span and see if it overlaps the insertion. ITrackingSpan sourceTrackingSpan = this.sourceSpans[spanPosition]; SnapshotSpan afterSpan = sourceTrackingSpan.GetSpan(afterSourceSnapshot); Span newSpan = new Span(incomingChange.NewPosition + accumulatedDelta, incomingChange.NewLength); Span? over = newSpan.Overlap(afterSpan); return over.HasValue ? BufferFactoryService.StringRebuilderFromSnapshotAndSpan(afterSourceSnapshot, over.Value) : StringRebuilder.Empty; //if (futureSpan.Contains(renormalizedSourcePosition)) //{ // if (BufferGroup.Tracing) // { // Debug.WriteLine(string.Format(System.Globalization.CultureInfo.CurrentCulture, // "Custom renormPosition {0} priorSpan [{1}..{2})", renormalizedSourcePosition, priorSpan.Start.Position + accumulatedDelta, priorSpan.End.Position + accumulatedDelta)); // } // if (renormalizedSourcePosition == (priorSpan.Start.Position + accumulatedDelta) || renormalizedSourcePosition == (priorSpan.End.Position + accumulatedDelta)) // { // SnapshotPoint? urPoint = MappingHelper.MapDownToFirstMatchNoTrack(new SnapshotPoint(afterSourceSnapshot, renormalizedSourcePosition), // (buffer) => (buffer is TextBuffer), // renormalizedSourcePosition == priorSpan.Start.Position + accumulatedDelta // ? PositionAffinity.Successor // : PositionAffinity.Predecessor); // Debug.Assert(urPoint.HasValue); // bool added = urPoints.Add(urPoint.Value); // Debug.Assert(added); // if this is false we are sorta hosed - we already handled this point // if (BufferGroup.Tracing) // { // Debug.WriteLine("UR-Point [custom]" + urPoint.Value.ToString()); // } // } // return true; //} //else //{ // return false; //} } private static StringRebuilder InsertionLiesInPermissiveInclusiveSpan(ITextSnapshot afterSourceSnapshot, SnapshotSpan rawSpan, Span deletionSpan, int sourcePosition, int renormalizedSourcePosition, ITextChange incomingChange, HashSet urPoints) { bool leadingInclusiveGrowth = sourcePosition < rawSpan.Start && deletionSpan.End >= rawSpan.Start; if (sourcePosition == rawSpan.Start || leadingInclusiveGrowth) { SnapshotPoint? urPoint = MappingHelper.MapDownToFirstMatchNoTrack(new SnapshotPoint(afterSourceSnapshot, renormalizedSourcePosition), (buffer) => (buffer is TextBuffer), PositionAffinity.Successor); Debug.Assert(urPoint.HasValue); bool added = urPoints.Add(urPoint.Value); Debug.Assert(added); // if this is false we are sorta hosed - we already handled this point if (BufferGroup.Tracing) { Debug.WriteLine("UR-Point [inclusive:start]" + urPoint.Value.ToString()); } return TextChange.NewStringRebuilder(incomingChange); } else if (sourcePosition == rawSpan.End) { SnapshotPoint? urPoint = MappingHelper.MapDownToFirstMatchNoTrack(new SnapshotPoint(afterSourceSnapshot, renormalizedSourcePosition), (buffer) => (buffer is TextBuffer), PositionAffinity.Predecessor); Debug.Assert(urPoint.HasValue); bool added = urPoints.Add(urPoint.Value); Debug.Assert(added); // if this is false we are sorta hosed - we already handled this point if (BufferGroup.Tracing) { Debug.WriteLine("UR-Point [inclusive:end]" + urPoint.Value.ToString()); } return TextChange.NewStringRebuilder(incomingChange); } else { return rawSpan.Contains(sourcePosition) ? TextChange.NewStringRebuilder(incomingChange) : StringRebuilder.Empty; } } /// /// Fetch for this span if it already exists, or create a new one. /// private static SpanAdjustment GetAdjustment(SortedDictionary spanAdjustments, int spanPosition) { SpanAdjustment adjust; if (!spanAdjustments.TryGetValue(spanPosition, out adjust)) { adjust = new SpanAdjustment(); spanAdjustments.Add(spanPosition, adjust); } return adjust; } #endregion #region Change Application /// /// Given the set of changes to apply to this buffer, compute the set of changes to apply to its /// source buffers. These edit objects are managed by the buffer group, which will decide when to /// apply them. /// private void ComputeSourceEdits(FrugalList changes) { foreach (TextChange change in changes) { if (change.OldLength > 0 && change.NewLength == 0) { // the change is a deletion IList sourceDeletionSpans = this.currentProjectionSnapshot.MapToSourceSnapshots(new Span(change.NewPosition, change.OldLength)); foreach (SnapshotSpan sourceDeletionSpan in sourceDeletionSpans) { DeleteFromSource(sourceDeletionSpan); } } else if (change.OldLength > 0 && change.NewLength > 0) { // the change is a replacement ReadOnlyCollection allSourceReplacementSpans = this.currentProjectionSnapshot.MapReplacementSpanToSourceSnapshots (new Span(change.OldPosition, change.OldLength), (this.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) == 0 ? this.literalBuffer : null); //Filter out replacement spans that are read-only (since we couldn't edit them in any case). FrugalList sourceReplacementSpans = new FrugalList(); foreach (var s in allSourceReplacementSpans) { if (!s.Snapshot.TextBuffer.IsReadOnly(s.Span, true)) sourceReplacementSpans.Add(s); } Debug.Assert(sourceReplacementSpans.Count > 0); // if replacement is on read-only buffers, the read only check will have already caught it if (sourceReplacementSpans.Count == 1) { ReplaceInSource(sourceReplacementSpans[0], change.NewText, 0 + change.MasterChangeOffset); } else { // the replacement hits the boundary of source spans int[] insertionSizes = new int[sourceReplacementSpans.Count]; if (this.resolver != null) { SnapshotSpan projectionReplacementSpan = new SnapshotSpan(this.currentProjectionSnapshot, change.OldPosition, change.OldLength); this.resolver.FillInReplacementSizes(projectionReplacementSpan, new ReadOnlyCollection(sourceReplacementSpans), change.NewText, insertionSizes); if (BufferGroup.Tracing) { Debug.WriteLine(string.Format(System.Globalization.CultureInfo.CurrentCulture, "## Seam Replacement @:{0}", projectionReplacementSpan)); for (int s = 0; s < sourceReplacementSpans.Count; ++s) { Debug.WriteLine(string.Format(System.Globalization.CultureInfo.CurrentCulture, "## {0,4}: {1}", insertionSizes[s], sourceReplacementSpans[s])); } Debug.WriteLine(string.Format(System.Globalization.CultureInfo.CurrentCulture, "## Replacement Text:'{0}'", TextUtilities.Escape(change.NewText))); } } insertionSizes[insertionSizes.Length - 1] = int.MaxValue; int pos = 0; for (int i = 0; i < insertionSizes.Length; ++i) { // contend with any old garbage that the client passed back. int insertionSize = Math.Min(insertionSizes[i], change.NewLength - pos); if (insertionSize > 0) { ReplaceInSource(sourceReplacementSpans[i], TextChange.ChangeNewSubstring(change, pos, insertionSize), pos + change.MasterChangeOffset); pos += insertionSize; } else if (sourceReplacementSpans[i].Length > 0) { DeleteFromSource(sourceReplacementSpans[i]); } } } } else { Debug.Assert(change.OldLength == 0 && change.NewLength > 0); // the change is an insertion ReadOnlyCollection allSourceInsertionPoints = this.currentProjectionSnapshot.MapInsertionPointToSourceSnapshots (change.NewPosition, (this.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) == 0 ? this.literalBuffer : null); Debug.Assert(allSourceInsertionPoints.Count > 0); // if insertion point is between two literal spans, the read only check will have already caught it //Filter out replacement spans that are read-only (since we couldn't edit them in any case). FrugalList sourceInsertionPoints = new FrugalList(); foreach (var p in allSourceInsertionPoints) { if (!p.Snapshot.TextBuffer.IsReadOnly(p.Position, true)) sourceInsertionPoints.Add(p); } Debug.Assert(sourceInsertionPoints.Count > 0); // if insertion point is between only read-only buffers, the read only check will have already caught it if (sourceInsertionPoints.Count == 1) { // the insertion point is unambiguous InsertInSource(sourceInsertionPoints[0], change.NewText, 0 + change.MasterChangeOffset); } else { // the insertion is at the boundary of source spans int[] insertionSizes = new int[sourceInsertionPoints.Count]; if (this.resolver != null) { this.resolver.FillInInsertionSizes(new SnapshotPoint(this.currentProjectionSnapshot, change.NewPosition), new ReadOnlyCollection(sourceInsertionPoints), change.NewText, insertionSizes); } // if resolver was not provided, we just use zeros for the insertion sizes, which will push the entire insertion // into the last slot. insertionSizes[insertionSizes.Length - 1] = int.MaxValue; int pos = 0; for (int i = 0; i < insertionSizes.Length; ++i) { // contend with any old garbage that the client passed back. int size = Math.Min(insertionSizes[i], change.NewLength - pos); if (size > 0) { InsertInSource(sourceInsertionPoints[i], change._newText.GetText(new Span(pos, size)), pos + change.MasterChangeOffset); pos += size; if (pos == change.NewLength) { break; // inserted text is used up, whether we've visited all of the insertionSizes or not } } } } } } // defer interpretation of events that will be raised by source buffers as we make these edits this.editApplicationInProgress = true; } #endregion #region Snapshots protected override BaseSnapshot TakeSnapshot() { List newSourceSpans = new List(this.sourceSpans.Count); foreach (ITrackingSpan sourceSpan in this.sourceSpans) { // since we are on the main thread, we can safely just look at current snapshots newSourceSpans.Add(sourceSpan.GetSpan(sourceSpan.TextBuffer.CurrentSnapshot)); } this.currentProjectionSnapshot = MakeSnapshot(newSourceSpans); return this.currentProjectionSnapshot; } private ProjectionSnapshot TakeStaticSnapshot(List newSourceSpans) { // this form of snapshot uses the same source snapshots as current snapshot rather than current snapshots return MakeSnapshot(newSourceSpans); } private ProjectionSnapshot MakeSnapshot(List newSourceSpans) { return new ProjectionSnapshot(this, this.currentVersion, this.builder, newSourceSpans); } protected override StringRebuilder GetDoppelgangerBuilder() { ITextBuffer doppelBottom; if (Properties.TryGetProperty("IdentityMapping", out doppelBottom)) { var snapshot = doppelBottom.CurrentSnapshot; return BufferFactoryService.StringRebuilderFromSnapshotAndSpan(snapshot, new Span(0, snapshot.Length)); } if (this.sourceSpans.Count == 1) { var sourceSpan = this.sourceSpans[0]; var source = sourceSpan.GetSpan(sourceSpan.TextBuffer.CurrentSnapshot); if ((source.Length == this.currentVersion.Length) && (source.Snapshot is BaseSnapshot)) { // We're mapped to a single span that is equal to the entire contents the source buffer. return BufferFactoryService.StringRebuilderFromSnapshotSpan(source); } } return null; } public override IProjectionSnapshot CurrentSnapshot { get { return this.currentProjectionSnapshot; } } protected override BaseProjectionSnapshot CurrentBaseSnapshot { get { return this.currentProjectionSnapshot; } } #endregion #region Events internal event EventHandler SourceBuffersChangedImmediate; internal event EventHandler SourceSpansChangedImmediate; public event EventHandler SourceBuffersChanged; public event EventHandler SourceSpansChanged; #endregion } }