-
Notifications
You must be signed in to change notification settings - Fork 6
/
HighlightWordTagger.cs
202 lines (170 loc) · 8.29 KB
/
HighlightWordTagger.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Operations;
using Microsoft.VisualStudio.Text.Tagging;
namespace JAEE.AX.EditorExtensions
{
internal class HighlightWordTagger : ITagger<HighlightWordTag>
{
ITextView View { get; set; }
ITextBuffer SourceBuffer { get; set; }
ITextSearchService TextSearchService { get; set; }
ITextStructureNavigator TextStructureNavigator { get; set; }
NormalizedSnapshotSpanCollection WordSpans { get; set; }
SnapshotSpan? CurrentWord { get; set; }
SnapshotPoint RequestedPoint { get; set; }
object updateLock = new object();
public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
public HighlightWordTagger(ITextView view, ITextBuffer sourceBuffer, ITextSearchService textSearchService, ITextStructureNavigator textStructureNavigator)
{
this.View = view;
this.SourceBuffer = sourceBuffer;
this.TextSearchService = textSearchService;
this.TextStructureNavigator = textStructureNavigator;
this.WordSpans = new NormalizedSnapshotSpanCollection();
this.CurrentWord = null;
// START - 20140117 by kicko (http://ax2012editorext.codeplex.com/discussions/452826)
this.View.Selection.SelectionChanged += new EventHandler(SelectionChanged);
// END - 20140117 by kicko
this.View.LayoutChanged += ViewLayoutChanged;
}
void ViewLayoutChanged(object sender, TextViewLayoutChangedEventArgs e)
{
// If a new snapshot wasn't generated, then skip this layout
if (e.NewSnapshot != e.OldSnapshot)
{
UpdateAtCaretPosition(View.Caret.Position);
}
}
// START - 20140117 by kicko (http://ax2012editorext.codeplex.com/discussions/452826)
void SelectionChanged(object sender, EventArgs e)
{
TextExtent word;
if (View.Selection.SelectedSpans[0].IsEmpty)
{
SynchronousUpdate(RequestedPoint, new NormalizedSnapshotSpanCollection(), null);
return;
}
if (View.Selection.SelectedSpans.Count == 1)
{
word = TextStructureNavigator.GetExtentOfWord(View.Selection.Start.Position);
if (word.Span.Equals(View.Selection.SelectedSpans[0]))
{
RequestedPoint = View.Selection.Start.Position;
UpdateWordAdornments();
}
}
}
// END - 20140117 by kicko
void UpdateAtCaretPosition(CaretPosition caretPosition)
{
SnapshotPoint? point = caretPosition.Point.GetPoint(SourceBuffer, caretPosition.Affinity);
if (!point.HasValue)
return;
// If the new caret position is still within the current word (and on the same snapshot), we don't need to check it
if (CurrentWord.HasValue
&& CurrentWord.Value.Snapshot == View.TextSnapshot
&& point.Value >= CurrentWord.Value.Start
&& point.Value <= CurrentWord.Value.End)
{
return;
}
RequestedPoint = point.Value;
UpdateWordAdornments();
}
void UpdateWordAdornments()
{
SnapshotPoint currentRequest = RequestedPoint;
List<SnapshotSpan> wordSpans = new List<SnapshotSpan>();
//Find all words in the buffer like the one the caret is on
TextExtent word = TextStructureNavigator.GetExtentOfWord(currentRequest);
bool foundWord = true;
//If we've selected something not worth highlighting, we might have missed a "word" by a little bit
if (!WordExtentIsValid(currentRequest, word))
{
//Before we retry, make sure it is worthwhile
if (word.Span.Start != currentRequest
|| currentRequest == currentRequest.GetContainingLine().Start
|| char.IsWhiteSpace((currentRequest - 1).GetChar()))
{
foundWord = false;
}
else
{
// Try again, one character previous.
//If the caret is at the end of a word, pick up the word.
word = TextStructureNavigator.GetExtentOfWord(currentRequest - 1);
//If the word still isn't valid, we're done
if (!WordExtentIsValid(currentRequest, word))
foundWord = false;
}
}
if (!foundWord)
{
//If we couldn't find a word, clear out the existing markers
SynchronousUpdate(currentRequest, new NormalizedSnapshotSpanCollection(), null);
return;
}
SnapshotSpan currentWord = word.Span;
//If this is the current word, and the caret moved within a word, we're done.
if (CurrentWord.HasValue && currentWord == CurrentWord)
return;
//Find the new spans
FindData findData = new FindData(currentWord.GetText(), currentWord.Snapshot);
findData.FindOptions = FindOptions.WholeWord | FindOptions.MatchCase;
wordSpans.AddRange(TextSearchService.FindAll(findData));
//If another change hasn't happened, do a real update
if (currentRequest == RequestedPoint)
SynchronousUpdate(currentRequest, new NormalizedSnapshotSpanCollection(wordSpans), currentWord);
}
static bool WordExtentIsValid(SnapshotPoint currentRequest, TextExtent word)
{
return word.IsSignificant
&& currentRequest.Snapshot.GetText(word.Span).Any(c => char.IsLetter(c));
}
void SynchronousUpdate(SnapshotPoint currentRequest, NormalizedSnapshotSpanCollection newSpans, SnapshotSpan? newCurrentWord)
{
lock (updateLock)
{
if (currentRequest != RequestedPoint)
return;
WordSpans = newSpans;
CurrentWord = newCurrentWord;
var tempEvent = TagsChanged;
if (tempEvent != null)
tempEvent(this, new SnapshotSpanEventArgs(new SnapshotSpan(SourceBuffer.CurrentSnapshot, 0, SourceBuffer.CurrentSnapshot.Length)));
}
}
public IEnumerable<ITagSpan<HighlightWordTag>> GetTags(NormalizedSnapshotSpanCollection spans)
{
if (CurrentWord == null)
yield break;
// Hold on to a "snapshot" of the word spans and current word, so that we maintain the same
// collection throughout
SnapshotSpan currentWord = CurrentWord.Value;
NormalizedSnapshotSpanCollection wordSpans = WordSpans;
if (spans.Count == 0 || WordSpans.Count <= 1)
yield break;
// If the requested snapshot isn't the same as the one our words are on, translate our spans to the expected snapshot
if (spans[0].Snapshot != wordSpans[0].Snapshot)
{
wordSpans = new NormalizedSnapshotSpanCollection(
wordSpans.Select(span => span.TranslateTo(spans[0].Snapshot, SpanTrackingMode.EdgeExclusive)));
currentWord = currentWord.TranslateTo(spans[0].Snapshot, SpanTrackingMode.EdgeExclusive);
}
// First, yield back the word the cursor is under (if it overlaps)
// Note that we'll yield back the same word again in the wordspans collection;
// the duplication here is expected.
if (spans.OverlapsWith(new NormalizedSnapshotSpanCollection(currentWord)))
yield return new TagSpan<HighlightWordTag>(currentWord, new HighlightWordTag());
// Second, yield all the other words in the file
foreach (SnapshotSpan span in NormalizedSnapshotSpanCollection.Overlap(spans, wordSpans))
{
yield return new TagSpan<HighlightWordTag>(span, new HighlightWordTag());
}
}
}
}