Skip to content

Commit

Permalink
Feature: Razor templates support
Browse files Browse the repository at this point in the history
  • Loading branch information
lordfanger committed May 11, 2023
1 parent 36c12e5 commit 477e59f
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 10 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ Both light and dark themes are supported.

![Screenshot](art/theme-colors.png)

Razor templates (.razor) are now supported.

![Razor](art/razor.png)

To toggle the brace colorization on and off, a menu item is available under the **Edit -> Advanced** menu or using the shortcut **Ctrl+Shift+9**.

![Menu](art/menu.png)
Expand Down
Binary file added art/razor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/RainbowBraces.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
<Generator>VsctGenerator</Generator>
<LastGenOutput>VSCommandTable.cs</LastGenOutput>
</VSCTCompile>
<Compile Include="Tagger\RazorAllowanceResolver.cs" />
<Compile Include="Tagger\TagAllowance.cs" />
<Compile Include="Tagger\VerticalAdornmentsColorizer.cs" />
<Compile Include="VSCommandTable.cs">
Expand Down
23 changes: 22 additions & 1 deletion src/Tagger/AllowanceResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ namespace RainbowBraces.Tagger
{
public abstract class AllowanceResolver
{
/// <summary>
/// Returns allowance category for given tag.
/// </summary>
public TagAllowance GetAllowance(IMappingTagSpan<IClassificationTag> tagSpan)
{
IClassificationType tagType = tagSpan.Tag.ClassificationType;
Expand All @@ -29,14 +32,32 @@ public TagAllowance GetAllowance(IMappingTagSpan<IClassificationTag> tagSpan)
return TagAllowance.Disallowed;
}

/// <summary>
/// If this property is <see langword="true"/> brace should be considered pair if is not in another tag.
/// Otherwise brace will be treated as unrelated text.
/// </summary>
public virtual bool DefaultAllowed => true;

/// <summary>
/// If this property is <see langword="true"/> we expect tags from other sources to change dynamically
/// and so we'll have to listen to these changes a parse tags more often without user changes.
/// </summary>
public virtual bool CanChangeTags => false;

private TagAllowance GetAllowance(IClassificationType tagType)
{
if (tagType is ILayeredClassificationType layeredType) return IsAllowed(layeredType);
else return IsAllowed(tagType);
}

/// <summary>
/// Implementation for allowance resolution for <see cref="IClassificationType"/>.
/// </summary>
protected abstract TagAllowance IsAllowed(IClassificationType tagType);

/// <summary>
/// Implementation for allowance resolution for <see cref="ILayeredClassificationType"/>.
/// </summary>
protected abstract TagAllowance IsAllowed(ILayeredClassificationType layeredType);
}
}
}
102 changes: 93 additions & 9 deletions src/Tagger/RainbowTagger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public class RainbowTagger : ITagger<IClassificationTag>
private readonly Debouncer _debouncer;
private List<ITagSpan<IClassificationTag>> _tags = new();
private readonly BracePairCache _pairsCache = new();
private readonly List<IMappingTagSpan<IClassificationTag>> _tagList = new();
private List<IMappingTagSpan<IClassificationTag>> _tagList = new();
private List<IMappingTagSpan<IClassificationTag>> _tempTagList = new();
private readonly List<(SnapshotSpan, TagAllowance)> _spanList = new();
private bool _isEnabled;
private bool _scanWholeFile;
Expand Down Expand Up @@ -94,8 +95,25 @@ private void RemoveView(ITextView view)

