Skip to content

Commit 5a6bf33

Browse files
Merge pull request #1942 from gentoo90/ko
HTML: Add <!-- ko --> virtual element outlining
2 parents 47bee41 + 5c758e4 commit 5a6bf33

File tree

2 files changed

+231
-0
lines changed

2 files changed

+231
-0
lines changed
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.ComponentModel.Composition;
4+
using System.Linq;
5+
using System.Text.RegularExpressions;
6+
using System.Windows.Threading;
7+
using MadsKristensen.EditorExtensions.JavaScript;
8+
using Microsoft.VisualStudio.Text;
9+
using Microsoft.VisualStudio.Text.Tagging;
10+
using Microsoft.VisualStudio.Utilities;
11+
12+
namespace MadsKristensen.EditorExtensions.HTML.Outlining
13+
{
14+
[Export(typeof(ITaggerProvider))]
15+
[TagType(typeof(IOutliningRegionTag))]
16+
[ContentType("htmlx")]
17+
internal sealed class KoTaggerProvider : ITaggerProvider
18+
{
19+
public ITagger<T> CreateTagger<T>(ITextBuffer buffer) where T : ITag
20+
{
21+
return buffer.Properties.GetOrCreateSingletonProperty(() => new KoTagger(buffer)) as ITagger<T>;
22+
}
23+
}
24+
25+
internal sealed class KoTagger : ITagger<IOutliningRegionTag>
26+
{
27+
string startHide = "<!-- ko"; //the characters that start the outlining region
28+
string endHide = "/ko -->"; //the characters that end the outlining region
29+
ITextBuffer buffer;
30+
ITextSnapshot snapshot;
31+
List<Region> regions;
32+
private static Regex regex = new Regex(@"ko (.*?)-->", RegexOptions.Compiled);
33+
34+
public KoTagger(ITextBuffer buffer)
35+
{
36+
this.buffer = buffer;
37+
this.snapshot = buffer.CurrentSnapshot;
38+
this.regions = new List<Region>();
39+
this.buffer.Changed += BufferChanged;
40+
41+
Dispatcher.CurrentDispatcher.BeginInvoke(
42+
new Action(() => ReParse()), DispatcherPriority.ApplicationIdle, null);
43+
44+
this.buffer.Changed += BufferChanged;
45+
}
46+
47+
public IEnumerable<ITagSpan<IOutliningRegionTag>> GetTags(NormalizedSnapshotSpanCollection spans)
48+
{
49+
if (spans.Count == 0)
50+
yield break;
51+
52+
List<Region> currentRegions = this.regions;
53+
ITextSnapshot currentSnapshot = this.snapshot;
54+
55+
SnapshotSpan entire = new SnapshotSpan(spans[0].Start, spans[spans.Count - 1].End).TranslateTo(currentSnapshot, SpanTrackingMode.EdgeExclusive);
56+
int startLineNumber = entire.Start.GetContainingLine().LineNumber;
57+
int endLineNumber = entire.End.GetContainingLine().LineNumber;
58+
foreach (var region in currentRegions)
59+
{
60+
if (region.StartLine <= endLineNumber && region.EndLine >= startLineNumber)
61+
{
62+
var startLine = currentSnapshot.GetLineFromLineNumber(region.StartLine);
63+
var endLine = currentSnapshot.GetLineFromLineNumber(region.EndLine);
64+
65+
var snapshot = new SnapshotSpan(startLine.Start + region.StartOffset, endLine.End);
66+
Match match = regex.Match(snapshot.GetText());
67+
68+
string text = string.IsNullOrWhiteSpace(match.Groups[1].Value) ? "" : match.Groups[1].Value.Trim();
69+
string hoverText = snapshot.GetText();
70+
71+
//the region starts at the beginning of the "<!-- ko ", and goes until the *end* of the line that contains the "/ko -->".
72+
yield return new TagSpan<IOutliningRegionTag>(
73+
snapshot,
74+
new OutliningRegionTag(false, true, "<!-- ko -->...<!-- /ko -->", hoverText));
75+
}
76+
}
77+
}
78+
79+
public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
80+
void BufferChanged(object sender, TextContentChangedEventArgs e)
81+
{
82+
// 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).
83+
if (e.After != buffer.CurrentSnapshot)
84+
return;
85+
86+
Dispatcher.CurrentDispatcher.BeginInvoke(
87+
new Action(() => ReParse()), DispatcherPriority.ApplicationIdle, null);
88+
}
89+
90+
void ReParse()
91+
{
92+
ITextSnapshot newSnapshot = buffer.CurrentSnapshot;
93+
List<Region> newRegions = new List<Region>();
94+
95+
//keep the current (deepest) partial region, which will have
96+
// references to any parent partial regions.
97+
PartialRegion currentRegion = null;
98+
99+
foreach (var line in newSnapshot.Lines)
100+
{
101+
int regionStart = -1;
102+
string text = line.GetText();
103+
104+
//lines that contain a "[" denote the start of a new region.
105+
if ((regionStart = text.IndexOf(startHide, StringComparison.Ordinal)) != -1 || (regionStart = text.IndexOf(startHide.Replace(" ", string.Empty), StringComparison.Ordinal)) != -1)
106+
{
107+
int currentLevel = (currentRegion != null) ? currentRegion.Level : 1;
108+
int newLevel;
109+
if (!TryGetLevel(text, regionStart, out newLevel))
110+
newLevel = currentLevel + 1;
111+
112+
//levels are the same and we have an existing region;
113+
//end the current region and start the next
114+
if (currentLevel == newLevel && currentRegion != null)
115+
{
116+
newRegions.Add(new Region()
117+
{
118+
Level = currentRegion.Level,
119+
StartLine = currentRegion.StartLine,
120+
StartOffset = currentRegion.StartOffset,
121+
EndLine = line.LineNumber
122+
});
123+
124+
currentRegion = new PartialRegion()
125+
{
126+
Level = newLevel,
127+
StartLine = line.LineNumber,
128+
StartOffset = regionStart,
129+
PartialParent = currentRegion.PartialParent
130+
};
131+
}
132+
//this is a new (sub)region
133+
else
134+
{
135+
currentRegion = new PartialRegion()
136+
{
137+
Level = newLevel,
138+
StartLine = line.LineNumber,
139+
StartOffset = regionStart,
140+
PartialParent = currentRegion
141+
};
142+
}
143+
}
144+
//lines that contain "]" denote the end of a region
145+
else if ((regionStart = text.IndexOf(endHide, StringComparison.Ordinal)) != -1 || (regionStart = text.IndexOf(endHide.Replace(" ", string.Empty), StringComparison.Ordinal)) != -1)
146+
{
147+
int currentLevel = (currentRegion != null) ? currentRegion.Level : 1;
148+
int closingLevel;
149+
if (!TryGetLevel(text, regionStart, out closingLevel))
150+
closingLevel = currentLevel;
151+
152+
//the regions match
153+
if (currentRegion != null &&
154+
currentLevel == closingLevel)
155+
{
156+
newRegions.Add(new Region()
157+
{
158+
Level = currentLevel,
159+
StartLine = currentRegion.StartLine,
160+
StartOffset = currentRegion.StartOffset,
161+
EndLine = line.LineNumber
162+
});
163+
164+
currentRegion = currentRegion.PartialParent;
165+
}
166+
}
167+
}
168+
169+
//determine the changed span, and send a changed event with the new spans
170+
List<Span> oldSpans =
171+
new List<Span>(this.regions.Select(r => AsSnapshotSpan(r, this.snapshot)
172+
.TranslateTo(newSnapshot, SpanTrackingMode.EdgeExclusive)
173+
.Span));
174+
List<Span> newSpans =
175+
new List<Span>(newRegions.Select(r => AsSnapshotSpan(r, newSnapshot).Span));
176+
177+
NormalizedSpanCollection oldSpanCollection = new NormalizedSpanCollection(oldSpans);
178+
NormalizedSpanCollection newSpanCollection = new NormalizedSpanCollection(newSpans);
179+
180+
//the changed regions are regions that appear in one set or the other, but not both.
181+
NormalizedSpanCollection removed =
182+
NormalizedSpanCollection.Difference(oldSpanCollection, newSpanCollection);
183+
184+
int changeStart = int.MaxValue;
185+
int changeEnd = -1;
186+
187+
if (removed.Count > 0)
188+
{
189+
changeStart = removed[0].Start;
190+
changeEnd = removed[removed.Count - 1].End;
191+
}
192+
193+
if (newSpans.Count > 0)
194+
{
195+
changeStart = Math.Min(changeStart, newSpans[0].Start);
196+
changeEnd = Math.Max(changeEnd, newSpans[newSpans.Count - 1].End);
197+
}
198+
199+
this.snapshot = newSnapshot;
200+
this.regions = newRegions;
201+
202+
if (changeStart <= changeEnd)
203+
{
204+
if (this.TagsChanged != null)
205+
this.TagsChanged(this, new SnapshotSpanEventArgs(
206+
new SnapshotSpan(this.snapshot, Span.FromBounds(changeStart, changeEnd))));
207+
}
208+
}
209+
210+
static bool TryGetLevel(string text, int startIndex, out int level)
211+
{
212+
level = -1;
213+
if (text.Length > startIndex + 3)
214+
{
215+
if (int.TryParse(text.Substring(startIndex + 1), out level))
216+
return true;
217+
}
218+
219+
return false;
220+
}
221+
222+
static SnapshotSpan AsSnapshotSpan(Region region, ITextSnapshot snapshot)
223+
{
224+
var startLine = snapshot.GetLineFromLineNumber(region.StartLine);
225+
var endLine = (region.StartLine == region.EndLine) ? startLine
226+
: snapshot.GetLineFromLineNumber(region.EndLine);
227+
return new SnapshotSpan(startLine.Start + region.StartOffset, endLine.End);
228+
}
229+
}
230+
}

EditorExtensions/WebEssentials2013.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@
361361
<Compile Include="HTML\Completion\TwitterCardCompletion.cs" />
362362
<Compile Include="HTML\Completion\ViewportCompletion.cs" />
363363
<Compile Include="HTML\Helpers\HtmlHelpers.cs" />
364+
<Compile Include="HTML\Outlining\KoTagger.cs" />
364365
<Compile Include="HTML\Outlining\HtmlRegionTagger.cs" />
365366
<Compile Include="HTML\Peek\IDs\IdPeekDefinitionItem.cs" />
366367
<Compile Include="HTML\Peek\IDs\IdPeekItemProvider.cs" />

0 commit comments

Comments
 (0)