Skip to content

Commit 36034ee

Browse files
committed
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.
1 parent 8427859 commit 36034ee

8 files changed

Lines changed: 53 additions & 53 deletions

‎src/Models/TextMateHelper.cs‎

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,6 @@ private record ExtraGrammar(string Scope, List<string> Extensions, string File)
8484

8585
public class RegistryOptionsWrapper(ThemeName defaultTheme) : IRegistryOptions
8686
{
87-
public string LastScope { get; set; } = string.Empty;
88-
8987
public IRawTheme GetTheme(string scopeName) => _backend.GetTheme(scopeName);
9088
public IRawTheme GetDefaultTheme() => _backend.GetDefaultTheme();
9189
public IRawTheme LoadTheme(ThemeName name) => _backend.LoadTheme(name);
@@ -98,11 +96,20 @@ public class RegistryOptionsWrapper(ThemeName defaultTheme) : IRegistryOptions
9896

9997
public static class TextMateHelper
10098
{
99+
private static RegistryOptionsWrapper s_darkRegistry;
100+
private static RegistryOptionsWrapper s_lightRegistry;
101+
102+
private static RegistryOptionsWrapper GetRegistry(bool isDark)
103+
{
104+
if (isDark)
105+
return s_darkRegistry ??= new RegistryOptionsWrapper(ThemeName.DarkPlus);
106+
return s_lightRegistry ??= new RegistryOptionsWrapper(ThemeName.LightPlus);
107+
}
108+
101109
public static TextMate.Installation CreateForEditor(TextEditor editor)
102110
{
103-
return editor.InstallTextMate(Application.Current?.ActualThemeVariant == ThemeVariant.Dark ?
104-
new RegistryOptionsWrapper(ThemeName.DarkPlus) :
105-
new RegistryOptionsWrapper(ThemeName.LightPlus));
111+
var isDark = Application.Current?.ActualThemeVariant == ThemeVariant.Dark;
112+
return editor.InstallTextMate(GetRegistry(isDark == true));
106113
}
107114

108115
public static void SetThemeByApp(TextMate.Installation installation)
@@ -114,18 +121,29 @@ public static void SetThemeByApp(TextMate.Installation installation)
114121
}
115122
}
116123

117-
public static void SetGrammarByFileName(TextMate.Installation installation, string filePath)
124+
public static void SetGrammarByFileName(TextMate.Installation installation, string filePath, ref string lastScope)
118125
{
119126
if (installation is { RegistryOptions: RegistryOptionsWrapper reg } && !string.IsNullOrEmpty(filePath))
120127
{
128+
// Registry options are shared across editors, so the last grammar scope must stay editor-local.
121129
var scope = reg.GetScope(filePath);
122-
if (reg.LastScope != scope)
130+
if (lastScope != scope)
123131
{
124-
reg.LastScope = scope;
125-
installation.SetGrammar(reg.GetScope(filePath));
132+
lastScope = scope;
133+
installation.SetGrammar(scope);
126134
GC.Collect();
127135
}
128136
}
129137
}
138+
139+
public static void DisposeInstallation(ref TextMate.Installation installation, ref string lastScope, bool collectGarbage = false)
140+
{
141+
installation?.Dispose();
142+
installation = null;
143+
lastScope = string.Empty;
144+
145+
if (collectGarbage)
146+
GC.Collect();
147+
}
130148
}
131149
}

‎src/Views/AIAssistant.axaml.cs‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ protected override void OnLoaded(RoutedEventArgs e)
6363
if (_textMate == null)
6464
{
6565
_textMate = Models.TextMateHelper.CreateForEditor(this);
66-
Models.TextMateHelper.SetGrammarByFileName(_textMate, "README.md");
66+
Models.TextMateHelper.SetGrammarByFileName(_textMate, "README.md", ref _lastGrammarScope);
6767
TextArea.TextView.LineTransformers.Add(new LineStyleTransformer());
6868
}
6969
}
@@ -75,10 +75,7 @@ protected override void OnUnloaded(RoutedEventArgs e)
7575
TextArea.TextView.ContextRequested -= OnTextViewContextRequested;
7676

7777
if (_textMate != null)
78-
{
79-
_textMate.Dispose();
80-
_textMate = null;
81-
}
78+
Models.TextMateHelper.DisposeInstallation(ref _textMate, ref _lastGrammarScope);
8279

8380
GC.Collect();
8481
}
@@ -125,6 +122,7 @@ private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs
125122
}
126123

127124
private TextMate.Installation _textMate = null;
125+
private string _lastGrammarScope = string.Empty;
128126
}
129127

130128
public partial class AIAssistant : ChromelessWindow

‎src/Views/Blame.axaml.cs‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -375,10 +375,7 @@ protected override void OnUnloaded(RoutedEventArgs e)
375375
TextArea.TextView.VisualLinesChanged -= OnTextViewVisualLinesChanged;
376376

