From 39fba17648d11589de8944a2fd133d5938300130 Mon Sep 17 00:00:00 2001 From: leo Date: Sun, 18 Aug 2024 00:18:18 +0800 Subject: [PATCH] enhance: text editor (#365) * support extra grammars. * avoid crashing on text editor detached from visual tree --- src/Models/TextMateHelper.cs | 96 ++++++++- src/Native/Linux.cs | 1 - src/Resources/Grammars/toml.json | 343 +++++++++++++++++++++++++++++++ src/SourceGit.csproj | 1 + src/Views/Blame.axaml.cs | 6 + src/Views/TextDiffView.axaml.cs | 39 ++-- 6 files changed, 453 insertions(+), 33 deletions(-) create mode 100644 src/Resources/Grammars/toml.json diff --git a/src/Models/TextMateHelper.cs b/src/Models/TextMateHelper.cs index 0ae46c90..151d8bf5 100644 --- a/src/Models/TextMateHelper.cs +++ b/src/Models/TextMateHelper.cs @@ -1,24 +1,104 @@ using System; +using System.Collections.Generic; using System.IO; using Avalonia; +using Avalonia.Platform; using Avalonia.Styling; using AvaloniaEdit; using AvaloniaEdit.TextMate; using TextMateSharp.Grammars; +using TextMateSharp.Internal.Grammars.Reader; +using TextMateSharp.Internal.Types; +using TextMateSharp.Registry; +using TextMateSharp.Themes; namespace SourceGit.Models { + public class RegistryOptionsWrapper : IRegistryOptions + { + public RegistryOptionsWrapper(ThemeName defaultTheme) + { + _backend = new RegistryOptions(defaultTheme); + _extraGrammars = new Dictionary(); + + string[] extraGrammarFiles = ["toml.json"]; + foreach (var file in extraGrammarFiles) + { + var asset = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/Grammars/{file}", + UriKind.RelativeOrAbsolute)); + + try + { + var grammar = GrammarReader.ReadGrammarSync(new StreamReader(asset)); + _extraGrammars.Add(grammar.GetScopeName(), grammar); + } + catch + { + // ignore + } + } + } + + public IRawTheme GetTheme(string scopeName) + { + return _backend.GetTheme(scopeName); + } + + public IRawGrammar GetGrammar(string scopeName) + { + if (_extraGrammars.TryGetValue(scopeName, out var grammar)) + return grammar; + + return _backend.GetGrammar(scopeName); + } + + public ICollection GetInjections(string scopeName) + { + return _backend.GetInjections(scopeName); + } + + public IRawTheme GetDefaultTheme() + { + return _backend.GetDefaultTheme(); + } + + public IRawTheme LoadTheme(ThemeName name) + { + return _backend.LoadTheme(name); + } + + public string GetScopeByFileName(string filename) + { + var extension = Path.GetExtension(filename); + var scope = $"source{extension}"; + if (_extraGrammars.ContainsKey(scope)) + return scope; + + if (extension == ".h") + extension = ".cpp"; + else if (extension == ".resx" || extension == ".plist") + extension = ".xml"; + else if (extension == ".command") + extension = ".sh"; + + return _backend.GetScopeByExtension(extension); + } + + private RegistryOptions _backend = null; + private Dictionary _extraGrammars = null; + } + public static class TextMateHelper { public static TextMate.Installation CreateForEditor(TextEditor editor) { if (Application.Current?.ActualThemeVariant == ThemeVariant.Dark) - return editor.InstallTextMate(new RegistryOptions(ThemeName.DarkPlus)); + return editor.InstallTextMate(new RegistryOptionsWrapper(ThemeName.DarkPlus)); - return editor.InstallTextMate(new RegistryOptions(ThemeName.LightPlus)); + return editor.InstallTextMate(new RegistryOptionsWrapper(ThemeName.LightPlus)); } public static void SetThemeByApp(TextMate.Installation installation) @@ -26,7 +106,7 @@ namespace SourceGit.Models if (installation == null) return; - if (installation.RegistryOptions is RegistryOptions reg) + if (installation.RegistryOptions is RegistryOptionsWrapper reg) { if (Application.Current?.ActualThemeVariant == ThemeVariant.Dark) installation.SetTheme(reg.LoadTheme(ThemeName.DarkPlus)); @@ -37,15 +117,9 @@ namespace SourceGit.Models public static void SetGrammarByFileName(TextMate.Installation installation, string filePath) { - if (installation is { RegistryOptions: RegistryOptions reg }) + if (installation is { RegistryOptions: RegistryOptionsWrapper reg }) { - var ext = Path.GetExtension(filePath); - if (ext == ".h") - ext = ".cpp"; - else if (ext == ".resx" || ext == ".plist") - ext = ".xml"; - - installation.SetGrammar(reg.GetScopeByExtension(ext)); + installation.SetGrammar(reg.GetScopeByFileName(filePath)); GC.Collect(); } } diff --git a/src/Native/Linux.cs b/src/Native/Linux.cs index 02c4bb37..9d444dae 100644 --- a/src/Native/Linux.cs +++ b/src/Native/Linux.cs @@ -5,7 +5,6 @@ using System.IO; using System.Runtime.Versioning; using Avalonia; -using Avalonia.Dialogs; using Avalonia.Media; namespace SourceGit.Native diff --git a/src/Resources/Grammars/toml.json b/src/Resources/Grammars/toml.json new file mode 100644 index 00000000..86c2ef87 --- /dev/null +++ b/src/Resources/Grammars/toml.json @@ -0,0 +1,343 @@ +{ + "version": "1.0.0", + "scopeName": "source.toml", + "uuid": "8b4e5008-c50d-11ea-a91b-54ee75aeeb97", + "information_for_contributors": [ + "Originally was maintained by aster (galaster@foxmail.com). This notice is only kept here for the record, please don't send e-mails about bugs and other issues." + ], + "patterns": [ + { + "include": "#commentDirective" + }, + { + "include": "#comment" + }, + { + "include": "#table" + }, + { + "include": "#entryBegin" + }, + { + "include": "#value" + } + ], + "repository": { + "comment": { + "captures": { + "1": { + "name": "comment.line.number-sign.toml" + }, + "2": { + "name": "punctuation.definition.comment.toml" + } + }, + "comment": "Comments", + "match": "\\s*((#).*)$" + }, + "commentDirective": { + "captures": { + "1": { + "name": "meta.preprocessor.toml" + }, + "2": { + "name": "punctuation.definition.meta.preprocessor.toml" + } + }, + "comment": "Comments", + "match": "\\s*((#):.*)$" + }, + "table": { + "patterns": [ + { + "name": "meta.table.toml", + "match": "^\\s*(\\[)\\s*((?:(?:(?:[A-Za-z0-9_+-]+)|(?:\"[^\"]+\")|(?:'[^']+'))\\s*\\.?\\s*)+)\\s*(\\])", + "captures": { + "1": { + "name": "punctuation.definition.table.toml" + }, + "2": { + "patterns": [ + { + "match": "(?:[A-Za-z0-9_+-]+)|(?:\"[^\"]+\")|(?:'[^']+')", + "name": "support.type.property-name.table.toml" + }, + { + "match": "\\.", + "name": "punctuation.separator.dot.toml" + } + ] + }, + "3": { + "name": "punctuation.definition.table.toml" + } + } + }, + { + "name": "meta.array.table.toml", + "match": "^\\s*(\\[\\[)\\s*((?:(?:(?:[A-Za-z0-9_+-]+)|(?:\"[^\"]+\")|(?:'[^']+'))\\s*\\.?\\s*)+)\\s*(\\]\\])", + "captures": { + "1": { + "name": "punctuation.definition.array.table.toml" + }, + "2": { + "patterns": [ + { + "match": "(?:[A-Za-z0-9_+-]+)|(?:\"[^\"]+\")|(?:'[^']+')", + "name": "support.type.property-name.array.toml" + }, + { + "match": "\\.", + "name": "punctuation.separator.dot.toml" + } + ] + }, + "3": { + "name": "punctuation.definition.array.table.toml" + } + } + }, + { + "begin": "(\\{)", + "end": "(\\})", + "name": "meta.table.inline.toml", + "beginCaptures": { + "1": { + "name": "punctuation.definition.table.inline.toml" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.table.inline.toml" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "match": ",", + "name": "punctuation.separator.table.inline.toml" + }, + { + "include": "#entryBegin" + }, + { + "include": "#value" + } + ] + } + ] + }, + "entryBegin": { + "name": "meta.entry.toml", + "match": "\\s*((?:(?:(?:[A-Za-z0-9_+-]+)|(?:\"[^\"]+\")|(?:'[^']+'))\\s*\\.?\\s*)+)\\s*(=)", + "captures": { + "1": { + "patterns": [ + { + "match": "(?:[A-Za-z0-9_+-]+)|(?:\"[^\"]+\")|(?:'[^']+')", + "name": "support.type.property-name.toml" + }, + { + "match": "\\.", + "name": "punctuation.separator.dot.toml" + } + ] + }, + "2": { + "name": "punctuation.eq.toml" + } + } + }, + "value": { + "patterns": [ + { + "name": "string.quoted.triple.basic.block.toml", + "begin": "\"\"\"", + "end": "\"\"\"", + "patterns": [ + { + "match": "\\\\([btnfr\"\\\\\\n/ ]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})", + "name": "constant.character.escape.toml" + }, + { + "match": "\\\\[^btnfr/\"\\\\\\n]", + "name": "invalid.illegal.escape.toml" + } + ] + }, + { + "name": "string.quoted.single.basic.line.toml", + "begin": "\"", + "end": "\"", + "patterns": [ + { + "match": "\\\\([btnfr\"\\\\\\n/ ]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})", + "name": "constant.character.escape.toml" + }, + { + "match": "\\\\[^btnfr/\"\\\\\\n]", + "name": "invalid.illegal.escape.toml" + } + ] + }, + { + "name": "string.quoted.triple.literal.block.toml", + "begin": "'''", + "end": "'''" + }, + { + "name": "string.quoted.single.literal.line.toml", + "begin": "'", + "end": "'" + }, + { + "captures": { + "1": { + "name": "constant.other.time.datetime.offset.toml" + } + }, + "match": "(? + diff --git a/src/Views/Blame.axaml.cs b/src/Views/Blame.axaml.cs index d873fdce..9666ee68 100644 --- a/src/Views/Blame.axaml.cs +++ b/src/Views/Blame.axaml.cs @@ -40,6 +40,9 @@ namespace SourceGit.Views foreach (var line in view.VisualLines) { + if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted) + continue; + var lineNumber = line.FirstDocumentLine.LineNumber; if (lineNumber > _editor.BlameData.LineInfos.Count) break; @@ -151,6 +154,9 @@ namespace SourceGit.Views foreach (var line in view.VisualLines) { + if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted) + continue; + var lineNumber = line.FirstDocumentLine.LineNumber; if (lineNumber >= _editor.BlameData.LineInfos.Count) break; diff --git a/src/Views/TextDiffView.axaml.cs b/src/Views/TextDiffView.axaml.cs index 2ce0219b..da939e1e 100644 --- a/src/Views/TextDiffView.axaml.cs +++ b/src/Views/TextDiffView.axaml.cs @@ -41,8 +41,8 @@ namespace SourceGit.Views Math.Abs(Height - old.Height) > 0.001 || StartIdx != old.StartIdx || EndIdx != old.EndIdx || - Combined != Combined || - IsOldSide != IsOldSide; + Combined != old.Combined || + IsOldSide != old.IsOldSide; } } @@ -92,6 +92,9 @@ namespace SourceGit.Views var typeface = view.CreateTypeface(); foreach (var line in view.VisualLines) { + if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted) + continue; + var index = line.FirstDocumentLine.LineNumber; if (index > lines.Count) break; @@ -160,7 +163,7 @@ namespace SourceGit.Views var width = textView.Bounds.Width; foreach (var line in textView.VisualLines) { - if (line.FirstDocumentLine == null) + if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted) continue; var index = line.FirstDocumentLine.LineNumber; @@ -256,8 +259,6 @@ namespace SourceGit.Views v.TextRunProperties.SetForegroundBrush(_presenter.IndicatorForeground); v.TextRunProperties.SetTypeface(new Typeface(_presenter.FontFamily, FontStyle.Italic)); }); - - return; } } @@ -421,7 +422,7 @@ namespace SourceGit.Views if (chunk == null || (!chunk.Combined && chunk.IsOldSide != IsOld)) return; - var color = (Color)this.FindResource("SystemAccentColor"); + var color = (Color)this.FindResource("SystemAccentColor")!; var brush = new SolidColorBrush(color, 0.1); var pen = new Pen(color.ToUInt32()); var rect = new Rect(0, chunk.Y, Bounds.Width, chunk.Height); @@ -710,12 +711,7 @@ namespace SourceGit.Views var firstLineIdx = view.VisualLines[0].FirstDocumentLine.LineNumber - 1; var lastLineIdx = view.VisualLines[^1].FirstDocumentLine.LineNumber - 1; - if (endIdx < firstLineIdx) - { - TrySetChunk(null); - return; - } - else if (startIdx > lastLineIdx) + if (endIdx < firstLineIdx || startIdx > lastLineIdx) { TrySetChunk(null); return; @@ -746,6 +742,9 @@ namespace SourceGit.Views var lineIdx = -1; foreach (var line in view.VisualLines) { + if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted) + continue; + var index = line.FirstDocumentLine.LineNumber; if (index > diff.Lines.Count) break; @@ -888,12 +887,7 @@ namespace SourceGit.Views var firstLineIdx = view.VisualLines[0].FirstDocumentLine.LineNumber - 1; var lastLineIdx = view.VisualLines[^1].FirstDocumentLine.LineNumber - 1; - if (endIdx < firstLineIdx) - { - TrySetChunk(null); - return; - } - else if (startIdx > lastLineIdx) + if (endIdx < firstLineIdx || startIdx > lastLineIdx) { TrySetChunk(null); return; @@ -930,6 +924,9 @@ namespace SourceGit.Views var lineIdx = -1; foreach (var line in view.VisualLines) { + if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted) + continue; + var index = line.FirstDocumentLine.LineNumber; if (index > lines.Count) break; @@ -1164,7 +1161,7 @@ namespace SourceGit.Views SetCurrentValue(SelectedChunkProperty, null); } - private void OnStageChunk(object sender, RoutedEventArgs e) + private void OnStageChunk(object _1, RoutedEventArgs _2) { var chunk = SelectedChunk; if (chunk == null) @@ -1222,7 +1219,7 @@ namespace SourceGit.Views repo.SetWatcherEnabled(true); } - private void OnUnstageChunk(object sender, RoutedEventArgs e) + private void OnUnstageChunk(object _1, RoutedEventArgs _2) { var chunk = SelectedChunk; if (chunk == null) @@ -1276,7 +1273,7 @@ namespace SourceGit.Views repo.SetWatcherEnabled(true); } - private void OnDiscardChunk(object sender, RoutedEventArgs e) + private void OnDiscardChunk(object _1, RoutedEventArgs _2) { var chunk = SelectedChunk; if (chunk == null)