-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSimileClassifier.cs
More file actions
348 lines (308 loc) · 11.1 KB
/
SimileClassifier.cs
File metadata and controls
348 lines (308 loc) · 11.1 KB
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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Classification;
using System;
using System.Collections.Generic;
using System.IO;
namespace ConsoleCompare
{
/// <summary>
/// Classifier that classifies all text as an instance of the "SimileClassifier" classification type.
/// Details on overall classifier setup (slightly different than this one): https://stackoverflow.com/a/37602798
/// </summary>
internal class SimileClassifier : IClassifier
{
/// <summary>
/// Classification type.
/// </summary>
private readonly IClassificationType simileCommentType;
private readonly IClassificationType simileTagErrorType;
private readonly IClassificationType simileLineErrorType;
private readonly IClassificationType simileInputTagType;
private readonly IClassificationType simileNumericTagType;
/// <summary>
/// Initializes a new instance of the <see cref="SimileClassifier"/> class.
/// </summary>
/// <param name="registry">Classification registry.</param>
internal SimileClassifier(IClassificationTypeRegistryService registry)
{
simileCommentType = registry.GetClassificationType(SimileClassifications.SimileCommentClassifier);
simileTagErrorType = registry.GetClassificationType(SimileClassifications.SimileTagErrorClassifier);
simileLineErrorType = registry.GetClassificationType(SimileClassifications.SimileLineErrorClassifier);
simileInputTagType = registry.GetClassificationType(SimileClassifications.SimileInputTagClassifier);
simileNumericTagType = registry.GetClassificationType(SimileClassifications.SimileNumericTagClassifier);
}
#region IClassifier
#pragma warning disable 67
/// <summary>
/// An event that occurs when the classification of a span of text has changed.
/// </summary>
/// <remarks>
/// This event gets raised if a non-text change would affect the classification in some way,
/// for example typing /* would cause the classification to change in C# without directly
/// affecting the span.
/// </remarks>
public event EventHandler<ClassificationChangedEventArgs> ClassificationChanged;
#pragma warning restore 67
/// <summary>
/// Gets all the <see cref="ClassificationSpan"/> objects that intersect with the given range of text.
/// </summary>
/// <remarks>
/// This method scans the given SnapshotSpan for potential matches for this classification.
/// </remarks>
/// <param name="span">The span currently being classified.</param>
/// <returns>A list of ClassificationSpans that represent spans identified to be of this classification.</returns>
public IList<ClassificationSpan> GetClassificationSpans(SnapshotSpan span)
{
SimileErrorSnapshot errorSnapshot = new SimileErrorSnapshot();
// Grab possible error details
int lineNumber = span.Snapshot.GetLineNumberFromPosition(span.Start);
string filename = GetDocumentFilename(span.Snapshot);
// Build the results up as we go
List<ClassificationSpan> results = new List<ClassificationSpan>();
// Grab the overall text of this span
string text = span.GetText();
// Is it a comment?
if (text.StartsWith(SimileParser.PrefaceComment))
{
// Whole line is a comment; add as a comment and return
results.Add(CreateTagSpan(span, 0, span.Length, simileCommentType));
return results;
}
// Loop through, character by character, looking for start tags
bool isError = false;
int numericTagCount = 0;
int numericTagsOpen = 0;
int numericTagStart = -1;
int inputTagCount = 0;
int inputTagsOpen = 0;
int inputTagStart = -1;
for (int i = 0; i < text.Length; i++)
{
// Does a tag start or end here?
if (ContainsAtIndex(text, SimileParser.NumericTagStart, i))
{
// New numeric tag starting
numericTagCount++;
numericTagsOpen++;
numericTagStart = i;
}
else if (ContainsAtIndex(text, SimileParser.NumericTagEnd, i))
{
// Numeric tag ending
numericTagsOpen--;
// If we're at zero, we've just finished a tag
if (numericTagsOpen == 0)
{
// Check the tag's validity
int numericTagLength = i - numericTagStart + SimileParser.NumericTagEnd.Length;
string tag = text.Substring(numericTagStart, numericTagLength);
string errorDetails = null;
bool validTag = SimileParser.ParseNumericTag(tag, null, out errorDetails);
// Create the span either way, but color code based on validity
results.Add(CreateTagSpan(
span,
numericTagStart,
numericTagLength,
validTag ? simileNumericTagType : simileTagErrorType));
if (!validTag)
{
errorSnapshot.AddError(new SimileError()
{
Text = "Error with numeric tag: " + errorDetails,
DocumentName = filename,
LineNumber = lineNumber,
ColumnNumber = i
});
}
// While we're here, check for invalid post-numeric-tag character
int postTagCharIndex = i + SimileParser.NumericTagEnd.Length; // Take into account end tag length!
if (postTagCharIndex < text.Length && text[postTagCharIndex] != ' ')
{
// There are more characters, and the next one isn't a space!
// Highlight the very next character as an error
results.Add(CreateTagSpan(
span,
postTagCharIndex,
1,
simileTagErrorType));
errorSnapshot.AddError(new SimileError()
{
Text = "Numeric tags appearing before the end of a line must be followed by a space",
DocumentName = filename,
LineNumber = lineNumber,
ColumnNumber = postTagCharIndex
});
}
}
}
else if (ContainsAtIndex(text, SimileParser.InputTagStart, i))
{
// New input tag starting
inputTagCount++;
inputTagsOpen++;
inputTagStart = i;
}
else if (ContainsAtIndex(text, SimileParser.InputTagEnd, i))
{
// Input tag ending
inputTagsOpen--;
// If we're at zero here, we've just finished a tag
if (inputTagsOpen == 0)
{
results.Add(CreateTagSpan(
span,
inputTagStart,
i - inputTagStart + SimileParser.InputTagEnd.Length,
simileInputTagType));
// Is there anything after this tag?
int afterTag = i + SimileParser.InputTagEnd.Length;
if (afterTag < text.TrimEnd('\r', '\n').Length) // TODO: Maybe optimize this slightly?
{
// Error: Input tag is not at the end of the line
errorSnapshot.AddError(new SimileError()
{
Text = "Input tag is not at the end of line",
DocumentName = filename,
LineNumber = lineNumber,
ColumnNumber = inputTagStart
});
// Classify everything AFTER the tag as an error
results.Add(CreateTagSpan(
span,
afterTag,
text.Length - afterTag,
simileLineErrorType));
}
}
}
// == Check all possible errors ==
// Numeric tag start/end mismatch
if (numericTagsOpen < 0 || numericTagsOpen > 1)
{
errorSnapshot.AddError(new SimileError()
{
Text = "Numeric tag open/close mismatch",
DocumentName = filename,
LineNumber = lineNumber,
ColumnNumber = i
});
isError = true;
break;
}
// Input tag start/end mismatch
if (inputTagsOpen < 0 || inputTagsOpen > 1)
{
errorSnapshot.AddError(new SimileError()
{
Text = "Input tag open/close mismatch",
DocumentName = filename,
LineNumber = lineNumber,
ColumnNumber = i
});
isError = true;
break;
}
// Mixing of tags (input tag cannot appear with a numeric tag)
if (inputTagCount == 1 && numericTagCount > 0)
{
errorSnapshot.AddError(new SimileError()
{
Text = "Input tags cannot appear on the same line as numeric tags",
DocumentName = filename,
LineNumber = lineNumber,
ColumnNumber = i
});
isError = true;
break;
}
// Multiple input tags
if (inputTagCount > 1)
{
errorSnapshot.AddError(new SimileError()
{
Text = "Cannot have more than one input tag per line",
DocumentName = filename,
LineNumber = lineNumber,
ColumnNumber = i
});
isError = true;
break;
}
}
// Was there an error anywhere on the line?
if (isError)
{
// Classify the whole line as an error
results.Add(CreateTagSpan(span, 0, span.Length, simileLineErrorType));
}
// Push the errors if they exist, otherwise clear any old ones
if (errorSnapshot.Count > 0)
SimileErrorSource.Instance.AddErrorSnapshot(span.Snapshot.TextBuffer, lineNumber, errorSnapshot);
else
SimileErrorSource.Instance.ClearErrorSnapshot(span.Snapshot.TextBuffer, lineNumber);
// Return overall results (which may be null)
return results;
}
/// <summary>
/// Helper for creating a classification span
/// </summary>
/// <param name="span">The span upon which this is based</param>
/// <param name="localStart">The local start position, which will be added to the span's start</param>
/// <param name="length">The length of this new span</param>
/// <param name="type">The classification type of this span</param>
/// <returns>A new classification span</returns>
private ClassificationSpan CreateTagSpan(SnapshotSpan span, int localStart, int length, IClassificationType type)
{
return new ClassificationSpan(new SnapshotSpan(span.Snapshot, new Span(localStart + span.Start, length)), type);
}
/// <summary>
/// Determines if the given string contains the given value starting at the specified index
/// </summary>
/// <param name="str">The string to search</param>
/// <param name="value">The string to look for</param>
/// <param name="index">The index to check</param>
/// <returns>True if the string contains the entire value starting at index, false otherwise</returns>
private bool ContainsAtIndex(string str, string value, int index)
{
int valOffset = 0;
while (
valOffset < value.Length &&
valOffset + index <= str.Length &&
value[valOffset] == str[index])
{
valOffset++;
index++;
}
// Did we make it through the search value?
return valOffset == value.Length;
}
/// <summary>
/// Gets just the filename of the document that the given snapshot belongs to
/// </summary>
/// <param name="snapshot">Snapshot of text</param>
/// <returns>Just the filename, or null if not found</returns>
private string GetDocumentFilename(ITextSnapshot snapshot)
{
// Grab the full path and strip down to filename
string path = GetDocumentPath(snapshot);
if (!string.IsNullOrEmpty(path))
return Path.GetFileName(path);
// Path/filename not found
return null;
}
/// <summary>
/// Gets the overall file path to the document that the given snapshot belongs to
/// </summary>
/// <param name="snapshot">Snapshot of text</param>
/// <returns>The full file path, or null if not found</returns>
private string GetDocumentPath(ITextSnapshot snapshot)
{
// Attempt to get the text document from the snapshot and return the filepath
if (snapshot.TextBuffer.Properties.TryGetProperty(typeof(ITextDocument), out ITextDocument doc) && doc != null)
return doc.FilePath;
// Unable to find text document
return null;
}
#endregion
}
}