377377
if (_textMate != null)
378-
{
379-
_textMate.Dispose();
380-
_textMate = null;
381-
}
378+
Models.TextMateHelper.DisposeInstallation(ref _textMate, ref _lastGrammarScope);
382379
}
383380

384381
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
@@ -388,7 +385,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
388385
if (change.Property == FileProperty)
389386
{
390387
if (File is { Length: > 0 })
391-
Models.TextMateHelper.SetGrammarByFileName(_textMate, File);
388+
Models.TextMateHelper.SetGrammarByFileName(_textMate, File, ref _lastGrammarScope);
392389
}
393390
if (change.Property == BlameDataProperty)
394391
{
@@ -454,6 +451,7 @@ private void OnTextViewVisualLinesChanged(object sender, EventArgs e)
454451
}
455452

456453
private TextMate.Installation _textMate = null;
454+
private string _lastGrammarScope = string.Empty;
457455
private string _highlight = string.Empty;
458456
}
459457

‎src/Views/CommandLogContentPresenter.cs‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ protected override void OnLoaded(RoutedEventArgs e)
107107
if (_textMate == null)
108108
{
109109
_textMate = Models.TextMateHelper.CreateForEditor(this);
110-
Models.TextMateHelper.SetGrammarByFileName(_textMate, "Log.log");
110+
Models.TextMateHelper.SetGrammarByFileName(_textMate, "Log.log", ref _lastGrammarScope);
111111
TextArea.TextView.LineTransformers.Add(new LineStyleTransformer());
112112
}
113113
}
@@ -117,10 +117,7 @@ protected override void OnUnloaded(RoutedEventArgs e)
117117
base.OnUnloaded(e);
118118

119119
if (_textMate != null)
120-
{
121-
_textMate.Dispose();
122-
_textMate = null;
123-
}
120+
Models.TextMateHelper.DisposeInstallation(ref _textMate, ref _lastGrammarScope);
124121

125122
GC.Collect();
126123
}
@@ -152,5 +149,6 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
152149
}
153150

154151
private TextMate.Installation _textMate = null;
152+
private string _lastGrammarScope = string.Empty;
155153
}
156154
}

‎src/Views/MergeConflictEditor.axaml.cs‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ protected override void OnLoaded(RoutedEventArgs e)
310310

311311
_textMate = Models.TextMateHelper.CreateForEditor(this);
312312
if (!string.IsNullOrEmpty(FileName))
313-
Models.TextMateHelper.SetGrammarByFileName(_textMate, FileName);
313+
Models.TextMateHelper.SetGrammarByFileName(_textMate, FileName, ref _lastGrammarScope);
314314

315315
TextArea.TextView.ContextRequested += OnTextViewContextRequested;
316316
TextArea.TextView.PointerEntered += OnTextViewPointerChanged;
@@ -331,10 +331,7 @@ protected override void OnUnloaded(RoutedEventArgs e)
331331
TextArea.TextView.VisualLinesChanged -= OnTextViewVisualLinesChanged;
332332

333333
if (_textMate != null)
334-
{
335-
_textMate.Dispose();
336-
_textMate = null;
337-
}
334+
Models.TextMateHelper.DisposeInstallation(ref _textMate, ref _lastGrammarScope);
338335

339336
base.OnUnloaded(e);
340337
}
@@ -346,7 +343,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
346343
if (change.Property == LinesProperty)
347344
UpdateContent();
348345
else if (change.Property == FileNameProperty)
349-
Models.TextMateHelper.SetGrammarByFileName(_textMate, FileName);
346+
Models.TextMateHelper.SetGrammarByFileName(_textMate, FileName, ref _lastGrammarScope);
350347
else if (change.Property.Name == nameof(ActualThemeVariant) && change.NewValue != null)
351348
Models.TextMateHelper.SetThemeByApp(_textMate);
352349
else if (change.Property == SelectedChunkProperty)
@@ -539,6 +536,7 @@ private void UpdateSelectedChunkPosition(ViewModels.MergeConflictEditor vm, doub
539536
}
540537

541538
private TextMate.Installation _textMate;
539+
private string _lastGrammarScope = string.Empty;
542540
private ScrollViewer _scrollViewer;
543541
}
544542

‎src/Views/RevisionFileContentViewer.axaml.cs‎

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,7 @@ protected override void OnUnloaded(RoutedEventArgs e)
6767
TextArea.TextView.ContextRequested -= OnTextViewContextRequested;
6868

6969
if (_textMate != null)
70-
{
71-
_textMate.Dispose();
72-
_textMate = null;
73-
}
70+
Models.TextMateHelper.DisposeInstallation(ref _textMate, ref _lastGrammarScope);
7471

