Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .runsettings
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
<CodeCoverage>
<ModulePaths>
<Include>
<ModulePath>.*\.dll$</ModulePath>
<ModulePath>avaloniaedit.textmate.dll$</ModulePath>
<ModulePath>avaloniaedit.dll$</ModulePath>
</Include>
<Exclude>
<ModulePath>.*Tests\.dll$</ModulePath>
Expand Down
1 change: 1 addition & 0 deletions AvaloniaEdit.slnx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<Solution>
<Folder Name="/Solution Items/">
<File Path=".runsettings" />
<File Path="azure-pipelines.yml" />
<File Path="Directory.Build.props" />
<File Path="Directory.Packages.props" />
Expand Down
4 changes: 3 additions & 1 deletion src/AvaloniaEdit.TextMate/TextMate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,16 @@ public class Installation : IDisposable
private readonly Registry _textMateRegistry;
private readonly TextEditor _editor;
private Action<Exception> _exceptionHandler;
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2213:Disposable fields should be disposed",
Justification = "Disposed in Dispose(bool) and DisposeEditorModel methods")]
private TextEditorModel _editorModel;
private IGrammar _grammar;
private TMModel _tmModel;
private TextMateColoringTransformer _transformer;
private readonly bool _ownsTransformer;
private ReadOnlyDictionary<string, string> _themeColorsDictionary;
public IRegistryOptions RegistryOptions { get; }
public TextEditorModel EditorModel { get { return Volatile.Read(ref _editorModel); } }
public TextEditorModel EditorModel => Volatile.Read(ref _editorModel);

public event EventHandler<Installation> AppliedTheme;

Expand Down
8 changes: 5 additions & 3 deletions src/AvaloniaEdit/Editing/EditingCommandHandler.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
Expand Down Expand Up @@ -197,6 +197,8 @@ private static void TransformSelectedSegments(Action<TextArea, ISegment> transfo
{
foreach (var segment in segments.Reverse())
{
// Use Enumerable.Reverse explicitly to avoid a breaking change in C# 14 where Reverse() now resolves to MemoryExtensions.Reverse instead of Enumerable.Reverse
// see https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/breaking-changes/compiler%20breaking%20changes%20-%20dotnet%2010#enumerablereverse
foreach (var writableSegment in System.Linq.Enumerable.Reverse(textArea.GetDeletableSegments(segment)))
{
transformSegment(textArea, writableSegment);
Expand Down Expand Up @@ -416,7 +418,7 @@ private static bool CopySelectedText(TextArea textArea)
text = TextUtilities.NormalizeNewLines(text, Environment.NewLine);


var df = new DataTransfer();
using var df = new DataTransfer();
var item = new DataTransferItem();
item.Set(DataFormat.Text, text);
df.Add(item);
Expand Down Expand Up @@ -482,7 +484,7 @@ private static bool CopyWholeLine(TextArea textArea, DocumentLine line)
//textArea.RaiseEvent(copyingEventArgs);
//if (copyingEventArgs.CommandCancelled)
// return false;
var df = new DataTransfer();
using var df = new DataTransfer();
var item = new DataTransferItem();
item.Set(DataFormat.Text, text);
df.Add(item);
Expand Down
4 changes: 2 additions & 2 deletions src/AvaloniaEdit/Highlighting/HighlightingManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public IHighlightingDefinition GetDefinition(string name)
{
lock (_lockObj)
{
return _highlightingsByName.TryGetValue(name, out var rh) ? rh : null;
return _highlightingsByName.GetValueOrDefault(name);
}
}

Expand All @@ -152,7 +152,7 @@ public IHighlightingDefinition GetDefinitionByExtension(string extension)
{
lock (_lockObj)
{
return _highlightingsByExtension.TryGetValue(extension, out var rh) ? rh : null;
return _highlightingsByExtension.GetValueOrDefault(extension);
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/AvaloniaEdit/Highlighting/Xshd/V2Loader.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
Expand Down Expand Up @@ -48,6 +48,7 @@ internal static class V2Loader
// }
//}

[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "XmlReader is disposed by caller")]
public static XshdSyntaxDefinition LoadDefinition(XmlReader reader, bool skipValidation)
{
reader = HighlightingLoader.GetValidatingReader(reader, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -406,12 +406,12 @@ public HighlightingRuleSet GetNamedRuleSet(string name)
{
if (string.IsNullOrEmpty(name))
return MainRuleSet;
return _ruleSetDict.TryGetValue(name, out var r) ? r : null;
return _ruleSetDict.GetValueOrDefault(name);
}

public HighlightingColor GetNamedColor(string name)
{
return _colorDict.TryGetValue(name, out var c) ? c : null;
return _colorDict.GetValueOrDefault(name);
}

public IEnumerable<HighlightingColor> NamedHighlightingColors => _colorDict.Values;
Expand Down
181 changes: 162 additions & 19 deletions src/AvaloniaEdit/Snippets/InsertionContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
Expand All @@ -17,10 +17,12 @@
// DEALINGS IN THE SOFTWARE.

using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using AvaloniaEdit.Document;
using AvaloniaEdit.Editing;
using AvaloniaEdit.Utils;

namespace AvaloniaEdit.Snippets
{
Expand All @@ -38,6 +40,12 @@ private enum Status
Deactivated
}

/// <summary>
/// Pre-compiled, SIMD-accelerated search values for newline characters
/// used during snippet text insertion.
/// </summary>
private static readonly SearchValues<char> _newlineChars = SearchValues.Create("\r\n");

private Status _currentStatus = Status.Insertion;

/// <summary>
Expand Down Expand Up @@ -116,28 +124,161 @@ public int StartPosition
/// This method will add the current indentation to every line in <paramref name="text"/> and will
/// replace newlines with the expected newline for the document.
/// </summary>
/// <remarks>
/// <para>
/// This method uses a two-phase algorithm that is semantically equivalent to the
/// original implementation:
/// </para>
/// <list type="number">
/// <item>
/// <description>
/// <b>Phase 1 - Tab expansion:</b> Replaces tab characters with the configured
/// indentation string using <see cref="string.Replace(string, string)"/>, which
/// leverages the runtime's native SIMD-optimized implementation. When
/// <see cref="Tab"/> equals <c>"\t"</c> (identity replacement), this phase is
/// skipped entirely.
/// </description>
/// </item>
/// <item>
/// <description>
/// <b>Phase 2 - Newline normalization:</b> Scans the fully tab-expanded text for
/// newline characters using <see cref="SearchValues{T}"/> (SIMD-accelerated) and
/// replaces them with <see cref="LineTerminator"/> + <see cref="Indentation"/>. Uses
/// a stack-allocated <see cref="ValueStringBuilder"/> that falls back to
/// <see cref="ArrayPool{T}"/> for large inputs, minimizing heap allocations.
/// </description>
/// </item>
/// </list>
/// <para>
/// The two-phase approach ensures that if <see cref="Tab"/> contains newline characters
/// (possible via a custom <see cref="TextEditorOptions"/> subclass), those newlines are
/// correctly processed by the newline normalization phase - including CRLF pairs that
/// span the boundary between expanded tab content and adjacent source text.
/// </para>
/// <para>
/// The entire transformed result is inserted into the document with a single
/// <see cref="TextDocument.Insert(int, string)"/> call within a
/// <see cref="TextDocument.RunUpdate"/> scope, eliminating per-line substring
/// allocations and reducing document change, anchor update, and undo-grouping overhead.
/// </para>
/// <para>
/// A fast path is provided for text that contains no special characters, avoiding
/// all intermediate allocations entirely.
/// </para>
/// </remarks>
public void InsertText(string text)
{
if (_currentStatus != Status.Insertion)
throw new InvalidOperationException();

text = text?.Replace("\t", Tab) ?? throw new ArgumentNullException(nameof(text));
text = text ?? throw new ArgumentNullException(nameof(text));

using (Document.RunUpdate())
// Fast path: if Tab is identity ("\t") and text has no newlines,
// or if Tab is not identity and text has no tabs or newlines,
// insert directly with zero extra allocations
bool isTabIdentity = Tab == "\t";

ReadOnlySpan<char> span = text.AsSpan();
bool hasNewlines = span.IndexOfAny(_newlineChars) >= 0;
bool hasTabs = !isTabIdentity && span.Contains('\t');

if (!hasNewlines && !hasTabs)
{
using (Document.RunUpdate())
{
Document.Insert(InsertionPosition, text);
InsertionPosition += text.Length;
}
return;
}

// Phase 1: Tab expansion
// Equivalent to the original's: text = text.Replace("\t", Tab)
//
// Uses the runtime's native string.Replace which is SIMD-optimized
// and significantly faster than a managed StringBuilder loop for
// this operation. When Tab == "\t" (identity), this is skipped.
//
// This must happen before newline normalization so that any newline
// characters within Tab are visible to Phase 2. This is what makes
// CRLF boundary merging work correctly - Phase 2 sees the fully
// expanded text as one contiguous stream.
string expanded = hasTabs ? text.Replace("\t", Tab) : text;
ReadOnlySpan<char> expandedSpan = expanded.AsSpan();

// Check if newlines exist in the expanded text (tab expansion may
// have introduced newlines if Tab contains \r or \n)
if (expandedSpan.IndexOfAny(_newlineChars) < 0)
{
int textOffset = 0;
SimpleSegment segment;
while ((segment = NewLineFinder.NextNewLine(text, textOffset)) != SimpleSegment.Invalid)
// Tab expansion produced text with no newlines - insert directly
using (Document.RunUpdate())
{
string insertString = text.Substring(textOffset, segment.Offset - textOffset)
+ LineTerminator + Indentation;
Document.Insert(InsertionPosition, insertString);
InsertionPosition += insertString.Length;
textOffset = segment.EndOffset;
Document.Insert(InsertionPosition, expanded);
InsertionPosition += expanded.Length;
}
string remainingInsertString = text.Substring(textOffset);
Document.Insert(InsertionPosition, remainingInsertString);
InsertionPosition += remainingInsertString.Length;
return;
}

// Phase 2: Newline normalization
// Equivalent to the original's NewLineFinder.NextNewLine loop
//
// Uses a ValueStringBuilder backed by stackalloc for small inputs
// (typical snippets < 512 chars) with ArrayPool<char> fallback for
// larger inputs. This eliminates the StringBuilder object allocation
// and its internal char[] buffer allocation on the heap for the
// common case.
//
// Because this operates on the fully tab-expanded text, CRLF pairs
// that span tab-expansion boundaries are correctly handled as single
// units - identical to the original implementation.
string result;
const int optimizedStackBufferSize = 512;
using (var vsb = new ValueStringBuilder(stackalloc char[optimizedStackBufferSize]))
{
int pos = 0;
while (pos < expandedSpan.Length)
{
int index = expandedSpan[pos..].IndexOfAny(_newlineChars);
if (index < 0)
{
// No more newlines - append remaining text
vsb.Append(expandedSpan[pos..]);
break;
}

// Append literal text before the newline
if (index > 0)
{
vsb.Append(expandedSpan.Slice(pos, index));
}

pos += index;
char c = expandedSpan[pos];

// Handle \r\n as a single unit, or \r / \n individually
if (c == '\r' && pos + 1 < expandedSpan.Length && expandedSpan[pos + 1] == '\n')
{
pos += 2; // skip \r\n
}
else
{
pos++; // skip \r or \n
}

// Replace with document's line terminator + indentation
vsb.Append(LineTerminator);
vsb.Append(Indentation);
}

// Single document insert wrapped in RunUpdate() for update-scope
// and undo-grouping parity with the original implementation
result = vsb.ToString();
}

using (Document.RunUpdate())
{
Document.Insert(InsertionPosition, result);
InsertionPosition += result.Length;
}
}

Expand All @@ -158,6 +299,7 @@ public void RegisterActiveElement(SnippetElement owner, IActiveElement element)
throw new ArgumentNullException(nameof(element));
if (_currentStatus != Status.Insertion)
throw new InvalidOperationException();

_elementMap.Add(owner, element);
_registeredElements.Add(element);
}
Expand All @@ -169,7 +311,8 @@ public IActiveElement GetActiveElement(SnippetElement owner)
{
if (owner == null)
throw new ArgumentNullException(nameof(owner));
return _elementMap.TryGetValue(owner, out var element) ? element : null;

return _elementMap.GetValueOrDefault(owner);
}

/// <summary>
Expand All @@ -188,8 +331,8 @@ public void RaiseInsertionCompleted(EventArgs e)
{
if (_currentStatus != Status.Insertion)
throw new InvalidOperationException();
if (e == null)
e = EventArgs.Empty;

e ??= EventArgs.Empty;

_currentStatus = Status.RaisingInsertionCompleted;
int endPosition = InsertionPosition;
Expand Down Expand Up @@ -238,8 +381,8 @@ public void Deactivate(SnippetEventArgs e)
return;
if (_currentStatus != Status.Interactive)
throw new InvalidOperationException("Cannot call Deactivate() until RaiseInsertionCompleted() has finished.");
if (e == null)
e = new SnippetEventArgs(DeactivateReason.Unknown);

e ??= new SnippetEventArgs(DeactivateReason.Unknown);

TextDocumentWeakEventManager.UpdateFinished.RemoveHandler(Document, OnUpdateFinished);
_currentStatus = Status.RaisingDeactivated;
Expand Down
Loading