@mrclrchtr/supi-lsp 8.0.1 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,12 +13,7 @@ const ESCAPE = '\\'
13
13
  // The characters that carry a meaning of their own inside a regular
14
14
  // expression, so a literal one has to be escaped before it is emitted.
15
15
  const REGEX_LITERAL_SPECIAL = /[.*+?()[\]{}^$|\\/]/
16
- // A line of only spaces is blank -- the trailing-space trimming empties it --
17
- // but a line holding a tab is a pattern for a tab-named path, since git
18
- // never trims a tab.
19
- // A leading BOM is removed during compilation, so reject a line that would
20
- // become empty after removing it and trimming spaces.
21
- const REGEX_TEST_BLANK_LINE = /^\uFEFF? *$/
16
+ const BOM = '\uFEFF'
22
17
  const REGEX_INVALID_TRAILING_BACKSLASH = /(?:[^\\]|^)\\$/
23
18
  const REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION = /^\\!/
24
19
  const REGEX_REPLACE_LEADING_EXCAPED_HASH = /^\\#/
@@ -255,6 +250,11 @@ const REGEX_RESTORE_PLACEHOLDER = new RegExp(
255
250
  // into a `RegExp` -- `_make` always turns it back into a real wildcard first.
256
251
  const TRAILING_WILDCARD = '\uE000'
257
252
 
253
+ // A trailing "/**" is deferred the same way, because `checkRegex` has to tell
254
+ // it apart from a trailing "/*" (see `checkSourceOf`). For `.ignores()` it
255
+ // still never matches the folder itself (#21).
256
+ const TRAILING_DOUBLESTAR = '\uE001'
257
+
258
258
  // Replace every bracket expression with a placeholder the replacers below
259
259
  // leave alone, and translate it separately.
260
260
  const extractBrackets = pattern => {
@@ -284,11 +284,17 @@ const extractBrackets = pattern => {
284
284
  // wildcard by `TRAILING_WILDCARD` and the wildcard replacers), `\[`
285
285
  // (a literal bracket, the one `[` the bracket replacer still expects),
286
286
  // `\ ` (a quoted trailing space), and `\\` (a literal backslash). A
287
- // lone trailing backslash never reaches here -- `checkPattern` throws
288
- // it out first.
287
+ // lone trailing backslash can reach here after line-ending cleanup.
289
288
  const escaped = pattern[index + 1]
290
289
 
291
290
  if (
291
+ escaped === UNDEFINED
292
+ || escaped === SLASH && index + 2 === length
293
+ ) {
294
+ // Git removes a final directory slash before matching. An escape
295
+ // without a character after that removal makes the pattern invalid.
296
+ out += hold(NEVER_MATCH)
297
+ } else if (
292
298
  escaped === '*'
293
299
  || escaped === '['
294
300
  || escaped === SPACE
@@ -376,42 +382,6 @@ const REGEX_INNER_SLASH = /\/(?!$)/
376
382
  // against 400ns.
377
383
  const REPLACERS = [
378
384
 
379
- [
380
- // Remove BOM
381
- // TODO:
382
- // Other similar zero-width characters?
383
- /^\uFEFF/,
384
- () => EMPTY,
385
- '\uFEFF'
386
- ],
387
-
388
- [
389
- // A trailing line terminator, left on when a whole file's contents are
390
- // added as one pattern rather than split into lines. git never sees one
391
- // -- it reads a `.gitignore` line by line -- so it is not part of the
392
- // pattern and is dropped here, apart from the trailing-space trimming,
393
- // which follows git in touching spaces and nothing else.
394
- /[\r\n]+$/,
395
- () => EMPTY
396
- ],
397
-
398
- // > Trailing spaces are ignored unless they are quoted with backslash ("\")
399
- [
400
- // Only spaces, never tabs or other whitespace: git trims a trailing run
401
- // of `' '` and nothing else (dir.c, `trim_trailing_spaces`, a single
402
- // `case ' '`), so a pattern ending in a tab keeps it as a literal.
403
- // (a\ ) -> (a )
404
- // (a ) -> (a)
405
- // (a ) -> (a)
406
- // (a \ ) -> (a )
407
- /((?:\\\\)*?)(\\? +)$/,
408
- (_, m1, m2) => m1 + (
409
- m2.indexOf('\\') === 0
410
- ? SPACE
411
- : EMPTY
412
- )
413
- ],
414
-
415
385
  // Replace (\ ) with ' '
416
386
  // Only a space: an escaped tab or other whitespace is already a literal by
417
387
  // the time it reaches here, and a bare tab must be left as one, not turned
@@ -420,11 +390,14 @@ const REPLACERS = [
420
390
  // (\\ ) -> '\\ '
421
391
  // (\\\ ) -> '\\ '
422
392
  [
423
- /(\\+?) /g,
424
- (_, m1) => {
425
- const {length} = m1
426
- return m1.slice(0, length - length % 2) + SPACE
427
- }
393
+ // A run of backslashes is taken whole, from its first one, so no match
394
+ // ever starts again inside it -- `(\\+?) ` did, which made a long run
395
+ // quadratic.
396
+ /(\\+)( ?)/g,
397
+ (_, run, space) => space
398
+ ? run.slice(0, run.length - run.length % 2) + SPACE
399
+ : run,
400
+ ESCAPE + SPACE
428
401
  ],
429
402
 
430
403
  // Escape metacharacters
@@ -553,8 +526,9 @@ const REPLACERS = [
553
526
  // case: /**
554
527
  // > A trailing `"/**"` matches everything inside.
555
528
 
556
- // #21: everything inside but it should not include the current folder
557
- : '\\/.+',
529
+ // #21: everything inside but it should not include the current folder,
530
+ // resolved by `_make`
531
+ : `\\/${TRAILING_DOUBLESTAR}`,
558
532
  '*'
559
533
  ],
560
534
 
@@ -658,10 +632,11 @@ const REPLACERS = [
658
632
  source => {
659
633
  const last = source[source.length - 1]
660
634
 
661
- // The pattern is empty, or ends in the pending trailing wildcard the next
662
- // step owns. A trailing `*` that is not the marker is a literal star,
663
- // which anchors like any other final character.
664
- if (!last || last === TRAILING_WILDCARD) {
635
+ // The pattern is empty, or ends in a pending marker `_make` owns --
636
+ // the trailing wildcard, or a trailing "/**". A trailing `*` that is
637
+ // not the marker is a literal star, which anchors like any other
638
+ // final character.
639
+ if (!last || last === TRAILING_WILDCARD || last === TRAILING_DOUBLESTAR) {
665
640
  return source
666
641
  }
667
642
 
@@ -679,38 +654,27 @@ const MODE_IGNORE = 'regex'
679
654
  const MODE_CHECK_IGNORE = 'checkRegex'
680
655
  const UNDERSCORE = '_'
681
656
 
682
- const TRAILING_WILD_CARD_REPLACERS = {
683
- [MODE_IGNORE] (_, p1) {
684
- const prefix = p1
685
- // '\^':
686
- // '/*' does not match EMPTY
687
- // '/*' does not match everything
657
+ const replaceTrailingWildcard = (_, p1) => {
658
+ const prefix = p1
659
+ // '\^':
660
+ // '/*' does not match EMPTY
661
+ // '/*' does not match everything
688
662
 
689
- // '\\\/':
690
- // 'abc/*' does not match 'abc/'
691
- ? `${p1}[^/]+`
663
+ // '\\\/':
664
+ // 'abc/*' does not match 'abc/'
665
+ ? `${p1}[^/]+`
692
666
 
693
- // 'a*' matches 'a'
694
- // 'a*' matches 'aa'
695
- : '[^/]*'
667
+ // 'a*' matches 'a'
668
+ // 'a*' matches 'aa'
669
+ : '[^/]*'
696
670
 
697
- return `${prefix}(?=$|\\/$)`
698
- },
699
-
700
- [MODE_CHECK_IGNORE] (_, p1) {
701
- // When doing `git check-ignore`
702
- const prefix = p1
703
- // '\\\/':
704
- // 'abc/*' DOES match 'abc/' !
705
- ? `${p1}[^/]*`
671
+ return `${prefix}(?=$|\\/$)`
672
+ }
706
673
 
707
- // 'a*' matches 'a'
708
- // 'a*' matches 'aa'
709
- : '[^/]*'
674
+ const REGEX_REPLACE_TRAILING_DOUBLESTAR = /\uE001$/
710
675
 
711
- return `${prefix}(?=$|\\/$)`
712
- }
713
- }
676
+ // 'abc/**' matches everything under 'abc', but not 'abc/' itself (#21)
677
+ const replaceTrailingDoublestar = () => '.+(?=$|\\/$)'
714
678
 
715
679
  const WILDCARD = '[^\\/]*'
716
680
 
@@ -898,6 +862,28 @@ const makeRegexPrefix = pattern => {
898
862
  : replaced
899
863
  }
900
864
 
865
+ // `checkRegex` tests a path with a trailing slash, 'abc/', the way
866
+ // `git check-ignore` matches that literal string once the directory itself
867
+ // is known not to be excluded (see `checkIgnore`): a trailing '/' of the
868
+ // pattern is dropped, a basename pattern sees an empty basename, and only a
869
+ // pattern whose last segment is a pending wildcard can match what is left
870
+ // after the final slash -- 'abc/*' and 'abc/**' match 'abc/', and 'abc/**'
871
+ // also matches 'abc/d/' (#77, #169).
872
+ const checkSourceOf = (body, prefix) => {
873
+ if (body[body.length - 1] === SLASH) {
874
+ prefix = makeRegexPrefix(body.slice(0, - 1))
875
+ }
876
+
877
+ const head = prefix.slice(0, - 1)
878
+ const last = prefix[prefix.length - 1]
879
+
880
+ return last === TRAILING_WILDCARD
881
+ ? `${head}$`
882
+ : last === TRAILING_DOUBLESTAR
883
+ ? `${head}.*$`
884
+ : NEVER_MATCH
885
+ }
886
+
901
887
  // A trailing slash does not stop a pattern being basename-only: it restricts
902
888
  // the match to a directory, it does not let the pattern reach across one.
903
889
  // Everything else a pattern can hold -- a wildcard, a character class, an
@@ -978,15 +964,12 @@ const parentOf = path => {
978
964
 
979
965
  const isString = subject => typeof subject === 'string'
980
966
 
981
- // > A blank line matches no files, so it can serve as a separator for readability.
967
+ // Blank lines and comments are told by `createRule`, once the pattern is
968
+ // normalized.
982
969
  const checkPattern = pattern => pattern
983
970
  && isString(pattern)
984
- && !REGEX_TEST_BLANK_LINE.test(pattern)
985
971
  && !REGEX_INVALID_TRAILING_BACKSLASH.test(pattern)
986
972
 
987
- // > A line starting with # serves as a comment.
988
- && pattern.indexOf('#') !== 0
989
-
990
973
  const splitPattern = pattern => pattern
991
974
  .split(REGEX_SPLITALL_CRLF)
992
975
  .filter(Boolean)
@@ -1039,12 +1022,13 @@ class IgnoreRule {
1039
1022
  }
1040
1023
 
1041
1024
  _make (mode, key) {
1042
- const str = pinWildcards(this.regexPrefix.replace(
1043
- REGEX_REPLACE_TRAILING_WILDCARD,
1044
-
1045
- // It does not need to bind pattern
1046
- TRAILING_WILD_CARD_REPLACERS[mode]
1047
- ))
1025
+ const str = pinWildcards(
1026
+ mode === MODE_IGNORE
1027
+ ? this.regexPrefix
1028
+ .replace(REGEX_REPLACE_TRAILING_WILDCARD, replaceTrailingWildcard)
1029
+ .replace(REGEX_REPLACE_TRAILING_DOUBLESTAR, replaceTrailingDoublestar)
1030
+ : checkSourceOf(this.body, this.regexPrefix)
1031
+ )
1048
1032
 
1049
1033
  const regex = this.ignoreCase
1050
1034
  ? new RegExp(str, 'i')
@@ -1054,17 +1038,79 @@ class IgnoreRule {
1054
1038
  }
1055
1039
  }
1056
1040
 
1041
+ const isLineEnd = char => char === '\r' || char === '\n'
1042
+
1043
+ // Drop a line terminator, which can remain when patterns are passed as an
1044
+ // array, and then the trailing spaces. Git trims spaces only, and a space
1045
+ // escaped by an odd run of backslashes stays, unescaped, as the last one.
1046
+ // Scanned from the end: a regular expression can only try each start in
1047
+ // turn, which is quadratic over a long run of spaces or backslashes.
1048
+ // 'a ' -> 'a'; 'a\ ' -> 'a '; 'a\\ ' -> 'a\\'; 'a\t' -> 'a\t'
1049
+ const trimEnd = body => {
1050
+ let end = body.length
1051
+
1052
+ while (end && isLineEnd(body[end - 1])) {
1053
+ end --
1054
+ }
1055
+
1056
+ const lineEnd = end
1057
+
1058
+ while (end && body[end - 1] === SPACE) {
1059
+ end --
1060
+ }
1061
+
1062
+ if (end === lineEnd) {
1063
+ return body.slice(0, end)
1064
+ }
1065
+
1066
+ let backslashes = 0
1067
+
1068
+ while (backslashes < end && body[end - backslashes - 1] === ESCAPE) {
1069
+ backslashes ++
1070
+ }
1071
+
1072
+ return backslashes % 2
1073
+ ? body.slice(0, end - 1) + SPACE
1074
+ : body.slice(0, end)
1075
+ }
1076
+
1057
1077
  const createRule = ({
1058
1078
  pattern,
1059
1079
  mark
1060
1080
  }, ignoreCase) => {
1081
+ // Git drops a BOM before it reads the first line, so before anything else.
1082
+ let body = pattern[0] === BOM
1083
+ ? pattern.slice(1)
1084
+ : pattern
1085
+
1086
+ // > A line starting with # serves as a comment.
1087
+ if (body[0] === '#') {
1088
+ return
1089
+ }
1090
+
1061
1091
  let negative = false
1062
- let body = pattern
1063
1092
 
1064
1093
  // > An optional prefix "!" which negates the pattern;
1065
- if (body.indexOf('!') === 0) {
1094
+ if (body[0] === '!') {
1066
1095
  negative = true
1067
- body = body.substr(1)
1096
+ body = body.slice(1)
1097
+ }
1098
+
1099
+ // Normalize the body before checking which slashes anchor the pattern.
1100
+ // Otherwise `bar/ ` is mistaken for a pattern containing an inner slash.
1101
+ // Only a body ending in a space or a line terminator has anything to trim.
1102
+ const last = body[body.length - 1]
1103
+
1104
+ if (last === SPACE || last === '\r' || last === '\n') {
1105
+ body = trimEnd(body)
1106
+ }
1107
+
1108
+ // > A blank line matches no files, so it can serve as a separator for
1109
+ // > readability.
1110
+ // Nor does a lone "!", or anything else left empty here: an empty pattern
1111
+ // would compile to a regex matching every path.
1112
+ if (!body) {
1113
+ return
1068
1114
  }
1069
1115
 
1070
1116
  body = body
@@ -1123,8 +1169,10 @@ class RuleManager {
1123
1169
  }
1124
1170
  }
1125
1171
 
1126
- if (checkPattern(pattern.pattern)) {
1127
- const rule = createRule(pattern, this._ignoreCase)
1172
+ const rule = checkPattern(pattern.pattern)
1173
+ && createRule(pattern, this._ignoreCase)
1174
+
1175
+ if (rule) {
1128
1176
  this._added = true
1129
1177
  this._rules.push(rule)
1130
1178
 
@@ -1147,9 +1195,54 @@ class RuleManager {
1147
1195
  : pattern
1148
1196
  ).forEach(this._add, this)
1149
1197
 
1198
+ if (this._added) {
1199
+ this._literalRules = UNDEFINED
1200
+ }
1201
+
1150
1202
  return this._added
1151
1203
  }
1152
1204
 
1205
+ // Match the literal 'abc/' for `checkIgnore`, last rule wins. Only a rule
1206
+ // ending in a wildcard can match it (see `checkSourceOf`), so the rest
1207
+ // are left out once, rather than tested or compiled for every path.
1208
+ testLiteral (path) {
1209
+ const rules = this._literalRules || (
1210
+ this._literalRules = this._rules.filter(
1211
+ ({body}) => body[body.length - (
1212
+ body[body.length - 1] === SLASH ? 2 : 1
1213
+ )] === '*'
1214
+ )
1215
+ )
1216
+
1217
+ let ignored = false
1218
+ let unignored = false
1219
+ let matchedRule
1220
+
1221
+ for (let index = rules.length - 1; index >= 0; index --) {
1222
+ const rule = rules[index]
1223
+
1224
+ if (rule.checkRegex.test(path)) {
1225
+ ignored = !rule.negative
1226
+ unignored = rule.negative
1227
+ matchedRule = rule.negative
1228
+ ? UNDEFINED
1229
+ : rule
1230
+ break
1231
+ }
1232
+ }
1233
+
1234
+ const ret = {
1235
+ ignored,
1236
+ unignored
1237
+ }
1238
+
1239
+ if (matchedRule) {
1240
+ ret.rule = matchedRule
1241
+ }
1242
+
1243
+ return ret
1244
+ }
1245
+
1153
1246
  // Test one single path without recursively checking parent directories
1154
1247
  //
1155
1248
  // - checkUnignored `boolean` whether should check if the path is unignored,
@@ -1361,17 +1454,20 @@ class Ignore {
1361
1454
  return this.test(path)
1362
1455
  }
1363
1456
 
1364
- const parentPath = parentOf(path)
1365
-
1366
- if (parentPath) {
1367
- const parent = this._t(parentPath, this._testCache, true)
1457
+ // Like `git check-ignore`: a directory excluded by itself or by an
1458
+ // ancestor settles it, and only then is the literal 'abc/' matched
1459
+ // against the rules (see `checkSourceOf`).
1460
+ const dir = this._t(path, this._testCache, true)
1368
1461
 
1369
- if (parent.ignored) {
1370
- return parent
1371
- }
1462
+ if (dir.ignored) {
1463
+ return dir
1372
1464
  }
1373
1465
 
1374
- return this._rules.test(path, false, MODE_CHECK_IGNORE)
1466
+ const literal = this._rules.testLiteral(path)
1467
+
1468
+ return literal.ignored || literal.unignored
1469
+ ? literal
1470
+ : dir
1375
1471
  }
1376
1472
 
1377
1473
  _t (