7572
GC.Collect();
7673
}
@@ -82,7 +79,7 @@ protected override void OnDataContextChanged(EventArgs e)
8279
if (DataContext is Models.RevisionTextFile source)
8380
{
8481
Text = source.Content;
85-
Models.TextMateHelper.SetGrammarByFileName(_textMate, source.FileName);
82+
Models.TextMateHelper.SetGrammarByFileName(_textMate, source.FileName, ref _lastGrammarScope);
8683
ScrollToHome();
8784
}
8885
else
@@ -139,19 +136,18 @@ private void UpdateTextMate()
139136
_textMate ??= Models.TextMateHelper.CreateForEditor(this);
140137

141138
if (DataContext is Models.RevisionTextFile file)
142-
Models.TextMateHelper.SetGrammarByFileName(_textMate, file.FileName);
139+
Models.TextMateHelper.SetGrammarByFileName(_textMate, file.FileName, ref _lastGrammarScope);
143140
}
144141
else if (_textMate != null)
145142
{
146-
_textMate.Dispose();
147-
_textMate = null;
148-
GC.Collect();
143+
Models.TextMateHelper.DisposeInstallation(ref _textMate, ref _lastGrammarScope, true);
149144

150145
TextArea.TextView.Redraw();
151146
}
152147
}
153148

154149
private TextMate.Installation _textMate = null;
150+
private string _lastGrammarScope = string.Empty;
155151
}
156152

157153
public partial class RevisionFileContentViewer : UserControl

‎src/Views/SelfUpdate.axaml.cs‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ protected override void OnLoaded(RoutedEventArgs e)
3434
if (_textMate == null)
3535
{
3636
_textMate = Models.TextMateHelper.CreateForEditor(this);
37-
Models.TextMateHelper.SetGrammarByFileName(_textMate, "README.md");
37+
Models.TextMateHelper.SetGrammarByFileName(_textMate, "README.md", ref _lastGrammarScope);
3838
}
3939
}
4040

@@ -43,10 +43,7 @@ protected override void OnUnloaded(RoutedEventArgs e)
4343
base.OnUnloaded(e);
4444

4545
if (_textMate != null)
46-
{
47-
_textMate.Dispose();
48-
_textMate = null;
49-
}
46+
Models.TextMateHelper.DisposeInstallation(ref _textMate, ref _lastGrammarScope);
5047

5148
GC.Collect();
5249
}
@@ -60,6 +57,7 @@ protected override void OnDataContextChanged(EventArgs e)
6057
}
6158

6259
private TextMate.Installation _textMate = null;
60+
private string _lastGrammarScope = string.Empty;
6361
}
6462

6563
public partial class SelfUpdate : ChromelessWindow

‎src/Views/TextDiffView.axaml.cs‎

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -550,10 +550,7 @@ protected override void OnUnloaded(RoutedEventArgs e)
550550
TextArea.TextView.VisualLinesChanged -= OnTextViewVisualLinesChanged;
551551

552552
if (_textMate != null)
553-
{
554-
_textMate.Dispose();
555-
_textMate = null;
556-
}
553+
Models.TextMateHelper.DisposeInstallation(ref _textMate, ref _lastGrammarScope);
557554
}
558555

559556
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
@@ -576,7 +573,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
576573
}
577574
else if (change.Property == FileNameProperty)
578575
{
579-
Models.TextMateHelper.SetGrammarByFileName(_textMate, FileName);
576+
Models.TextMateHelper.SetGrammarByFileName(_textMate, FileName, ref _lastGrammarScope);
580577
}
581578
else if (change.Property.Name == nameof(ActualThemeVariant) && change.NewValue != null)
582579
{
@@ -773,16 +770,14 @@ private void UpdateTextMate()
773770
TextArea.TextView.LineTransformers.Remove(_lineStyleTransformer);
774771
_textMate = Models.TextMateHelper.CreateForEditor(this);
775772
TextArea.TextView.LineTransformers.Add(_lineStyleTransformer);
776-
Models.TextMateHelper.SetGrammarByFileName(_textMate, FileName);
773+
Models.TextMateHelper.SetGrammarByFileName(_textMate, FileName, ref _lastGrammarScope);
777774
}
778775
}
779776
else
780777
{
781778
if (_textMate != null)
782779
{
783-
_textMate.Dispose();
784-
_textMate = null;
785-
GC.Collect();
780+
Models.TextMateHelper.DisposeInstallation(ref _textMate, ref _lastGrammarScope, true);
786781

787782
TextArea.TextView.Redraw();
788783
}
@@ -885,6 +880,7 @@ private async Task CopyWithoutIndicatorsAsync()
885880

886881
private bool _execSizeChanged;
887882
private TextMate.Installation _textMate;
883+
private string _lastGrammarScope = string.Empty;
888884
private TextLocation _lastSelectStart = TextLocation.Empty;
889885
private TextLocation _lastSelectEnd = TextLocation.Empty;
890886
private LineStyleTransformer _lineStyleTransformer;

0 commit comments

Comments
 (0)