-
-
Notifications
You must be signed in to change notification settings - Fork 252
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1942 from gentoo90/ko
HTML: Add <!-- ko --> virtual element outlining
- Loading branch information
Showing
2 changed files
with
231 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,230 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.ComponentModel.Composition; | ||
using System.Linq; | ||
using System.Text.RegularExpressions; | ||
using System.Windows.Threading; | ||
using MadsKristensen.EditorExtensions.JavaScript; | ||
using Microsoft.VisualStudio.Text; | ||
using Microsoft.VisualStudio.Text.Tagging; | ||
using Microsoft.VisualStudio.Utilities; | ||
|
||
namespace MadsKristensen.EditorExtensions.HTML.Outlining | ||
{ | ||
[Export(typeof(ITaggerProvider))] | ||
[TagType(typeof(IOutliningRegionTag))] | ||
[ContentType("htmlx")] | ||
internal sealed class KoTaggerProvider : ITaggerProvider | ||
{ | ||
public ITagger<T> CreateTagger<T>(ITextBuffer buffer) where T : ITag | ||
{ | ||
return buffer.Properties.GetOrCreateSingletonProperty(() => new KoTagger(buffer)) as ITagger<T>; | ||
} | ||
} | ||
|
||
internal sealed class KoTagger : ITagger<IOutliningRegionTag> | ||
{ | ||
string startHide = "<!-- ko"; //the characters that start the outlining region | ||
string endHide = "/ko -->"; //the characters that end the outlining region | ||
ITextBuffer buffer; | ||
ITextSnapshot snapshot; | ||
List<Region> regions; | ||
private static Regex regex = new Regex(@"ko (.*?)-->", RegexOptions.Compiled); | ||
|
||
public KoTagger(ITextBuffer buffer) | ||
{ | ||
this.buffer = buffer; | ||
this.snapshot = buffer.CurrentSnapshot; | ||
this.regions = new List<Region>(); | ||
this.buffer.Changed += BufferChanged; | ||
|
||
Dispatcher.CurrentDispatcher.BeginInvoke( | ||
new Action(() => ReParse()), DispatcherPriority.ApplicationIdle, null); | ||
|
||
this.buffer.Changed += BufferChanged; | ||
} | ||
|
||
public IEnumerable<ITagSpan<IOutliningRegionTag>> GetTags(NormalizedSnapshotSpanCollection spans) | ||
{ | ||
if (spans.Count == 0) | ||
yield break; | ||
|
||
List<Region> currentRegions = this.regions; | ||
ITextSnapshot currentSnapshot = this.snapshot; | ||
|
||
SnapshotSpan entire = new SnapshotSpan(spans[0].Start, spans[spans.Count - 1].End).TranslateTo(currentSnapshot, SpanTrackingMode.EdgeExclusive); | ||
int startLineNumber = entire.Start.GetContainingLine().LineNumber; | ||
int endLineNumber = entire.End.GetContainingLine().LineNumber; | ||
foreach (var region in currentRegions) | ||
{ | ||
if (region.StartLine <= endLineNumber && region.EndLine >= startLineNumber) | ||
{ | ||
var startLine = currentSnapshot.GetLineFromLineNumber(region.StartLine); | ||
var endLine = currentSnapshot.GetLineFromLineNumber(region.EndLine); | ||
|
||
var snapshot = new SnapshotSpan(startLine.Start + region.StartOffset, endLine.End); | ||
Match match = regex.Match(snapshot.GetText()); | ||
|
||
string text = string.IsNullOrWhiteSpace(match.Groups[1].Value) ? "" : match.Groups[1].Value.Trim(); | ||
string hoverText = snapshot.GetText(); | ||
|
||
//the region starts at the beginning of the "<!-- ko ", and goes until the *end* of the line that contains the "/ko -->". | ||
yield return new TagSpan<IOutliningRegionTag>( | ||
snapshot, | ||
new OutliningRegionTag(false, true, "<!-- ko -->...<!-- /ko -->", hoverText)); | ||
} | ||
} | ||
} | ||
|
||
public event EventHandler<SnapshotSpanEventArgs> TagsChanged; | ||
void BufferChanged(object sender, TextContentChangedEventArgs e) | ||
{ | ||
// If this isn't the most up-to-date version of the buffer, then ignore it for now (we'll eventually get another change event). | ||
if (e.After != buffer.CurrentSnapshot) | ||
return; | ||
|
||
Dispatcher.CurrentDispatcher.BeginInvoke( | ||
new Action(() => ReParse()), DispatcherPriority.ApplicationIdle, null); | ||
} | ||
|
||
void ReParse() | ||
{ | ||
ITextSnapshot newSnapshot = buffer.CurrentSnapshot; | ||
List<Region> newRegions = new List<Region>(); | ||
|
||
//keep the current (deepest) partial region, which will have | ||
// references to any parent partial regions. | ||
PartialRegion currentRegion = null; | ||
|
||
foreach (var line in newSnapshot.Lines) | ||
{ | ||
int regionStart = -1; | ||
string text = line.GetText(); | ||
|
||
//lines that contain a "[" denote the start of a new region. | ||
if ((regionStart = text.IndexOf(startHide, StringComparison.Ordinal)) != -1 || (regionStart = text.IndexOf(startHide.Replace(" ", string.Empty), StringComparison.Ordinal)) != -1) | ||
{ | ||
int currentLevel = (currentRegion != null) ? currentRegion.Level : 1; | ||
int newLevel; | ||
if (!TryGetLevel(text, regionStart, out newLevel)) | ||
newLevel = currentLevel + 1; | ||
|
||
//levels are the same and we have an existing region; | ||
//end the current region and start the next | ||
if (currentLevel == newLevel && currentRegion != null) | ||
{ | ||
newRegions.Add(new Region() | ||
{ | ||
Level = currentRegion.Level, | ||
StartLine = currentRegion.StartLine, | ||
StartOffset = currentRegion.StartOffset, | ||
EndLine = line.LineNumber | ||
}); | ||
|
||
currentRegion = new PartialRegion() | ||
{ | ||
Level = newLevel, | ||
StartLine = line.LineNumber, | ||
StartOffset = regionStart, | ||
PartialParent = currentRegion.PartialParent | ||
}; | ||
} | ||
//this is a new (sub)region | ||
else | ||
{ | ||
currentRegion = new PartialRegion() | ||
{ | ||
Level = newLevel, | ||
StartLine = line.LineNumber, | ||
StartOffset = regionStart, | ||
PartialParent = currentRegion | ||
}; | ||
} | ||
} | ||
//lines that contain "]" denote the end of a region | ||
else if ((regionStart = text.IndexOf(endHide, StringComparison.Ordinal)) != -1 || (regionStart = text.IndexOf(endHide.Replace(" ", string.Empty), StringComparison.Ordinal)) != -1) | ||
{ | ||
int currentLevel = (currentRegion != null) ? currentRegion.Level : 1; | ||
int closingLevel; | ||
if (!TryGetLevel(text, regionStart, out closingLevel)) | ||
closingLevel = currentLevel; | ||
|
||
//the regions match | ||
if (currentRegion != null && | ||
currentLevel == closingLevel) | ||
{ | ||
newRegions.Add(new Region() | ||
{ | ||
Level = currentLevel, | ||
StartLine = currentRegion.StartLine, | ||
StartOffset = currentRegion.StartOffset, | ||
EndLine = line.LineNumber | ||
}); | ||
|
||
currentRegion = currentRegion.PartialParent; | ||
} | ||
} | ||
} | ||
|
||
//determine the changed span, and send a changed event with the new spans | ||
List<Span> oldSpans = | ||
new List<Span>(this.regions.Select(r => AsSnapshotSpan(r, this.snapshot) | ||
.TranslateTo(newSnapshot, SpanTrackingMode.EdgeExclusive) | ||
.Span)); | ||
List<Span> newSpans = | ||
new List<Span>(newRegions.Select(r => AsSnapshotSpan(r, newSnapshot).Span)); | ||
|
||
NormalizedSpanCollection oldSpanCollection = new NormalizedSpanCollection(oldSpans); | ||
NormalizedSpanCollection newSpanCollection = new NormalizedSpanCollection(newSpans); | ||
|
||
//the changed regions are regions that appear in one set or the other, but not both. | ||
NormalizedSpanCollection removed = | ||
NormalizedSpanCollection.Difference(oldSpanCollection, newSpanCollection); | ||
|
||
int changeStart = int.MaxValue; | ||
int changeEnd = -1; | ||
|
||
if (removed.Count > 0) | ||
{ | ||
changeStart = removed[0].Start; | ||
changeEnd = removed[removed.Count - 1].End; | ||
} | ||
|
||
if (newSpans.Count > 0) | ||
{ | ||
changeStart = Math.Min(changeStart, newSpans[0].Start); | ||
changeEnd = Math.Max(changeEnd, newSpans[newSpans.Count - 1].End); | ||
} | ||
|
||
this.snapshot = newSnapshot; | ||
this.regions = newRegions; | ||
|
||
if (changeStart <= changeEnd) | ||
{ | ||
if (this.TagsChanged != null) | ||
this.TagsChanged(this, new SnapshotSpanEventArgs( | ||
new SnapshotSpan(this.snapshot, Span.FromBounds(changeStart, changeEnd)))); | ||
} | ||
} | ||
|
||
static bool TryGetLevel(string text, int startIndex, out int level) | ||
{ | ||
level = -1; | ||
if (text.Length > startIndex + 3) | ||
{ | ||
if (int.TryParse(text.Substring(startIndex + 1), out level)) | ||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
static SnapshotSpan AsSnapshotSpan(Region region, ITextSnapshot snapshot) | ||
{ | ||
var startLine = snapshot.GetLineFromLineNumber(region.StartLine); | ||
var endLine = (region.StartLine == region.EndLine) ? startLine | ||
: snapshot.GetLineFromLineNumber(region.EndLine); | ||
return new SnapshotSpan(startLine.Start + region.StartOffset, endLine.End); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters