From 36034eec28dcaf46f286d153ac19ce65c47dc612 Mon Sep 17 00:00:00 2001 From: Gadfly Date: Thu, 28 May 2026 14:12:49 +0800 Subject: [PATCH] enhance: share TextMate registry across editors to reduce memory usage (#2396) Each text editor previously created its own `RegistryOptionsWrapper` with an independent grammar cache, causing redundant TextMate grammar loading when viewing files of the same type in different editors. Over long sessions this accumulated hundreds of megabytes of duplicated grammar data in the managed heap. - Make `RegistryOptionsWrapper` a lazy singleton per theme (dark/light) so all editors share one grammar cache. - Move `LastScope` tracking from the shared wrapper to a per-editor `ref string` parameter to avoid cross-editor state conflicts. - Add `DisposeInstallation` helper for consistent cleanup. --- src/Models/TextMateHelper.cs | 36 +++++++++++++++----- src/Views/AIAssistant.axaml.cs | 8 ++--- src/Views/Blame.axaml.cs | 8 ++--- src/Views/CommandLogContentPresenter.cs | 8 ++--- src/Views/MergeConflictEditor.axaml.cs | 10 +++--- src/Views/RevisionFileContentViewer.axaml.cs | 14 +++----- src/Views/SelfUpdate.axaml.cs | 8 ++--- src/Views/TextDiffView.axaml.cs | 14 +++----- 8 files changed, 53 insertions(+), 53 deletions(-) diff --git a/src/Models/TextMateHelper.cs b/src/Models/TextMateHelper.cs index 38c0aaa72..d2d14eec6 100644 --- a/src/Models/TextMateHelper.cs +++ b/src/Models/TextMateHelper.cs @@ -84,8 +84,6 @@ private record ExtraGrammar(string Scope, List Extensions, string File) public class RegistryOptionsWrapper(ThemeName defaultTheme) : IRegistryOptions { - public string LastScope { get; set; } = string.Empty; - public IRawTheme GetTheme(string scopeName) => _backend.GetTheme(scopeName); public IRawTheme GetDefaultTheme() => _backend.GetDefaultTheme(); public IRawTheme LoadTheme(ThemeName name) => _backend.LoadTheme(name); @@ -98,11 +96,20 @@ public class RegistryOptionsWrapper(ThemeName defaultTheme) : IRegistryOptions public static class TextMateHelper { + private static RegistryOptionsWrapper s_darkRegistry; + private static RegistryOptionsWrapper s_lightRegistry; + + private static RegistryOptionsWrapper GetRegistry(bool isDark) + { + if (isDark) + return s_darkRegistry ??= new RegistryOptionsWrapper(ThemeName.DarkPlus); + return s_lightRegistry ??= new RegistryOptionsWrapper(ThemeName.LightPlus); + } + public static TextMate.Installation CreateForEditor(TextEditor editor) { - return editor.InstallTextMate(Application.Current?.ActualThemeVariant == ThemeVariant.Dark ? - new RegistryOptionsWrapper(ThemeName.DarkPlus) : - new RegistryOptionsWrapper(ThemeName.LightPlus)); + var isDark = Application.Current?.ActualThemeVariant == ThemeVariant.Dark; + return editor.InstallTextMate(GetRegistry(isDark == true)); } public static void SetThemeByApp(TextMate.Installation installation) @@ -114,18 +121,29 @@ public static void SetThemeByApp(TextMate.Installation installation) } } - public static void SetGrammarByFileName(TextMate.Installation installation, string filePath) + public static void SetGrammarByFileName(TextMate.Installation installation, string filePath, ref string lastScope) { if (installation is { RegistryOptions: RegistryOptionsWrapper reg } && !string.IsNullOrEmpty(filePath)) { + // Registry options are shared across editors, so the last grammar scope must stay editor-local. var scope = reg.GetScope(filePath); - if (reg.LastScope != scope) + if (lastScope != scope) { - reg.LastScope = scope; - installation.SetGrammar(reg.GetScope(filePath)); + lastScope = scope; + installation.SetGrammar(scope); GC.Collect(); } } } + + public static void DisposeInstallation(ref TextMate.Installation installation, ref string lastScope, bool collectGarbage = false) + { + installation?.Dispose(); + installation = null; + lastScope = string.Empty; + + if (collectGarbage) + GC.Collect(); + } } } diff --git a/src/Views/AIAssistant.axaml.cs b/src/Views/AIAssistant.axaml.cs index c49e407f9..c622f0ab4 100644 --- a/src/Views/AIAssistant.axaml.cs +++ b/src/Views/AIAssistant.axaml.cs @@ -63,7 +63,7 @@ protected override void OnLoaded(RoutedEventArgs e) if (_textMate == null) { _textMate = Models.TextMateHelper.CreateForEditor(this); - Models.TextMateHelper.SetGrammarByFileName(_textMate, "README.md"); + Models.TextMateHelper.SetGrammarByFileName(_textMate, "README.md", ref _lastGrammarScope); TextArea.TextView.LineTransformers.Add(new LineStyleTransformer()); } } @@ -75,10 +75,7 @@ protected override void OnUnloaded(RoutedEventArgs e) TextArea.TextView.ContextRequested -= OnTextViewContextRequested; if (_textMate != null) - { - _textMate.Dispose(); - _textMate = null; - } + Models.TextMateHelper.DisposeInstallation(ref _textMate, ref _lastGrammarScope); GC.Collect(); } @@ -125,6 +122,7 @@ private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs } private TextMate.Installation _textMate = null; + private string _lastGrammarScope = string.Empty; } public partial class AIAssistant : ChromelessWindow diff --git a/src/Views/Blame.axaml.cs b/src/Views/Blame.axaml.cs index 8bc36160c..4137abf6c 100644 --- a/src/Views/Blame.axaml.cs +++ b/src/Views/Blame.axaml.cs @@ -375,10 +375,7 @@ protected override void OnUnloaded(RoutedEventArgs e) TextArea.TextView.VisualLinesChanged -= OnTextViewVisualLinesChanged; if (_textMate != null) - { - _textMate.Dispose(); - _textMate = null; - } + Models.TextMateHelper.DisposeInstallation(ref _textMate, ref _lastGrammarScope); } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) @@ -388,7 +385,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang if (change.Property == FileProperty) { if (File is { Length: > 0 }) - Models.TextMateHelper.SetGrammarByFileName(_textMate, File); + Models.TextMateHelper.SetGrammarByFileName(_textMate, File, ref _lastGrammarScope); } if (change.Property == BlameDataProperty) { @@ -454,6 +451,7 @@ private void OnTextViewVisualLinesChanged(object sender, EventArgs e) } private TextMate.Installation _textMate = null; + private string _lastGrammarScope = string.Empty; private string _highlight = string.Empty; } diff --git a/src/Views/CommandLogContentPresenter.cs b/src/Views/CommandLogContentPresenter.cs index 99dc6d13d..64a06eaa3 100644 --- a/src/Views/CommandLogContentPresenter.cs +++ b/src/Views/CommandLogContentPresenter.cs @@ -107,7 +107,7 @@ protected override void OnLoaded(RoutedEventArgs e) if (_textMate == null) { _textMate = Models.TextMateHelper.CreateForEditor(this); - Models.TextMateHelper.SetGrammarByFileName(_textMate, "Log.log"); + Models.TextMateHelper.SetGrammarByFileName(_textMate, "Log.log", ref _lastGrammarScope); TextArea.TextView.LineTransformers.Add(new LineStyleTransformer()); } } @@ -117,10 +117,7 @@ protected override void OnUnloaded(RoutedEventArgs e) base.OnUnloaded(e); if (_textMate != null) - { - _textMate.Dispose(); - _textMate = null; - } + Models.TextMateHelper.DisposeInstallation(ref _textMate, ref _lastGrammarScope); GC.Collect(); } @@ -152,5 +149,6 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang } private TextMate.Installation _textMate = null; + private string _lastGrammarScope = string.Empty; } } diff --git a/src/Views/MergeConflictEditor.axaml.cs b/src/Views/MergeConflictEditor.axaml.cs index 8c7b2621d..1dd565b80 100644 --- a/src/Views/MergeConflictEditor.axaml.cs +++ b/src/Views/MergeConflictEditor.axaml.cs @@ -310,7 +310,7 @@ protected override void OnLoaded(RoutedEventArgs e) _textMate = Models.TextMateHelper.CreateForEditor(this); if (!string.IsNullOrEmpty(FileName)) - Models.TextMateHelper.SetGrammarByFileName(_textMate, FileName); + Models.TextMateHelper.SetGrammarByFileName(_textMate, FileName, ref _lastGrammarScope); TextArea.TextView.ContextRequested += OnTextViewContextRequested; TextArea.TextView.PointerEntered += OnTextViewPointerChanged; @@ -331,10 +331,7 @@ protected override void OnUnloaded(RoutedEventArgs e) TextArea.TextView.VisualLinesChanged -= OnTextViewVisualLinesChanged; if (_textMate != null) - { - _textMate.Dispose(); - _textMate = null; - } + Models.TextMateHelper.DisposeInstallation(ref _textMate, ref _lastGrammarScope); base.OnUnloaded(e); } @@ -346,7 +343,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang if (change.Property == LinesProperty) UpdateContent(); else if (change.Property == FileNameProperty) - Models.TextMateHelper.SetGrammarByFileName(_textMate, FileName); + Models.TextMateHelper.SetGrammarByFileName(_textMate, FileName, ref _lastGrammarScope); else if (change.Property.Name == nameof(ActualThemeVariant) && change.NewValue != null) Models.TextMateHelper.SetThemeByApp(_textMate); else if (change.Property == SelectedChunkProperty) @@ -539,6 +536,7 @@ private void UpdateSelectedChunkPosition(ViewModels.MergeConflictEditor vm, doub } private TextMate.Installation _textMate; + private string _lastGrammarScope = string.Empty; private ScrollViewer _scrollViewer; } diff --git a/src/Views/RevisionFileContentViewer.axaml.cs b/src/Views/RevisionFileContentViewer.axaml.cs index eddb7b95c..5511eb927 100644 --- a/src/Views/RevisionFileContentViewer.axaml.cs +++ b/src/Views/RevisionFileContentViewer.axaml.cs @@ -67,10 +67,7 @@ protected override void OnUnloaded(RoutedEventArgs e) TextArea.TextView.ContextRequested -= OnTextViewContextRequested; if (_textMate != null) - { - _textMate.Dispose(); - _textMate = null; - } + Models.TextMateHelper.DisposeInstallation(ref _textMate, ref _lastGrammarScope); GC.Collect(); } @@ -82,7 +79,7 @@ protected override void OnDataContextChanged(EventArgs e) if (DataContext is Models.RevisionTextFile source) { Text = source.Content; - Models.TextMateHelper.SetGrammarByFileName(_textMate, source.FileName); + Models.TextMateHelper.SetGrammarByFileName(_textMate, source.FileName, ref _lastGrammarScope); ScrollToHome(); } else @@ -139,19 +136,18 @@ private void UpdateTextMate() _textMate ??= Models.TextMateHelper.CreateForEditor(this); if (DataContext is Models.RevisionTextFile file) - Models.TextMateHelper.SetGrammarByFileName(_textMate, file.FileName); + Models.TextMateHelper.SetGrammarByFileName(_textMate, file.FileName, ref _lastGrammarScope); } else if (_textMate != null) { - _textMate.Dispose(); - _textMate = null; - GC.Collect(); + Models.TextMateHelper.DisposeInstallation(ref _textMate, ref _lastGrammarScope, true); TextArea.TextView.Redraw(); } } private TextMate.Installation _textMate = null; + private string _lastGrammarScope = string.Empty; } public partial class RevisionFileContentViewer : UserControl diff --git a/src/Views/SelfUpdate.axaml.cs b/src/Views/SelfUpdate.axaml.cs index 97f1bc772..0783c6fae 100644 --- a/src/Views/SelfUpdate.axaml.cs +++ b/src/Views/SelfUpdate.axaml.cs @@ -34,7 +34,7 @@ protected override void OnLoaded(RoutedEventArgs e) if (_textMate == null) { _textMate = Models.TextMateHelper.CreateForEditor(this); - Models.TextMateHelper.SetGrammarByFileName(_textMate, "README.md"); + Models.TextMateHelper.SetGrammarByFileName(_textMate, "README.md", ref _lastGrammarScope); } } @@ -43,10 +43,7 @@ protected override void OnUnloaded(RoutedEventArgs e) base.OnUnloaded(e); if (_textMate != null) - { - _textMate.Dispose(); - _textMate = null; - } + Models.TextMateHelper.DisposeInstallation(ref _textMate, ref _lastGrammarScope); GC.Collect(); } @@ -60,6 +57,7 @@ protected override void OnDataContextChanged(EventArgs e) } private TextMate.Installation _textMate = null; + private string _lastGrammarScope = string.Empty; } public partial class SelfUpdate : ChromelessWindow diff --git a/src/Views/TextDiffView.axaml.cs b/src/Views/TextDiffView.axaml.cs index 94940e743..4addf2d0b 100644 --- a/src/Views/TextDiffView.axaml.cs +++ b/src/Views/TextDiffView.axaml.cs @@ -550,10 +550,7 @@ protected override void OnUnloaded(RoutedEventArgs e) TextArea.TextView.VisualLinesChanged -= OnTextViewVisualLinesChanged; if (_textMate != null) - { - _textMate.Dispose(); - _textMate = null; - } + Models.TextMateHelper.DisposeInstallation(ref _textMate, ref _lastGrammarScope); } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) @@ -576,7 +573,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang } else if (change.Property == FileNameProperty) { - Models.TextMateHelper.SetGrammarByFileName(_textMate, FileName); + Models.TextMateHelper.SetGrammarByFileName(_textMate, FileName, ref _lastGrammarScope); } else if (change.Property.Name == nameof(ActualThemeVariant) && change.NewValue != null) { @@ -773,16 +770,14 @@ private void UpdateTextMate() TextArea.TextView.LineTransformers.Remove(_lineStyleTransformer); _textMate = Models.TextMateHelper.CreateForEditor(this); TextArea.TextView.LineTransformers.Add(_lineStyleTransformer); - Models.TextMateHelper.SetGrammarByFileName(_textMate, FileName); + Models.TextMateHelper.SetGrammarByFileName(_textMate, FileName, ref _lastGrammarScope); } } else { if (_textMate != null) { - _textMate.Dispose(); - _textMate = null; - GC.Collect(); + Models.TextMateHelper.DisposeInstallation(ref _textMate, ref _lastGrammarScope, true); TextArea.TextView.Redraw(); } @@ -885,6 +880,7 @@ private async Task CopyWithoutIndicatorsAsync() private bool _execSizeChanged; private TextMate.Installation _textMate; + private string _lastGrammarScope = string.Empty; private TextLocation _lastSelectStart = TextLocation.Empty; private TextLocation _lastSelectEnd = TextLocation.Empty; private LineStyleTransformer _lineStyleTransformer;