A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using System.Text.RegularExpressions;
4
5
6namespace Unity.Burst.Editor
7{
8 internal struct SearchCriteria
9 {
10 internal string filter;
11 internal bool isCaseSensitive;
12 internal bool isWholeWords;
13 internal bool isRegex;
14
15 internal SearchCriteria(string keyword, bool caseSensitive, bool wholeWord, bool regex)
16 {
17 filter = keyword;
18 isCaseSensitive = caseSensitive;
19 isWholeWords = wholeWord;
20 isRegex = regex;
21 }
22
23 internal bool Equals(SearchCriteria obj) =>
24 filter == obj.filter && isCaseSensitive == obj.isCaseSensitive && isWholeWords == obj.isWholeWords && isRegex == obj.isRegex;
25
26 public override bool Equals(object obj) =>
27 obj is SearchCriteria other && Equals(other);
28
29 public override int GetHashCode() => base.GetHashCode();
30 }
31
32 internal static class BurstStringSearch
33 {
34 /// <summary>
35 /// Gets index of line end in given string, both absolute and relative to start of line.
36 /// </summary>
37 /// <param name="str">String to search in.</param>
38 /// <param name="line">Line to get end index of.</param>
39 /// <returns>(absolute line end index of string, line end index relative to line start).</returns>
40 /// <exception cref="ArgumentOutOfRangeException">
41 /// Argument must be greater than 0 and less than or equal to number of lines in
42 /// <paramref name="str" />.
43 /// </exception>
44 internal static (int total, int relative) GetEndIndexOfPlainLine (string str, int line)
45 {
46 var lastIdx = -1;
47 var newIdx = -1;
48
49 for (var i = 0; i <= line; i++)
50 {
51 lastIdx = newIdx;
52 newIdx = str.IndexOf('\n', lastIdx + 1);
53
54 if (newIdx == -1 && i < line)
55 {
56 throw new ArgumentOutOfRangeException(nameof(line),
57 "Argument must be greater than 0 and less than or equal to number of lines in str.");
58 }
59 }
60 lastIdx++;
61 return newIdx != -1 ? (newIdx, newIdx - lastIdx) : (str.Length - 1, str.Length - 1 - lastIdx);
62 }
63
64 /// <summary>
65 /// Gets index of line end in given string, both absolute and relative to start of line.
66 /// Adjusts the index so color tags are not included in relative index.
67 /// </summary>
68 /// <param name="str">String to search in.</param>
69 /// <param name="line">Line to find end of in string.</param>
70 /// <returns>(absolute line end index of string, line end index relative to line start adjusted for color tags).</returns>
71 internal static (int total, int relative) GetEndIndexOfColoredLine(string str, int line)
72 {
73 var (total, relative) = GetEndIndexOfPlainLine(str, line);
74 return RemoveColorTagFromIdx(str, total, relative);
75 }
76
77 /// <summary>
78 /// Adjusts index of color tags on line.
79 /// </summary>
80 /// <remarks>Assumes that <see cref="tidx"/> is index of something not a color tag.</remarks>
81 /// <param name="str">String containing the indexes.</param>
82 /// <param name="tidx">Total index of line end.</param>
83 /// <param name="ridx">Relative index of line end.</param>
84 /// <returns>(<see cref="tidx"/>, <see cref="ridx"/>) adjusted for color tags on line.</returns>
85 private static (int total, int relative) RemoveColorTagFromIdx(string str, int tidx, int ridx)
86 {
87 var lineStartIdx = tidx - ridx;
88 var colorTagFiller = 0;
89
90 var tmp = str.LastIndexOf("</color", tidx);
91 var lastWasStart = true;
92 var colorTagStart = str.LastIndexOf("<color=", tidx);
93
94 if (tmp > colorTagStart)
95 {
96 // color tag end was closest
97 lastWasStart = false;
98 colorTagStart = tmp;
99 }
100
101 while (colorTagStart != -1 && colorTagStart >= lineStartIdx)
102 {
103 var colorTagEnd = str.IndexOf('>', colorTagStart);
104 // +1 as the index is zero based.
105 colorTagFiller += colorTagEnd - colorTagStart + 1;
106
107 if (lastWasStart)
108 {
109 colorTagStart = str.LastIndexOf("</color", colorTagStart);
110 lastWasStart = false;
111 }
112 else
113 {
114 colorTagStart = str.LastIndexOf("<color=", colorTagStart);
115 lastWasStart = true;
116 }
117 }
118 return (tidx - colorTagFiller, ridx - colorTagFiller);
119 }
120
121 /// <summary>
122 /// Finds the zero indexed line number of given <see cref="matchIdx"/>.
123 /// </summary>
124 /// <param name="str">String to search in.</param>
125 /// <param name="matchIdx">Index to find line number of.</param>
126 /// <returns>Line number of given index in string.</returns>
127 internal static int FindLineNr(string str, int matchIdx)
128 {
129 var lineNr = 0;
130 var idxn = str.IndexOf('\n');
131
132 while (idxn != -1 && idxn < matchIdx)
133 {
134 lineNr++;
135 idxn = str.IndexOf('\n', idxn + 1);
136 }
137
138 return lineNr;
139 }
140
141 /// <summary>
142 /// Finds first match of <see cref="criteria"/> in given string.
143 /// </summary>
144 /// <param name="str">String to search in.</param>
145 /// <param name="criteria">Search options.</param>
146 /// <param name="regx">Used when <see cref="criteria"/> specifies regex search.</param>
147 /// <param name="startIdx">Index to start the search at.</param>
148 /// <returns>(start index of match, length of match)</returns>
149 internal static (int idx, int length) FindMatch(string str, SearchCriteria criteria, Regex regx, int startIdx = 0)
150 {
151 var idx = -1;
152 var len = 0;
153
154 if (criteria.isRegex)
155 {
156 // regex will have the appropriate options in it if isCaseSensitive or/and isWholeWords is true.
157 var res = regx.Match(str, startIdx);
158
159 if (res.Success) (idx, len) = (res.Index, res.Length);
160 }
161 else if (criteria.isWholeWords)
162 {
163 (idx, len) = (IndexOfWholeWord(str, startIdx, criteria.filter, criteria.isCaseSensitive
164 ? StringComparison.InvariantCulture
165 : StringComparison.InvariantCultureIgnoreCase), criteria.filter.Length);
166 }
167 else
168 {
169 unsafe
170 {
171 fixed (char* source = str)
172 {
173 fixed (char* target = criteria.filter)
174 {
175 (idx, len) = (
176 IndexOfCustom(source, str.Length, target, criteria.filter.Length, startIdx, criteria.isCaseSensitive)
177 , criteria.filter.Length);
178 }
179 }
180 }
181 }
182
183 return (idx, len);
184 }
185
186 internal static List<(int idx, int length)> FindAllMatches(string str, SearchCriteria criteria, Regex regx,
187 int startIdx = 0)
188 {
189 var retVal = new List<(int, int)>();
190
191 if (criteria.isRegex)
192 {
193 var res = regx.Matches(str, startIdx);
194
195 foreach (Match match in res)
196 {
197 retVal.Add((match.Index, match.Length));
198 }
199 }
200 else if (criteria.isWholeWords)
201 {
202 retVal.AddRange(IndexOfWholeWordAll(str, startIdx, criteria.filter,
203 criteria.isCaseSensitive
204 ? StringComparison.InvariantCulture
205 : StringComparison.CurrentCultureIgnoreCase));
206 }
207 else
208 {
209 unsafe
210 {
211 fixed (char* source = str)
212 {
213 fixed (char* target = criteria.filter)
214 {
215 retVal.AddRange(FindAllIndices(source, str.Length, target, criteria.filter.Length, startIdx, criteria.isCaseSensitive));
216 }
217 }
218 }
219 }
220
221 return retVal;
222 }
223
224 private static char ToUpper(char c) => c - 97U > 25U ? c : (char)(c - 32U);
225
226 private static unsafe int ScanForFilterInsensitive(char* str, char* filter, int flen, int i)
227 {
228 int j = 0;
229 while (j < flen && ToUpper(str[i + j]) == ToUpper(filter[j]))
230 {
231 j++;
232 }
233 return j;
234 }
235
236 private static unsafe int ScanForFilter(char* str, char* filter, int flen, int i)
237 {
238 int j = 0;
239 while (j < flen && str[i + j] == filter[j])
240 {
241 j++;
242 }
243 return j;
244 }
245
246 private static unsafe List<(int, int)> FindAllIndices(char* str, int len, char* filter, int flen, int startIdx, bool caseSensitive)
247 {
248 var retVal = new List<(int,int)>();
249 if (len < flen) { return retVal; }
250
251 int stop = len - flen;
252 if (caseSensitive)
253 {
254 for (int i = startIdx; i < stop; i++)
255 {
256 if (ScanForFilter(str, filter, flen, i) == flen)
257 {
258 retVal.Add((i, flen));
259 i += flen - 1;
260 }
261 }
262 }
263 else
264 {
265 for (int i = startIdx; i < stop; i++)
266 {
267 if (ScanForFilterInsensitive(str, filter, flen, i) == flen)
268 {
269 retVal.Add((i, flen));
270 i += flen-1;
271 }
272 }
273 }
274 return retVal;
275 }
276
277
278 /// <summary>
279 /// Finds index of first occurence of <see cref="filter"/> in <see cref="str"/>.
280 /// </summary>
281 /// <param name="str">String to search through</param>
282 /// <param name="len">Length of <see cref="str"/></param>
283 /// <param name="filter">Needle to find</param>
284 /// <param name="flen">Lenght of <see cref="filter"/></param>
285 /// <param name="startIdx">Index to start search from</param>
286 /// <param name="caseSensitive">Whether search ignore casing</param>
287 /// <returns>index of first match or -1</returns>
288 private static unsafe int IndexOfCustom(char* str, int len, char* filter, int flen, int startIdx, bool caseSensitive)
289 {
290 if (len < flen) { return -1; }
291
292 int stop = len - flen;
293 if (caseSensitive)
294 {
295 for (int i = startIdx; i < stop; i++)
296 {
297 if (ScanForFilter(str, filter, flen, i) == flen)
298 {
299 return i;
300 }
301 }
302 }
303 else
304 {
305 for (int i = startIdx; i < stop; i++)
306 {
307 if (ScanForFilterInsensitive(str, filter, flen, i) == flen)
308 {
309 return i;
310 }
311 }
312 }
313
314 return -1;
315 }
316
317 /// <summary>
318 /// Finds index of <see cref="filter"/> matching for whole words.
319 /// </summary>
320 /// <param name="str">String to search in.</param>
321 /// <param name="startIdx">Index to start search from.</param>
322 /// <param name="filter">Key to search for.</param>
323 /// <param name="opt">Options for string comparison.</param>
324 /// <returns>Index of match or -1.</returns>
325 private static int IndexOfWholeWord(string str, int startIdx, string filter, StringComparison opt)
326 {
327 const string wholeWordMatch = @"\w";
328
329 var j = startIdx;
330 var filterLen = filter.Length;
331 var strLen = str.Length;
332 while (j < strLen && (j = str.IndexOf(filter, j, opt)) >= 0)
333 {
334 var noPrior = true;
335 if (j != 0)
336 {
337 var frontBorder = str[j - 1];
338 noPrior = !Regex.IsMatch(frontBorder.ToString(), wholeWordMatch);
339 }
340
341 var noAfter = true;
342 if (j + filterLen != strLen)
343 {
344 var endBorder = str[j + filterLen];
345 noAfter = !Regex.IsMatch(endBorder.ToString(), wholeWordMatch);
346 }
347
348 if (noPrior && noAfter) return j;
349
350 j++;
351 }
352 return -1;
353 }
354
355
356 private static List<(int idx, int len)> IndexOfWholeWordAll(string str, int startIdx, string filter, StringComparison opt)
357 {
358 const string wholeWordMatch = @"\w";
359 var retVal = new List<(int, int)>();
360
361 var j = startIdx;
362 var filterLen = filter.Length;
363 var strLen = str.Length;
364 while (j < strLen && (j = str.IndexOf(filter, j, opt)) >= 0)
365 {
366 var noPrior = true;
367 if (j != 0)
368 {
369 var frontBorder = str[j - 1];
370 noPrior = !Regex.IsMatch(frontBorder.ToString(), wholeWordMatch);
371 }
372
373 var noAfter = true;
374 if (j + filterLen != strLen)
375 {
376 var endBorder = str[j + filterLen];
377 noAfter = !Regex.IsMatch(endBorder.ToString(), wholeWordMatch);
378 }
379
380 if (noPrior && noAfter)
381 {
382 retVal.Add((j, filterLen));
383 j += filterLen - 1;
384 }
385
386 j++;
387 }
388 return retVal;
389 }
390 }
391}