private static TextChangeType CheckChangeInsideString(TextChangeContext context, out IAstNode node, out PositionType positionType) { positionType = context.EditorTree.AstRoot.GetPositionNode(context.NewStart, out node); if (positionType == PositionType.Token) { TokenNode tokenNode = node as TokenNode; Debug.Assert(tokenNode != null); if (tokenNode.Token.TokenType == RTokenType.String) { if (context.OldText.IndexOfAny(_stringSensitiveCharacters) >= 0) { return(TextChangeType.Structure); } if (context.NewText.IndexOfAny(_stringSensitiveCharacters) >= 0) { return(TextChangeType.Structure); } context.ChangedNode = node; return(TextChangeType.Token); } } return(TextChangeType.Trivial); }
public static void DetermineChangeType(TextChangeContext change) { var changeType = CheckChangeInsideComment(change); change.PendingChanges.TextChangeType = MathExtensions.Max(changeType, change.PendingChanges.TextChangeType); switch (change.PendingChanges.TextChangeType) { case TextChangeType.Comment: return; case TextChangeType.Trivial: changeType = CheckChangeInsideString(change, out var node, out var positionType); change.PendingChanges.TextChangeType = MathExtensions.Max(changeType, change.PendingChanges.TextChangeType); if (change.PendingChanges.TextChangeType == TextChangeType.Token) { return; } if (node != null && change.PendingChanges.TextChangeType == TextChangeType.Trivial) { changeType = CheckWhiteSpaceChange(change, node, positionType); change.PendingChanges.TextChangeType = MathExtensions.Max(changeType, change.PendingChanges.TextChangeType); if (change.PendingChanges.TextChangeType == TextChangeType.Trivial) { return; } } break; } change.PendingChanges.TextChangeType = TextChangeType.Structure; change.PendingChanges.FullParseRequired = true; }
public static void DetermineChangeType(TextChangeContext change) { change.PendingChanges.TextChangeType |= CheckChangeInsideComment(change); if (change.PendingChanges.TextChangeType == TextChangeType.Comment) { return; } else if (change.PendingChanges.TextChangeType == TextChangeType.Trivial) { IAstNode node; PositionType positionType; change.PendingChanges.TextChangeType |= CheckChangeInsideString(change, out node, out positionType); if (change.PendingChanges.TextChangeType == TextChangeType.Token) { return; } else if (node != null && change.PendingChanges.TextChangeType == TextChangeType.Trivial) { change.PendingChanges.TextChangeType |= CheckWhiteSpaceChange(change, node, positionType); if (change.PendingChanges.TextChangeType == TextChangeType.Trivial) { return; } } } change.PendingChanges.TextChangeType = TextChangeType.Structure; change.PendingChanges.FullParseRequired = true; }
/// <summary> /// Text buffer change event handler. Performs analysis of the change. /// If change is trivial, such as change in whitespace (excluding line /// breaks that in R may be sensistive), simply applies the changes /// by shifting tree elements. If some elements get deleted or otherwise /// damaged, removes them from the tree right away. Non-trivial changes /// are queued for background parsing which starts on next on idle. /// Methond must be called on a main thread only, typically from an event /// handler that receives text buffer change events. /// </summary> internal void OnTextChanges(IReadOnlyCollection <TextChangeEventArgs> textChanges) { if (Thread.CurrentThread.ManagedThreadId != _ownerThreadId) { throw new ThreadStateException("Method should only be called on the main thread"); } _editorTree.FireOnUpdatesPending(textChanges); if (UpdatesSuspended) { this.TextBufferChangedSinceSuspend = true; _pendingChanges.FullParseRequired = true; } else { foreach (TextChangeEventArgs change in textChanges) { _lastChangeTime = DateTime.UtcNow; var context = new TextChangeContext(_editorTree, change, _pendingChanges); // No need to analyze changes if full parse is already pending if (!_pendingChanges.FullParseRequired) { TextChangeAnalyzer.DetermineChangeType(context); } ProcessChange(context); } } }
private static TextChangeType CheckWhiteSpaceChange(TextChangeContext context, IAstNode node, PositionType positionType) { context.ChangedNode = node; if (string.IsNullOrWhiteSpace(context.OldText) && string.IsNullOrWhiteSpace(context.NewText)) { // In R there is no line continuation so expression may change when user adds or deletes line breaks. bool lineBreakSensitive = node is If; if (lineBreakSensitive) { string oldLineText = context.OldTextProvider.GetText(new TextRange(context.OldStart, context.OldLength)); string newLineText = context.NewTextProvider.GetText(new TextRange(context.NewStart, context.NewLength)); if (oldLineText.IndexOfAny(CharExtensions.LineBreakChars) >= 0 || newLineText.IndexOfAny(CharExtensions.LineBreakChars) >= 0) { return(TextChangeType.Structure); } } // Change inside token node is destructive: consider adding space inside an indentifier if (!IsChangeDestructiveForChildNodes(node, context.OldRange)) { return(TextChangeType.Trivial); } } return(TextChangeType.Structure); }
private static TextChangeType CheckChangeInsideComment(TextChangeContext context) { var comments = context.EditorTree.AstRoot.Comments; IReadOnlyList <int> affectedComments = comments.GetItemsContainingInclusiveEnd(context.NewStart); if (affectedComments.Count == 0) { return(TextChangeType.Trivial); } if (affectedComments.Count > 1) { return(TextChangeType.Structure); } // Make sure user is not deleting leading # effectively // destroying the comment RToken comment = comments[affectedComments[0]]; if (comment.Start == context.NewStart && context.OldLength > 0) { return(TextChangeType.Structure); } // The collection will return a match if the comment starts // at the requested location. However, the change is not // inside the comment if it's at the comment start and // the old length of the change is zero. if (comment.Start == context.NewStart && context.OldLength == 0) { if (context.NewText.IndexOf('#') < 0) { context.ChangedComment = comment; return(TextChangeType.Comment); } } if (context.NewText.IndexOfAny(CharExtensions.LineBreakChars) >= 0) { return(TextChangeType.Structure); } // The change is not safe if old or new text contains line breaks // as in R comments runs to the end of the line and deleting // line break at the end of the comment may bring code into // the comment range and change the entire file structure. if (context.OldText.IndexOfAny(CharExtensions.LineBreakChars) >= 0) { return(TextChangeType.Structure); } context.ChangedComment = comment; return(TextChangeType.Comment); }
/// <summary> /// Handles non-trivial changes like changes that delete elements, /// change identifier names, introducing new braces: changes /// that cannot be handled without background parse. /// </summary> private void ProcessComplexChange(TextChangeContext context) { // Cancel background parse if it is running Cancel(); TextChange textChange = new TextChange() { OldTextProvider = context.OldTextProvider, NewTextProvider = context.NewTextProvider }; try { // Get write lock since there may be concurrent readers // of the tree. Note that there are no concurrent writers // since changes can only come from a background parser // and are always applied from the main thread. _editorTree.AcquireWriteLock(); if (_pendingChanges.FullParseRequired) { // When full parse is required, change is like replace the entire file textChange.OldRange = TextRange.FromBounds(0, context.OldText.Length); textChange.NewRange = TextRange.FromBounds(0, context.NewText.Length); // Remove damaged elements if any and reflect text change. // Although we are invalidating the AST next, old copy will // be kept for operations that may need it such as smart indent. bool elementsChanged; _editorTree.InvalidateInRange(_editorTree.AstRoot, context.OldRange, out elementsChanged); _editorTree.NotifyTextChange(context.NewStart, context.OldLength, context.NewLength); // Invalidate will store existing AST as previous snapshot // and create temporary empty AST until the next async parse. _editorTree.Invalidate(); } else { textChange.OldRange = context.OldRange; textChange.NewRange = context.NewRange; DeleteAndShiftElements(context); Debug.Assert(_editorTree.AstRoot.Children.Count > 0); } _pendingChanges.Combine(textChange); _pendingChanges.Version = TextBuffer != null ? TextBuffer.CurrentSnapshot.Version.VersionNumber : 1; UpdateTreeTextSnapshot(); } finally { // Lock must be released before firing events otherwise we may hang _editorTree.ReleaseWriteLock(); } _editorTree.FireOnUpdateCompleted(TreeUpdateType.NodesRemoved); }
private void ProcessChange(TextChangeContext context) { _editorTree.FireOnUpdateBegin(); if (_pendingChanges.IsSimpleChange) { ProcessSimpleChange(context); } else { ProcessComplexChange(context); } }
/// <summary> /// Handles non-trivial changes like changes that delete elements, /// change identifier names, introducing new braces: changes /// that cannot be handled without background parse. /// </summary> private void ProcessComplexChange(TextChangeContext context) { // Cancel background parse if it is running Cancel(); var c = context.PendingChanges; try { // Get write lock since there may be concurrent readers // of the tree. Note that there are no concurrent writers // since changes can only come from a background parser // and are always applied from the main thread. _editorTree.AcquireWriteLock(); int start, oldLength, newLength; if (Changes.FullParseRequired) { // When full parse is required, change is like replace the entire file start = 0; oldLength = c.OldTextProvider.Length; newLength = c.NewTextProvider.Length; // Remove damaged elements if any and reflect text change. // the tree remains usable outside of the damaged scope. _editorTree.InvalidateInRange(c.OldRange); _editorTree.NotifyTextChange(c.Start, c.OldLength, c.NewLength); } else { start = c.Start; oldLength = c.OldLength; newLength = c.NewLength; DeleteAndShiftElements(context); Debug.Assert(_editorTree.AstRoot.Children.Count > 0); } var ttc = new TreeTextChange(start, oldLength, newLength, _editorTree.BufferSnapshot, EditorBuffer.CurrentSnapshot); Changes.Combine(ttc); Changes.Version = EditorBuffer?.CurrentSnapshot?.Version ?? 1; _editorTree.BufferSnapshot = EditorBuffer.CurrentSnapshot; } finally { // Lock must be released before firing events otherwise we may hang _editorTree.ReleaseWriteLock(); } _editorTree.FireOnUpdateCompleted(TreeUpdateType.NodesRemoved); }
/// <summary> /// Handles non-trivial changes like changes that delete elements, /// change identifier names, introducing new braces: changes /// that cannot be handled without background parse. /// </summary> private void ProcessComplexChange(TextChangeContext context) { // Cancel background parse if it is running Cancel(); TextChange textChange = new TextChange() { OldTextProvider = context.OldTextProvider, NewTextProvider = context.NewTextProvider }; try { // Get write lock since there may be concurrent readers // of the tree. Note that there are no concurrent writers // since changes can only come from a background parser // and are always applied from the main thread. _editorTree.AcquireWriteLock(); if (_pendingChanges.FullParseRequired) { // When full parse is required, change is like replace the entire file textChange.OldRange = TextRange.FromBounds(0, context.OldText.Length); textChange.NewRange = TextRange.FromBounds(0, context.NewText.Length); // Remove all elements from the tree _editorTree.Invalidate(); } else { textChange.OldRange = context.OldRange; textChange.NewRange = context.NewRange; DeleteAndShiftElements(context); Debug.Assert(_editorTree.AstRoot.Children.Count > 0); } _pendingChanges.Combine(textChange); _pendingChanges.Version = TextBuffer != null ? TextBuffer.CurrentSnapshot.Version.VersionNumber : 1; UpdateTreeTextSnapshot(); } finally { // Lock must be released before firing events otherwise we may hang _editorTree.ReleaseWriteLock(); } _editorTree.FireOnUpdateCompleted(TreeUpdateType.NodesRemoved); }
private static TextChangeType CheckChangeInsideComment(TextChangeContext context) { var comments = context.EditorTree.AstRoot.Comments; IReadOnlyList<int> affectedComments = comments.GetItemsContainingInclusiveEnd(context.NewStart); if (affectedComments.Count == 0) { return TextChangeType.Trivial; } if (affectedComments.Count > 1) { return TextChangeType.Structure; } // Make sure user is not deleting leading # effectively // destroying the comment RToken comment = comments[affectedComments[0]]; if (comment.Start == context.NewStart && context.OldLength > 0) { return TextChangeType.Structure; } // The collection will return a match if the comment starts // at the requested location. However, the change is not // inside the comment if it's at the comment start and // the old length of the change is zero. if (comment.Start == context.NewStart && context.OldLength == 0) { if (context.NewText.IndexOf('#') < 0) { context.ChangedComment = comment; return TextChangeType.Comment; } } if (context.NewText.IndexOfAny(CharExtensions.LineBreakChars) >= 0) { return TextChangeType.Structure; } // The change is not safe if old or new text contains line breaks // as in R comments runs to the end of the line and deleting // line break at the end of the comment may bring code into // the comment range and change the entire file structure. if (context.OldText.IndexOfAny(CharExtensions.LineBreakChars) >= 0) { return TextChangeType.Structure; } context.ChangedComment = comment; return TextChangeType.Comment; }
// internal for unit tests internal bool DeleteAndShiftElements(TextChangeContext context) { if (Thread.CurrentThread.ManagedThreadId != _ownerThreadId) { throw new ThreadStateException("Method should only be called on the main thread"); } TextChange textChange = context.PendingChanges; var changeType = textChange.TextChangeType; bool elementsChanged = false; if (changeType == TextChangeType.Structure) { IAstNode changedElement = context.ChangedNode; int start = context.NewStart; // We delete change nodes unless node is a token node // which range can be modified such as string or comment var positionType = PositionType.Undefined; if (changedElement != null) { IAstNode node; positionType = changedElement.GetPositionNode(context.NewStart, out node); } bool deleteElements = (context.OldLength > 0) || (positionType != PositionType.Token); // In case of delete or replace we need to invalidate elements that were // damaged by the delete operation. We need to remove elements and their keys // so they won't be found by validator and incremental change analysis // will not be looking at zombies. if (deleteElements) { _pendingChanges.FullParseRequired = _editorTree.InvalidateInRange(_editorTree.AstRoot, context.OldRange, out elementsChanged); } } _editorTree.NotifyTextChange(context.NewStart, context.OldLength, context.NewLength); return(elementsChanged); }
private static TextChangeType CheckWhiteSpaceChange(TextChangeContext context, IAstNode node, PositionType positionType) { context.ChangedNode = node; if (string.IsNullOrWhiteSpace(context.OldText) && string.IsNullOrWhiteSpace(context.NewText)) { // In R there is no line continuation so expression may change when user adds or deletes line breaks. bool lineBreakSensitive = node is If; if (lineBreakSensitive) { string oldLineText = context.OldTextProvider.GetText(new TextRange(context.OldStart, context.OldLength)); string newLineText = context.NewTextProvider.GetText(new TextRange(context.NewStart, context.NewLength)); if (oldLineText.IndexOfAny(CharExtensions.LineBreakChars) >= 0 || newLineText.IndexOfAny(CharExtensions.LineBreakChars) >= 0) { return TextChangeType.Structure; } } // Change inside token node is destructive: consider adding space inside an indentifier if (!IsChangeDestructiveForChildNodes(node, context.OldRange)) { return TextChangeType.Trivial; } } return TextChangeType.Structure; }
// internal for unit tests internal bool DeleteAndShiftElements(TextChangeContext context) { Check.InvalidOperation(() => Thread.CurrentThread.ManagedThreadId == _ownerThreadId, _threadCheckMessage); var change = context.PendingChanges; var changeType = context.PendingChanges.TextChangeType; var elementsChanged = false; if (changeType == TextChangeType.Structure) { var changedElement = context.ChangedNode; // We delete change nodes unless node is a token node // which range can be modified such as string or comment var positionType = PositionType.Undefined; if (changedElement != null) { positionType = changedElement.GetPositionNode(change.Start, out IAstNode node); } var deleteElements = change.OldLength > 0 || positionType != PositionType.Token; // In case of delete or replace we need to invalidate elements that were // damaged by the delete operation. We need to remove elements so they // won't be found by validator and it won't be looking at zombies. if (deleteElements) { Changes.FullParseRequired = true; elementsChanged = _editorTree.InvalidateInRange(change.OldRange); } } _editorTree.NotifyTextChange(change.Start, change.OldLength, change.NewLength); return(elementsChanged); }
private static bool IsLineBreakSensitive(TextChangeContext context, IAstNode node) { if (node is If) { return(true); } var position = context.PendingChanges.Start; var candidate = node.Root.GetNodeOfTypeFromPosition <If>(position); if (candidate != null) { return(true); } // Check if line break is added to removed on a line with closing } of 'if' // so we can full-parse if 'else' position changes relatively to the if var snapshot = context.EditorTree.EditorBuffer.CurrentSnapshot; var text = snapshot.GetLineFromPosition(context.PendingChanges.Start).GetText(); // We need to find if any position in the line belong to `if` scope // if there is 'else' the same line return(FindKeyword("if", text) || FindKeyword("else", text)); }
/// <summary> /// Handles simple (safe) changes. /// </summary> private void ProcessSimpleChange(TextChangeContext context) { bool elementsRemoved = false; try { _editorTree.AcquireWriteLock(); elementsRemoved = DeleteAndShiftElements(context); UpdateTreeTextSnapshot(); // If no elements were invalidated and full parse is not required, clear pending changes if (!elementsRemoved) { ClearChanges(); } } finally { _editorTree.ReleaseWriteLock(); } if (!elementsRemoved) { if (context.ChangedNode != null || context.PendingChanges.TextChangeType == TextChangeType.Trivial) { _editorTree.FireOnPositionsOnlyChanged(); } _editorTree.FireOnUpdateCompleted(TreeUpdateType.PositionsOnly); } else { _editorTree.FireOnUpdateCompleted(TreeUpdateType.NodesRemoved); } DebugTree.VerifyTree(_editorTree); Debug.Assert(_editorTree.AstRoot.Children.Count > 0); }
/// <summary> /// Text buffer change event handler. Performs analysis of the change. /// If change is trivial, such as change in whitespace (excluding line /// breaks that in R may be sensistive), simply applies the changes /// by shifting tree elements. If some elements get deleted or otherwise /// damaged, removes them from the tree right away. Non-trivial changes /// are queued for background parsing which starts on next on idle. /// Methond must be called on a main thread only, typically from an event /// handler that receives text buffer change events. /// </summary> internal void OnTextChange(TextChangeEventArgs e) { Check.InvalidOperation(() => Thread.CurrentThread.ManagedThreadId == _ownerThreadId, _threadCheckMessage); _editorTree.FireOnUpdatesPending(); if (UpdatesSuspended) { TextBufferChangedSinceSuspend = true; Changes.FullParseRequired = true; } else { _lastChangeTime = DateTime.UtcNow; var context = new TextChangeContext(_editorTree, new TreeTextChange(e.Change), Changes); // No need to analyze changes if full parse is already pending if (!Changes.FullParseRequired) { TextChangeAnalyzer.DetermineChangeType(context); } ProcessChange(context); } }
/// <summary> /// Text buffer change event handler. Performs analysis of the change. /// If change is trivial, such as change in whitespace (excluding line /// breaks that in R may be sensistive), simply applies the changes /// by shifting tree elements. If some elements get deleted or otherwise /// damaged, removes them from the tree right away. Non-trivial changes /// are queued for background parsing which starts on next on idle. /// Methond must be called on a main thread only, typically from an event /// handler that receives text buffer change events. /// </summary> internal void OnTextChanges(IReadOnlyCollection<TextChangeEventArgs> textChanges) { if (Thread.CurrentThread.ManagedThreadId != _ownerThreadId) throw new ThreadStateException("Method should only be called on the main thread"); _editorTree.FireOnUpdatesPending(textChanges); if (UpdatesSuspended) { this.TextBufferChangedSinceSuspend = true; _pendingChanges.FullParseRequired = true; } else { foreach (TextChangeEventArgs change in textChanges) { _lastChangeTime = DateTime.UtcNow; var context = new TextChangeContext(_editorTree, change, _pendingChanges); // No need to analyze changes if full parse is already pending if (!_pendingChanges.FullParseRequired) { TextChangeAnalyzer.DetermineChangeType(context); } ProcessChange(context); } } }
private static TextChangeType CheckChangeInsideString(TextChangeContext context, out IAstNode node, out PositionType positionType) { positionType = context.EditorTree.AstRoot.GetPositionNode(context.NewStart, out node); if (positionType == PositionType.Token) { TokenNode tokenNode = node as TokenNode; Debug.Assert(tokenNode != null); if (tokenNode.Token.TokenType == RTokenType.String) { if (context.OldText.IndexOfAny(_stringSensitiveCharacters) >= 0) { return TextChangeType.Structure; } if (context.NewText.IndexOfAny(_stringSensitiveCharacters) >= 0) { return TextChangeType.Structure; } context.ChangedNode = node; return TextChangeType.Token; } } return TextChangeType.Trivial; }
// internal for unit tests internal bool DeleteAndShiftElements(TextChangeContext context) { if (Thread.CurrentThread.ManagedThreadId != _ownerThreadId) throw new ThreadStateException("Method should only be called on the main thread"); TextChange textChange = context.PendingChanges; var changeType = textChange.TextChangeType; bool elementsChanged = false; if (changeType == TextChangeType.Structure) { IAstNode changedElement = context.ChangedNode; int start = context.NewStart; // We delete change nodes unless node is a token node // which range can be modified such as string or comment var positionType = PositionType.Undefined; if (changedElement != null) { IAstNode node; positionType = changedElement.GetPositionNode(context.NewStart, out node); } bool deleteElements = (context.OldLength > 0) || (positionType != PositionType.Token); // In case of delete or replace we need to invalidate elements that were // damaged by the delete operation. We need to remove elements and their keys // so they won't be found by validator and incremental change analysis // will not be looking at zombies. if (deleteElements) { _pendingChanges.FullParseRequired = _editorTree.InvalidateInRange(_editorTree.AstRoot, context.OldRange, out elementsChanged); } } _editorTree.NotifyTextChange(context.NewStart, context.OldLength, context.NewLength); return elementsChanged; }