diff --git a/src/AvaloniaEdit.TextMate/TextMateColoringTransformer.cs b/src/AvaloniaEdit.TextMate/TextMateColoringTransformer.cs
index 3370efd8..5b3ead1a 100644
--- a/src/AvaloniaEdit.TextMate/TextMateColoringTransformer.cs
+++ b/src/AvaloniaEdit.TextMate/TextMateColoringTransformer.cs
@@ -285,6 +285,10 @@ protected override void TransformLine(DocumentLine line, ITextRunConstructionCon
if (model == null || document == null || theme == null || brushes == null)
return;
+ // for long lines, do not tokenize and colorize for performance reasons
+ if (line.Length > VisualLine.LENGTH_LIMIT)
+ return;
+
int lineNumber = line.LineNumber;
var tokens = model.GetLineTokens(lineNumber - 1);
diff --git a/src/AvaloniaEdit/Editing/EditingCommandHandler.cs b/src/AvaloniaEdit/Editing/EditingCommandHandler.cs
index edd70a07..348f2f70 100644
--- a/src/AvaloniaEdit/Editing/EditingCommandHandler.cs
+++ b/src/AvaloniaEdit/Editing/EditingCommandHandler.cs
@@ -248,7 +248,7 @@ public static void OnTab(object target, RoutedEventArgs args)
while (true)
{
var offset = current.Offset;
- if (textArea.ReadOnlySectionProvider.CanInsert(offset))
+ if (textArea.CanInsert(offset))
textArea.Document.Replace(offset, 0, textArea.Options.IndentationString,
OffsetChangeMappingType.KeepAnchorBeforeInsertion);
if (current == end)
@@ -499,7 +499,7 @@ private static void CanPaste(object target, CanExecuteRoutedEventArgs args)
var textArea = GetTextArea(target);
if (textArea is { Document: not null })
{
- args.CanExecute = textArea.ReadOnlySectionProvider.CanInsert(textArea.Caret.Offset);
+ args.CanExecute = textArea.CanInsert(textArea.Caret.Offset);
args.Handled = true;
}
}
diff --git a/src/AvaloniaEdit/Editing/EmptySelection.cs b/src/AvaloniaEdit/Editing/EmptySelection.cs
index ad569833..aa3ba210 100644
--- a/src/AvaloniaEdit/Editing/EmptySelection.cs
+++ b/src/AvaloniaEdit/Editing/EmptySelection.cs
@@ -68,7 +68,7 @@ public override void ReplaceSelectionWithText(string newText)
newText = AddSpacesIfRequired(newText, TextArea.Caret.Position, TextArea.Caret.Position);
if (newText.Length > 0)
{
- if (TextArea.ReadOnlySectionProvider.CanInsert(TextArea.Caret.Offset))
+ if (TextArea.CanInsert(TextArea.Caret.Offset))
{
TextArea.Document.Insert(TextArea.Caret.Offset, newText);
}
diff --git a/src/AvaloniaEdit/Editing/LongLineEditProtection.cs b/src/AvaloniaEdit/Editing/LongLineEditProtection.cs
new file mode 100644
index 00000000..9e27875c
--- /dev/null
+++ b/src/AvaloniaEdit/Editing/LongLineEditProtection.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+
+using AvaloniaEdit.Document;
+using AvaloniaEdit.Rendering;
+
+namespace AvaloniaEdit.Editing
+{
+ ///
+ /// Disallow edits in the collapsed tail of long lines (see ):
+ ///
+ internal static class LongLineEditProtection
+ {
+ internal static bool CanInsert(TextDocument document, int offset)
+ {
+ if (document == null)
+ return true;
+
+ DocumentLine line = document.GetLineByOffset(offset);
+ return offset - line.Offset < VisualLine.LENGTH_LIMIT;
+ }
+
+ internal static IEnumerable GetDeletableSegments(
+ TextDocument document, ISegment segment)
+ {
+ if (segment == null)
+ throw new ArgumentNullException(nameof(segment));
+
+ if (document == null)
+ {
+ yield return segment;
+ yield break;
+ }
+
+ if (segment.Length == 0)
+ {
+ if (CanInsert(document, segment.Offset))
+ yield return segment;
+ yield break;
+ }
+
+ int deletableStart = segment.Offset;
+ int segmentEnd = segment.EndOffset;
+
+ DocumentLine line = document.GetLineByOffset(segment.Offset);
+ while (line != null && line.Offset < segmentEnd)
+ {
+ if (line.Length > VisualLine.LENGTH_LIMIT)
+ {
+ // protect the hidden text plus the line delimiter; deleting the
+ // delimiter would merge the next line into the hidden region
+ int protectedStart = line.Offset + VisualLine.LENGTH_LIMIT;
+ int protectedEnd = line.Offset + line.TotalLength;
+
+ if (protectedStart < segmentEnd && protectedEnd > deletableStart)
+ {
+ if (protectedStart > deletableStart)
+ {
+ yield return new SimpleSegment(
+ deletableStart, protectedStart - deletableStart);
+ }
+
+ deletableStart = Math.Min(protectedEnd, segmentEnd);
+ }
+ }
+
+ line = line.NextLine;
+ }
+
+ if (deletableStart < segmentEnd)
+ yield return new SimpleSegment(deletableStart, segmentEnd - deletableStart);
+ }
+ }
+}
diff --git a/src/AvaloniaEdit/Editing/RectangleSelection.cs b/src/AvaloniaEdit/Editing/RectangleSelection.cs
index 06593c21..0944731a 100644
--- a/src/AvaloniaEdit/Editing/RectangleSelection.cs
+++ b/src/AvaloniaEdit/Editing/RectangleSelection.cs
@@ -328,7 +328,7 @@ private void ReplaceSingleLineText(TextArea textArea, SelectionSegment lineSegme
{
if (lineSegment.Length == 0)
{
- if (newText.Length > 0 && textArea.ReadOnlySectionProvider.CanInsert(lineSegment.StartOffset))
+ if (newText.Length > 0 && textArea.CanInsert(lineSegment.StartOffset))
{
newText = AddSpacesIfRequired(newText, new TextViewPosition(_document.GetLocation(lineSegment.StartOffset), lineSegment.StartVisualColumn), new TextViewPosition(_document.GetLocation(lineSegment.EndOffset), lineSegment.EndVisualColumn));
textArea.Document.Insert(lineSegment.StartOffset, newText);
diff --git a/src/AvaloniaEdit/Editing/SelectionMouseHandler.cs b/src/AvaloniaEdit/Editing/SelectionMouseHandler.cs
index ef022d32..9e526607 100644
--- a/src/AvaloniaEdit/Editing/SelectionMouseHandler.cs
+++ b/src/AvaloniaEdit/Editing/SelectionMouseHandler.cs
@@ -188,7 +188,7 @@ DragDropEffects GetEffect(DragEventArgs e)
{
TextArea.Caret.Position = new TextViewPosition(TextArea.Document.GetLocation(offset), visualColumn) { IsAtEndOfLine = isAtEndOfLine };
TextArea.Caret.DesiredXPos = double.NaN;
- if (TextArea.ReadOnlySectionProvider.CanInsert(offset))
+ if (TextArea.CanInsert(offset))
{
if ((e.DragEffects & DragDropEffects.Move) == DragDropEffects.Move
&& (e.KeyModifiers & KeyModifiers.Control) != KeyModifiers.Control)
diff --git a/src/AvaloniaEdit/Editing/TextArea.cs b/src/AvaloniaEdit/Editing/TextArea.cs
index 286c9485..71dcde41 100644
--- a/src/AvaloniaEdit/Editing/TextArea.cs
+++ b/src/AvaloniaEdit/Editing/TextArea.cs
@@ -925,7 +925,7 @@ internal void RemoveSelectedText()
{
foreach (var s in _selection.Segments)
{
- Debug.Assert(!ReadOnlySectionProvider.GetDeletableSegments(s).Any());
+ Debug.Assert(GetDeletableSegments(s).Length == 0);
}
}
#endif
@@ -940,12 +940,20 @@ internal void ReplaceSelectionWithText(string newText)
_selection.ReplaceSelectionWithText(newText);
}
+ internal bool CanInsert(int offset)
+ {
+ return ReadOnlySectionProvider.CanInsert(offset) &&
+ LongLineEditProtection.CanInsert(Document, offset);
+ }
+
internal ISegment[] GetDeletableSegments(ISegment segment)
{
var deletableSegments = ReadOnlySectionProvider.GetDeletableSegments(segment);
if (deletableSegments == null)
throw new InvalidOperationException("ReadOnlySectionProvider.GetDeletableSegments returned null");
- var array = deletableSegments.ToArray();
+ var array = deletableSegments
+ .SelectMany(s => LongLineEditProtection.GetDeletableSegments(Document, s))
+ .ToArray();
var lastIndex = segment.Offset;
foreach (var t in array)
{
diff --git a/src/AvaloniaEdit/Rendering/VisualLine.cs b/src/AvaloniaEdit/Rendering/VisualLine.cs
index 4257a844..fbe290bb 100644
--- a/src/AvaloniaEdit/Rendering/VisualLine.cs
+++ b/src/AvaloniaEdit/Rendering/VisualLine.cs
@@ -41,6 +41,7 @@ namespace AvaloniaEdit.Rendering
///
public sealed class VisualLine
{
+ public const string LENGTH_LIMIT_MARKER = "...";
public const int LENGTH_LIMIT = 3000;
private enum LifetimePhase : byte
@@ -172,7 +173,10 @@ void PerformVisualElementConstruction(IReadOnlyList
// for long lines, do not run the element generators for performance reasons
if (lineLength > LENGTH_LIMIT)
{
- _elements.Add(new VisualLineText(this, lineLength));
+ // shape only the first LENGTH_LIMIT characters and collapse the remainder
+ // into a marker. Otherwise stalls the UI thread.
+ _elements.Add(new VisualLineText(this, LENGTH_LIMIT));
+ _elements.Add(new FormattedTextElement(LENGTH_LIMIT_MARKER, lineLength - LENGTH_LIMIT));
return;
}