rd-net/RdFramework/Text/Impl/RdTextBuffer.cs (254 lines of code) (raw):

using System; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using JetBrains.Collections.Viewable; using JetBrains.Diagnostics; using JetBrains.Lifetimes; using JetBrains.Rd.Base; using JetBrains.Rd.Impl; using JetBrains.Rd.Text.Impl.Intrinsics; using JetBrains.Rd.Text.Intrinsics; namespace JetBrains.Rd.Text.Impl { public class RdTextBuffer : RdDelegateBase<RdTextBufferState>, ITextBufferWithTypingSession { public bool IsMaster { get; } private readonly RdChangeOrigin myLocalOrigin; private readonly List<RdTextBufferChange> myChangesToConfirmOrRollback; private readonly IViewableProperty<RdTextChange> myTextChanged; private TextBufferTypingSession? myActiveSession; public bool IsCommitting => myActiveSession != null && myActiveSession.IsCommitting; public TextBufferVersion BufferVersion { get; private set; } private Lifetime myBindLifetime = Lifetime.Terminated; /// <summary> /// Slave of the text buffer supports a list of changes that were introduced locally and can be rolled back when master buffer reports incompatible change /// </summary> public RdTextBuffer() : this(new RdTextBufferState()) { } public RdTextBuffer(RdTextBufferState state, bool isMaster = false) : base(state) { IsMaster = isMaster; myTextChanged = new ViewableProperty<RdTextChange>(); myChangesToConfirmOrRollback = new List<RdTextBufferChange>(); BufferVersion = TextBufferVersion.InitVersion; myLocalOrigin = IsMaster ? RdChangeOrigin.Master : RdChangeOrigin.Slave; // disabling mastering, text buffer must resolve conflicts by itself ((RdProperty<RdTextBufferChange>) Delegate.Changes).IsMaster = false; } public override void PreBind(Lifetime lf, IRdDynamic parent, string name) { myBindLifetime = lf; base.PreBind(lf, parent, name); } public override void Bind() { var bindLifetime = myBindLifetime; base.Bind(); Delegate.Changes.AdviseNotNull(bindLifetime, change => { if (change.Origin == myLocalOrigin) return; if (myActiveSession != null && myActiveSession.TryPushRemoteChange(change)) { return; } ReceiveChange(change); }); Delegate.AssertedMasterText.Compose(bindLifetime, Delegate.AssertedSlaveText, Tuple.Create).Advise(bindLifetime, tuple => { var m = tuple.Item1; var s = tuple.Item2; if (m.MasterVersion == s.MasterVersion && m.SlaveVersion == s.SlaveVersion && m.Text != s.Text) { throw new Exception($"Master and Slave texts are different.\nMaster:\n{m.Text}\nSlave:\n{s.Text}"); } }); } private void ReceiveChange(RdTextBufferChange rdTextBufferChange) { var newVersion = rdTextBufferChange.Version; var change = rdTextBufferChange.Change; var side = rdTextBufferChange.Origin; if (Mode.IsAssertion) Assertion.Assert(side != myLocalOrigin, "side != mySide"); var masterVersionRemote = newVersion.Master; var slaveVersionRemote = newVersion.Slave; if (change.Kind == RdTextChangeKind.Reset) { ClearState(); } else if (change.Kind == RdTextChangeKind.PromoteVersion) { if (Mode.IsAssertion) Assertion.Assert(!IsMaster); BufferVersion = newVersion; return; } else { if (IsMaster) { if (Mode.IsAssertion) Assertion.Assert(myChangesToConfirmOrRollback.Count == 0); if (masterVersionRemote != BufferVersion.Master) { // reject the change. we've already sent overriding change. return; } } else { if (slaveVersionRemote != BufferVersion.Slave) { // rollback the changes and notify external subscribers // don't need to update history here - all reverted changes were already stored before on 'fire' stage foreach (var ch in Enumerable.Reverse(myChangesToConfirmOrRollback)) { if (ch.Version.Slave <= slaveVersionRemote) break; myTextChanged.SetValue(ch.Change.Reverse()); } myChangesToConfirmOrRollback.Clear(); } else { // confirm the changes queue. myChangesToConfirmOrRollback.Clear(); } } } BufferVersion = newVersion; if (!IsMaster || myActiveSession == null || !myActiveSession.IsCommitting) { myTextChanged.SetValue(change); } } private void ClearState() { myChangesToConfirmOrRollback.Clear(); } public IScheduler? Scheduler { get; set; } public void Fire(RdTextChange change) { if (Mode.IsAssertion) Assertion.Assert(Delegate.IsBound || BufferVersion == TextBufferVersion.InitVersion); if (Delegate.IsBound) this.GetProtoOrThrow().Scheduler.AssertThread(); if (IsMaster && myActiveSession != null && myActiveSession.IsCommitting) { return; } IncrementBufferVersion(); var bufferChange = new RdTextBufferChange(BufferVersion, myLocalOrigin, change); if (change.Kind == RdTextChangeKind.Reset) { ClearState(); } else if (!IsMaster) { myChangesToConfirmOrRollback.Add(bufferChange); } myActiveSession?.TryPushLocalChange(change); Delegate.Changes.SetValue(bufferChange); } private void IncrementBufferVersion() { BufferVersion = IsMaster ? BufferVersion.IncrementMaster() : BufferVersion.IncrementSlave(); } public void Advise(Lifetime lifetime, Action<RdTextChange> change) { Assertion.Assert(Delegate.IsBound); this.GetProtoOrThrow().Scheduler.AssertThread(); myTextChanged.Advise(lifetime, change); } public void Reset(string text) { Fire(new RdTextChange(RdTextChangeKind.Reset, 0, "", text, text.Length)); } public void AssertState(string allText) { var assertion = new RdAssertion(BufferVersion.Master, BufferVersion.Slave, allText); if (IsMaster) Delegate.AssertedMasterText.SetValue(assertion); else Delegate.AssertedSlaveText.SetValue(assertion); } public ITypingSession StartTypingSession(Lifetime lifetime) { Assertion.Assert(myActiveSession == null); Assertion.Assert(lifetime.IsAlive); myActiveSession = new TextBufferTypingSession(this); lifetime.OnTermination(() => myActiveSession = null); return myActiveSession; } public class TextBufferTypingSession : ITypingSession { private enum State { Opened, Committing } private readonly RdTextBuffer myBuffer; private readonly List<RdTextBufferChange> myRemoteChanges = new List<RdTextBufferChange>(); private readonly List<RdTextChange> myLocalChanges = new List<RdTextChange>(); private readonly TextBufferVersion myVersionBeforeOpening; private readonly TextBufferVersion myInitialBufferVersion; private State myState = State.Opened; public readonly Signal<RdTextChange> OnLocalChange = new Signal<RdTextChange>(); public readonly Signal<RdTextChange> OnRemoteChange = new Signal<RdTextChange>(); public TextBufferTypingSession(RdTextBuffer buffer) { myBuffer = buffer; myInitialBufferVersion = myBuffer.BufferVersion; if (buffer.IsMaster) { myVersionBeforeOpening = myBuffer.BufferVersion; myBuffer.Delegate.VersionBeforeTypingSession.Value = myInitialBufferVersion; } else { myVersionBeforeOpening = buffer.Delegate.VersionBeforeTypingSession.Value; } } public bool IsCommitting => myState == State.Committing; public bool TryPushLocalChange(RdTextChange change) { if (myState != State.Opened) return false; OnLocalChange.Fire(change); myLocalChanges.Add(change); return true; } private static int CompareVersions(TextBufferVersion first, TextBufferVersion second) { if (first.Master != second.Master) return first.Master - second.Master; return first.Slave - second.Slave; } public bool TryPushRemoteChange(RdTextBufferChange change) { if (myState != State.Opened) return false; if (!myBuffer.IsMaster && change.Version.Master <= myVersionBeforeOpening.Master) return false; if (myBuffer.IsMaster && CompareVersions(change.Version, myVersionBeforeOpening) <= 0) return false; OnRemoteChange.Fire(change.Change); myRemoteChanges.Add(change); return true; } public void CommitRemoteChanges() { StartCommitRemoteVersion(); FinishCommitRemoteVersion(); } public void FinishCommitRemoteVersion() { if (!myBuffer.IsMaster) { for (var i = myLocalChanges.Count - 1; i >= 0; i--) { var change = myLocalChanges[i]; myBuffer.myTextChanged.SetValue(change.Reverse()); } } myBuffer.BufferVersion = myInitialBufferVersion; foreach (var bufferChange in myRemoteChanges) { myBuffer.ReceiveChange(bufferChange); } } public void StartCommitRemoteVersion() { Assertion.Assert(myState == State.Opened); myState = State.Committing; } } } }