private void View_LayoutChanged(object sender, TextViewLayoutChangedEventArgs e)
{
if (_scanWholeFile) return;
if (_isEnabled && (e.VerticalTranslation || e.HorizontalTranslation))
if (!_isEnabled) return;

bool isViewportChange = e.VerticalTranslation || e.HorizontalTranslation;
bool canParse = (_scanWholeFile, _allowanceResolver.CanChangeTags, isViewportChange) switch
{
// We scan whole file and listen to tags change, we can ignore viewport change. (I'm not so sure if viewport change can introduce new tags we are aware of)
(true, true, true) => false,
// We scan whole file, listen to every change and event is not viewport change so it must be change in tags.
(true, true, false) => true,
// We scan whole file and ignore tags changes.
(true, false, _) => false,
// We listen to tags changes and so we listen to viewport changes.
(false, true, _) => true,
// Is viewport change and we don't scan whole file, we need to parse again.
(false, false, true) => true,
// Is tags change and we ignore it.
(false, false, false) => false,
};
if (canParse)
{
_debouncer.Debouce(() => { _ = ParseAsync(); });
}
Expand Down Expand Up @@ -212,7 +230,7 @@ public async Task ParseAsync(int topPosition = 0, bool forceActual = true)

private async Task ParseInternalAsync(int topPosition)
{
// If we are parsing after change pick the topmost change that occured.
// If we are parsing after change pick the topmost change that occurred.
if (topPosition != 0)
{
topPosition = _startPositionChange ?? topPosition;
Expand All @@ -239,12 +257,26 @@ private async Task ParseInternalAsync(int topPosition)

// Add tags to instantiated list to reduce allocations on UI thread and increase responsiveness
// We expect tags count not to differ a lot between invocations so memory should not be wasted a lot
_tagList.Clear();
_tagList.AddRange(_aggregator.GetTags(wholeDocSpan));
_tempTagList.Clear();
_tempTagList.AddRange(_aggregator.GetTags(wholeDocSpan));

// Move the rest of the execution to a background thread.
await TaskScheduler.Default;

// Check if tags are equal from last processing.
if (AreEqualTags(_tagList, _tempTagList))
{
// We can clear the temporary list to avoid memory leaks.
_tempTagList.Clear();
return;
}

// Swap tag lists.
(_tagList, _tempTagList) = (_tempTagList, _tagList);

// We can clear the temporary list to avoid memory leaks.
_tempTagList.Clear();

// Filter tags and get their spans
_spanList.Clear();
_spanList.AddRange(_tagList
Expand Down Expand Up @@ -333,12 +365,18 @@ private async Task ParseInternalAsync(int topPosition)
matchingSpans.Clear();
matchingSpans.AddRange(possibleMatchingSpans.Values.Where(s => s.Span.Start <= position && s.Span.End > position));

// If brace is part of another tag (not punctation, operator or delimiter) then ignore it. (eg. is in string literal)
// If brace is part of another tag (not punctuation, operator or delimiter) then ignore it. (eg. is in string literal)
if (matchingSpans.Any(s => s.Allowance == TagAllowance.Disallowed))
{
continue;
}

// If default allowance is Disallowed and no matching tag intersect then ignore it. (can be unrelated text in HTML)
if (!_allowanceResolver.DefaultAllowed && matchingSpans.Count == 0)
{
continue;
}

Span braceSpan = new(position, 1);

// Try all builders if any can accept matched brace
Expand All @@ -354,12 +392,57 @@ private async Task ParseInternalAsync(int topPosition)
}
}

List<ITagSpan<IClassificationTag>> tags = GenerateTagSpans(builders.SelectMany(b => b.Pairs), options.CycleLength);

// Check if tag collection is different from previous result. If so, we do not need to raise TagsChanged event.
if (AreEqualTags(tags, _tags)) return;

builders.SaveToCache(_pairsCache);
_tags = GenerateTagSpans(builders.SelectMany(b => b.Pairs), options.CycleLength);
_tags = tags;
TagsChanged?.Invoke(this, new(new(_buffer.CurrentSnapshot, visibleStart, visibleEnd - visibleStart)));
if (options.VerticalAdornments) ColorizeVerticalAdornments();
}

private bool AreEqualTags(IReadOnlyList<IMappingTagSpan<IClassificationTag>> originalTags, IReadOnlyList<IMappingTagSpan<IClassificationTag>> newTags)
{
if (originalTags.Count != newTags.Count) return false;
for (int i = 0; i < originalTags.Count; i++)
{
IMappingTagSpan<IClassificationTag> originalTag = originalTags[i];
IMappingTagSpan<IClassificationTag> newTag = newTags[i];
if (!originalTag.Tag.ClassificationType.Classification.Equals(newTag.Tag.ClassificationType.Classification)) return false;
if (!AreEqualSpans(originalTag.Span.GetSpans(_buffer), newTag.Span.GetSpans(_buffer))) return false;
}
return true;
}

private static bool AreEqualTags(IReadOnlyList<ITagSpan<IClassificationTag>> originalTags, IReadOnlyList<ITagSpan<IClassificationTag>> newTags)
{
if (originalTags.Count != newTags.Count) return false;
for (int i = 0; i < originalTags.Count; i++)
{
ITagSpan<IClassificationTag> originalTag = originalTags[i];
ITagSpan<IClassificationTag> newTag = newTags[i];
if (!originalTag.Tag.ClassificationType.Classification.Equals(newTag.Tag.ClassificationType.Classification)) return false;
if (originalTag.Span.Start.Position != newTag.Span.Start.Position) return false;
if (originalTag.Span.Length != newTag.Span.Length) return false;
}
return true;
}

private static bool AreEqualSpans(NormalizedSnapshotSpanCollection originalSpans, NormalizedSnapshotSpanCollection newSpans)
{
if (originalSpans.Count != newSpans.Count) return false;
for (int i = 0; i < originalSpans.Count; i++)
{
SnapshotSpan originalTag = originalSpans[i];
SnapshotSpan newTag = newSpans[i];
if (originalTag.Span.Start != newTag.Span.Start) return false;
if (originalTag.Span.Length != newTag.Span.Length) return false;
}
return true;
}

private void HandleRatingPrompt()
{
if (_tags.Count > 0)
Expand Down Expand Up @@ -442,6 +525,7 @@ private static AllowanceResolver GetAllowanceResolver(ITextBuffer buffer)
ContentTypes.Css => new CssAllowanceResolver(),
ContentTypes.Less => new CssAllowanceResolver(),
ContentTypes.Scss => new CssAllowanceResolver(),
"RAZOR" => new RazorAllowanceResolver(),
_ => new DefaultAllowanceResolver()
};

Expand Down Expand Up @@ -471,7 +555,7 @@ private void CascadiaCodeHack(ITextView view)
for (int level = 0; level < cycleLength; level++)
{
IClassificationType classification = _registry.GetClassificationType(ClassificationTypes.GetName(level, cycleLength));
TextFormattingRunProperties textProperties = formatMap.GetTextProperties(classification);
TextFormattingRunProperties textProperties = formatMap.GetTextProperties(classification);
if (FontFamilyMapper.TryGetEquivalentToCascadiaCode(textProperties.Typeface, out Typeface eqivalent))
{
// set font equivalent to current but with respect to colorization of individual tags
Expand Down
1 change: 1 addition & 0 deletions src/Tagger/RainbowTaggerProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ namespace RainbowBraces
[ContentType("phalanger")]
[ContentType("Code++")]
[ContentType("XSharp")]
[ContentType("Razor")]
[TextViewRole(PredefinedTextViewRoles.PrimaryDocument)]
[TagType(typeof(IClassificationTag))]
public class CreationListener : IViewTaggerProvider
Expand Down
16 changes: 16 additions & 0 deletions src/Tagger/RazorAllowanceResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace RainbowBraces.Tagger;

public class RazorAllowanceResolver : DefaultAllowanceResolver
{
/// <inheritdoc />
/// <remarks>
/// Braces in HTML text should not be treated as pairs.
/// </remarks>
public override bool DefaultAllowed => false;

/// <inheritdoc />
/// <remarks>
/// Tags are changed multiple times in razor templates, we need to listen to these changes.
/// </remarks>
public override bool CanChangeTags => true;
}
4 changes: 4 additions & 0 deletions vs-publish.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
{
"pathOnDisk": "art/vertical-adornments-options.png",
"targetPath": "art/vertical-adornments-options.png"
},
{
"pathOnDisk": "art/razor.png",
"targetPath": "art/razor.png"
}
],
"overview": "README.md",
Expand Down

0 comments on commit 477e59f

Please sign in to comment.