fork of https://github.com/sourcegraph/zoekt
0

Configure Feed

Select the types of activity you want to include in your feed.

Basic regexp support.

+411 -45
+10 -6
dociter.go
··· 41 41 } 42 42 43 43 type candidateMatch struct { 44 - query *SubstringQuery 44 + caseSensitive bool 45 + fileName bool 45 46 46 47 substrBytes []byte 47 48 substrLowered []byte ··· 49 50 caseMask [][]byte 50 51 caseBits [][]byte 51 52 52 - file uint32 53 - offset uint32 53 + file uint32 54 + offset uint32 55 + matchSz uint32 54 56 } 55 57 56 58 func (m *candidateMatch) String() string { ··· 58 60 } 59 61 60 62 func (m *candidateMatch) caseMatches(fileCaseBits []byte) bool { 61 - if !m.query.CaseSensitive { 63 + if !m.caseSensitive { 62 64 return true 63 65 } 64 66 patLen := len(m.substrBytes) ··· 83 85 } 84 86 85 87 func (m *candidateMatch) matchContent(content []byte) bool { 86 - return bytes.Compare(content[m.offset:m.offset+uint32(len(m.substrLowered))], m.substrLowered) == 0 88 + return bytes.Compare(content[m.offset:m.offset+uint32(m.matchSz)], m.substrLowered) == 0 87 89 } 88 90 89 91 func (m *candidateMatch) line(newlines []uint32, fileSize uint32) (lineNum, lineStart, lineEnd int) { ··· 141 143 &candidateMatch{ 142 144 caseMask: caseMasks, 143 145 caseBits: caseBits, 144 - query: s.query, 146 + caseSensitive: s.query.CaseSensitive, 147 + fileName: s.query.FileName, 145 148 substrBytes: patBytes, 146 149 substrLowered: lowerPatBytes, 150 + matchSz: uint32(len(lowerPatBytes)), 147 151 file: uint32(s.fileIdx), 148 152 offset: p1 - fileStart - s.leftPad, 149 153 })
+173 -34
eval.go
··· 3 3 import ( 4 4 "fmt" 5 5 "log" 6 + "regexp" 6 7 ) 7 8 8 9 var _ = log.Println ··· 10 11 // An expression tree coupled with matches 11 12 type matchTree interface { 12 13 // returns whether this matches, and if we are sure. 13 - matches(known map[matchTree]bool, docID uint32) (match bool, sure bool) 14 + matches(known map[matchTree]bool) (match bool, sure bool) 15 + 16 + // clears any per-document state of the matchTree, and prepares for 17 + // evaluating the given doc 18 + prepare(nextDoc uint32) 14 19 String() string 15 20 } 16 21 ··· 26 31 child matchTree 27 32 } 28 33 34 + type regexpMatchTree struct { 35 + query *RegexpQuery 36 + regexp *regexp.Regexp 37 + child matchTree 38 + 39 + // mutable 40 + reEvaluated bool 41 + found []*candidateMatch 42 + } 43 + 29 44 type substrMatchTree struct { 30 - query *SubstringQuery 45 + query *SubstringQuery 46 + 47 + cands []*candidateMatch 48 + coversContent bool 49 + 50 + // mutable 31 51 current []*candidateMatch 32 52 caseEvaluated bool 33 53 contEvaluated bool 34 - cands []*candidateMatch 35 - coversContent bool 36 54 } 37 55 38 56 type branchQueryMatchTree struct { 39 57 fileMasks []uint32 40 58 mask uint32 59 + 60 + // mutable 61 + docID uint32 62 + } 63 + 64 + // prepare 65 + func (t *andMatchTree) prepare(doc uint32) { 66 + for _, c := range t.children { 67 + c.prepare(doc) 68 + } 69 + } 70 + 71 + func (t *regexpMatchTree) prepare(doc uint32) { 72 + t.found = t.found[:0] 73 + t.reEvaluated = false 74 + t.child.prepare(doc) 75 + } 76 + 77 + func (t *orMatchTree) prepare(doc uint32) { 78 + for _, c := range t.children { 79 + c.prepare(doc) 80 + } 81 + } 82 + 83 + func (t *notMatchTree) prepare(doc uint32) { 84 + t.child.prepare(doc) 41 85 } 42 86 87 + func (t *substrMatchTree) prepare(nextDoc uint32) { 88 + for len(t.cands) > 0 && t.cands[0].file < nextDoc { 89 + t.cands = t.cands[1:] 90 + } 91 + 92 + i := 0 93 + for ; i < len(t.cands) && t.cands[i].file == nextDoc; i++ { 94 + } 95 + t.current = t.cands[:i] 96 + t.cands = t.cands[i:] 97 + t.contEvaluated = false 98 + t.caseEvaluated = false 99 + } 100 + 101 + func (t *branchQueryMatchTree) prepare(doc uint32) { 102 + t.docID = doc 103 + } 104 + 105 + // String. 43 106 func (t *andMatchTree) String() string { 44 107 return fmt.Sprintf("and%v", t.children) 45 108 } 46 109 110 + func (t *regexpMatchTree) String() string { 111 + return fmt.Sprintf("re(%s,%s)", t.regexp, t.child) 112 + } 113 + 47 114 func (t *orMatchTree) String() string { 48 - return fmt.Sprintf("and%v", t.children) 115 + return fmt.Sprintf("or%v", t.children) 49 116 } 50 117 51 118 func (t *notMatchTree) String() string { ··· 70 137 for _, ch := range s.children { 71 138 collectPositiveSubstrings(ch, f) 72 139 } 140 + case *regexpMatchTree: 141 + collectPositiveSubstrings(s.child, f) 73 142 case *notMatchTree: 74 143 case *substrMatchTree: 75 144 f(s) 76 145 } 77 146 } 78 147 148 + func collectRegexps(t matchTree, f func(*regexpMatchTree)) { 149 + switch s := t.(type) { 150 + case *andMatchTree: 151 + for _, ch := range s.children { 152 + collectRegexps(ch, f) 153 + } 154 + case *orMatchTree: 155 + for _, ch := range s.children { 156 + collectRegexps(ch, f) 157 + } 158 + case *regexpMatchTree: 159 + f(s) 160 + } 161 + } 162 + 79 163 func visitMatches(t matchTree, known map[matchTree]bool, f func(matchTree)) { 80 164 switch s := t.(type) { 81 165 case *andMatchTree: ··· 106 190 }) 107 191 } 108 192 193 + func visitRegexMatches(t matchTree, known map[matchTree]bool, f func(*regexpMatchTree)) { 194 + visitMatches(t, known, func(mt matchTree) { 195 + st, ok := mt.(*regexpMatchTree) 196 + if ok { 197 + f(st) 198 + } 199 + }) 200 + } 201 + 109 202 func (p *contentProvider) evalContentMatches(s *substrMatchTree) { 110 203 if !s.coversContent { 111 204 pruned := s.current[:0] ··· 119 212 s.contEvaluated = true 120 213 } 121 214 215 + func (p *contentProvider) evalRegexpMatches(s *regexpMatchTree) { 216 + idxs := s.regexp.FindAllIndex(p.data(false), -1) 217 + for _, idx := range idxs { 218 + s.found = append(s.found, &candidateMatch{ 219 + offset: uint32(idx[0]), 220 + matchSz: uint32(idx[1] - idx[0]), 221 + }) 222 + } 223 + s.reEvaluated = true 224 + } 225 + 122 226 func (p *contentProvider) evalCaseMatches(s *substrMatchTree) { 123 227 if s.query.CaseSensitive { 124 228 pruned := s.current[:0] ··· 132 236 s.caseEvaluated = true 133 237 } 134 238 135 - func (t *andMatchTree) matches(known map[matchTree]bool, docID uint32) (bool, bool) { 239 + func (t *andMatchTree) matches(known map[matchTree]bool) (bool, bool) { 136 240 sure := true 137 241 138 242 for _, ch := range t.children { 139 - v, ok := evalMatchTree(known, ch, docID) 243 + v, ok := evalMatchTree(known, ch) 140 244 if ok && !v { 141 245 return false, true 142 246 } ··· 147 251 return true, sure 148 252 } 149 253 150 - func (t *orMatchTree) matches(known map[matchTree]bool, docID uint32) (bool, bool) { 254 + func (t *orMatchTree) matches(known map[matchTree]bool) (bool, bool) { 151 255 sure := true 152 256 for _, ch := range t.children { 153 - v, ok := evalMatchTree(known, ch, docID) 257 + v, ok := evalMatchTree(known, ch) 154 258 if ok { 155 259 if v { 156 260 return true, true ··· 162 266 return false, sure 163 267 } 164 268 165 - func (t *branchQueryMatchTree) matches(known map[matchTree]bool, docID uint32) (bool, bool) { 166 - return t.fileMasks[docID]&t.mask != 0, true 269 + func (t *branchQueryMatchTree) matches(known map[matchTree]bool) (bool, bool) { 270 + return t.fileMasks[t.docID]&t.mask != 0, true 167 271 } 168 272 169 - func evalMatchTree(known map[matchTree]bool, mt matchTree, docID uint32) (bool, bool) { 273 + func (t *regexpMatchTree) matches(known map[matchTree]bool) (bool, bool) { 274 + v, ok := evalMatchTree(known, t.child) 275 + if ok && !v { 276 + return false, true 277 + } 278 + 279 + if !t.reEvaluated { 280 + return false, false 281 + } 282 + 283 + return len(t.found) > 0, true 284 + } 285 + 286 + func evalMatchTree(known map[matchTree]bool, mt matchTree) (bool, bool) { 170 287 if v, ok := known[mt]; ok { 171 288 return v, true 172 289 } 173 290 174 - v, ok := mt.matches(known, docID) 291 + v, ok := mt.matches(known) 175 292 if ok { 176 293 known[mt] = v 177 294 } ··· 179 296 return v, ok 180 297 } 181 298 182 - func (t *notMatchTree) matches(known map[matchTree]bool, docID uint32) (bool, bool) { 183 - v, ok := evalMatchTree(known, t.child, docID) 299 + func (t *notMatchTree) matches(known map[matchTree]bool) (bool, bool) { 300 + v, ok := evalMatchTree(known, t.child) 184 301 return !v, ok 185 302 } 186 303 187 - func (t *substrMatchTree) matches(known map[matchTree]bool, docID uint32) (bool, bool) { 304 + func (t *substrMatchTree) matches(known map[matchTree]bool) (bool, bool) { 188 305 if len(t.current) == 0 { 189 306 return false, true 190 307 } ··· 195 312 196 313 func (d *indexData) newMatchTree(q Query, sq map[*SubstringQuery]*substrMatchTree) (matchTree, error) { 197 314 switch s := q.(type) { 315 + case *RegexpQuery: 316 + subQ := regexpToQuery(s.Regexp) 317 + subMT, err := d.newMatchTree(subQ, sq) 318 + if err != nil { 319 + return nil, err 320 + } 321 + 322 + return &regexpMatchTree{ 323 + regexp: regexp.MustCompile(s.Regexp.String()), 324 + child: subMT, 325 + }, nil 198 326 case *AndQuery: 199 327 var r []matchTree 200 328 for _, ch := range s.Children { ··· 260 388 collectPositiveSubstrings(mt, func(sq *substrMatchTree) { 261 389 positiveAtoms = append(positiveAtoms, sq) 262 390 }) 391 + 392 + var regexpAtoms []*regexpMatchTree 393 + collectRegexps(mt, func(re *regexpMatchTree) { 394 + regexpAtoms = append(regexpAtoms, re) 395 + }) 396 + 263 397 for _, st := range atoms { 264 398 if st.query.FileName { 265 399 fileAtoms = append(fileAtoms, st) ··· 281 415 } 282 416 283 417 res.Stats.FilesConsidered++ 284 - for _, st := range atoms { 285 - for len(st.cands) > 0 && st.cands[0].file < nextDoc { 286 - st.cands = st.cands[1:] 287 - } 288 - 289 - i := 0 290 - for ; i < len(st.cands) && st.cands[i].file == nextDoc; i++ { 291 - } 292 - st.current = st.cands[:i] 293 - st.cands = st.cands[i:] 294 - st.contEvaluated = false 295 - st.caseEvaluated = false 296 - } 418 + mt.prepare(nextDoc) 297 419 298 420 var fileStart uint32 299 421 if nextDoc > 0 { ··· 308 430 } 309 431 310 432 known := make(map[matchTree]bool) 311 - if v, ok := evalMatchTree(known, mt, nextDoc); ok && !v { 433 + if v, ok := evalMatchTree(known, mt); ok && !v { 312 434 continue nextFileMatch 313 435 } 314 436 ··· 318 440 cp.evalCaseMatches(st) 319 441 cp.evalContentMatches(st) 320 442 } 321 - if v, ok := evalMatchTree(known, mt, nextDoc); ok && !v { 443 + if v, ok := evalMatchTree(known, mt); ok && !v { 322 444 continue nextFileMatch 323 445 } 324 446 } ··· 327 449 cp.evalCaseMatches(st) 328 450 } 329 451 330 - if v, ok := evalMatchTree(known, mt, nextDoc); ok && !v { 452 + if v, ok := evalMatchTree(known, mt); ok && !v { 331 453 continue nextFileMatch 332 454 } 333 455 ··· 336 458 cp.evalContentMatches(st) 337 459 } 338 460 339 - if v, ok := evalMatchTree(known, mt, nextDoc); !ok { 461 + if len(regexpAtoms) > 0 { 462 + if v, ok := evalMatchTree(known, mt); ok && !v { 463 + continue nextFileMatch 464 + } 465 + 466 + for _, re := range regexpAtoms { 467 + cp.evalRegexpMatches(re) 468 + } 469 + } 470 + 471 + if v, ok := evalMatchTree(known, mt); !ok { 340 472 panic("did not decide") 341 473 } else if !v { 342 474 continue nextFileMatch ··· 354 486 visitSubtreeMatches(mt, known, func(s *substrMatchTree) { 355 487 for _, c := range s.current { 356 488 fileMatch.Matches = append(fileMatch.Matches, cp.fillMatch(c)) 357 - if !c.query.FileName { 489 + if !c.fileName { 358 490 foundContentMatch = true 359 491 } 492 + } 493 + }) 494 + 495 + visitRegexMatches(mt, known, func(re *regexpMatchTree) { 496 + for _, c := range re.found { 497 + foundContentMatch = true 498 + fileMatch.Matches = append(fileMatch.Matches, cp.fillMatch(c)) 360 499 } 361 500 }) 362 501
+61
index_test.go
··· 458 458 Pattern: "helpers.go", 459 459 FileName: true, 460 460 }) 461 + clearScores(sres) 461 462 462 463 if err != nil { 463 464 t.Fatalf("Search: %v", err) ··· 578 579 t.Errorf("got %#v, want no FilesLoaded", sres.Stats) 579 580 } 580 581 } 582 + 583 + func TestRegexp(t *testing.T) { 584 + b := NewIndexBuilder() 585 + 586 + content := []byte("needle the bla") 587 + // ----------------01234567890123 588 + b.AddFile("f1", content) 589 + 590 + searcher := searcherForTest(t, b) 591 + sres, err := searcher.Search( 592 + &RegexpQuery{ 593 + mustParseRE("dle.*bla"), 594 + }) 595 + 596 + if err != nil { 597 + t.Fatalf("Search: %v", err) 598 + } 599 + clearScores(sres) 600 + if len(sres.Files) != 1 || len(sres.Files[0].Matches) != 1 { 601 + t.Fatalf("got %v, want 1 match in 1 file", sres.Files) 602 + } 603 + 604 + got := sres.Files[0].Matches[0] 605 + want := Match{ 606 + LineOff: 3, 607 + Offset: 3, 608 + MatchLength: 11, 609 + Line: content, 610 + FileName: false, 611 + LineNum: 1, 612 + LineStart: 0, 613 + LineEnd: 14, 614 + } 615 + 616 + if !reflect.DeepEqual(got, want) { 617 + t.Errorf("got %#v, want %#v", got, want) 618 + } 619 + } 620 + 621 + func TestRegexpOrder(t *testing.T) { 622 + b := NewIndexBuilder() 623 + 624 + content := []byte("bla the needle") 625 + // ----------------01234567890123 626 + b.AddFile("f1", content) 627 + 628 + searcher := searcherForTest(t, b) 629 + sres, err := searcher.Search( 630 + &RegexpQuery{ 631 + mustParseRE("dle.*bla"), 632 + }) 633 + 634 + if err != nil { 635 + t.Fatalf("Search: %v", err) 636 + } 637 + clearScores(sres) 638 + if len(sres.Files) != 0 { 639 + t.Fatalf("got %v, want 0 matches", sres.Files) 640 + } 641 + }
+22
parse.go
··· 18 18 "bytes" 19 19 "fmt" 20 20 "log" 21 + "regexp/syntax" 21 22 ) 22 23 23 24 var _ = log.Printf ··· 63 64 var casePrefix = []byte("case:") 64 65 var filePrefix = []byte("file:") 65 66 var branchPrefix = []byte("branch:") 67 + var regexPrefix = []byte("regex:") 66 68 67 69 type setCase string 68 70 ··· 131 133 return string(arg), n, ok, err 132 134 } 133 135 136 + func tryConsumeRegexp(in []byte) (string, int, bool, error) { 137 + arg, n, ok, err := consumeKeyword(in, regexPrefix) 138 + return string(arg), n, ok, err 139 + } 140 + 134 141 func Parse(qStr string) (Query, error) { 135 142 b := []byte(qStr) 136 143 ··· 180 187 } else if ok { 181 188 add(&BranchQuery{ 182 189 Name: fn, 190 + }) 191 + b = b[n:] 192 + continue 193 + } 194 + 195 + if arg, n, ok, err := tryConsumeRegexp(b); err != nil { 196 + return nil, err 197 + } else if ok { 198 + r, err := syntax.Parse(arg, 0) 199 + if err != nil { 200 + return nil, err 201 + } 202 + 203 + add(&RegexpQuery{ 204 + Regexp: r, 183 205 }) 184 206 b = b[n:] 185 207 continue
+12
parse_test.go
··· 16 16 17 17 import ( 18 18 "reflect" 19 + "regexp/syntax" 19 20 "testing" 20 21 ) 22 + 23 + func mustParseRE(s string) *syntax.Regexp { 24 + r, err := syntax.Parse(s, 0) 25 + if err != nil { 26 + panic(err) 27 + } 28 + 29 + return r 30 + } 21 31 22 32 func TestParseQuery(t *testing.T) { 23 33 type testcase struct { ··· 44 54 &SubstringQuery{Pattern: "helpers.go", FileName: true}, 45 55 &SubstringQuery{Pattern: "byte"}, 46 56 }}, false}, 57 + 58 + {"regex:abc[p-q]", &RegexpQuery{mustParseRE("abc[p-q]")}, false}, 47 59 48 60 // case 49 61 {"abc case:yes", &SubstringQuery{Pattern: "abc", CaseSensitive: true}, false},
+10
query.go
··· 18 18 "fmt" 19 19 "log" 20 20 "reflect" 21 + "regexp/syntax" 21 22 "strings" 22 23 ) 23 24 ··· 26 27 // Query is a representation for a possibly hierarchical search query. 27 28 type Query interface { 28 29 String() string 30 + } 31 + 32 + // RegexpQuery is a query looking for regular expressions matches. 33 + type RegexpQuery struct { 34 + Regexp *syntax.Regexp 35 + } 36 + 37 + func (q *RegexpQuery) String() string { 38 + return fmt.Sprintf("regex:%q", q.Regexp.String()) 29 39 } 30 40 31 41 // SubstringQuery is the most basic query: a query for a substring.
+43
regexp.go
··· 1 + package zoekt 2 + 3 + import ( 4 + "regexp/syntax" 5 + ) 6 + 7 + // regexpToQuery tries to distill a substring search query that 8 + // matches a superset of the regexp. 9 + func regexpToQuery(r *syntax.Regexp) Query { 10 + // TODO - we could perhaps transform Begin/EndText in '\n'? 11 + // TODO - we could perhaps transform CharClass in (OrQuery ) 12 + // if there are just a few runes, and part of a OpConcat? 13 + switch r.Op { 14 + case syntax.OpLiteral: 15 + s := string(r.Rune) 16 + if len(s) >= ngramSize { 17 + return &SubstringQuery{Pattern: s} 18 + } 19 + case syntax.OpCapture: 20 + return regexpToQuery(r.Sub[0]) 21 + 22 + case syntax.OpPlus: 23 + return regexpToQuery(r.Sub[0]) 24 + 25 + case syntax.OpRepeat: 26 + if r.Min >= 1 { 27 + return regexpToQuery(r.Sub[0]) 28 + } 29 + 30 + case syntax.OpConcat, syntax.OpAlternate: 31 + var qs []Query 32 + for _, sr := range r.Sub { 33 + if sq := regexpToQuery(sr); sq != nil { 34 + qs = append(qs, sq) 35 + } 36 + } 37 + if r.Op == syntax.OpConcat { 38 + return &AndQuery{qs} 39 + } 40 + return &OrQuery{qs} 41 + } 42 + return nil 43 + }
+75
regexp_test.go
··· 1 + package zoekt 2 + 3 + import ( 4 + "log" 5 + "reflect" 6 + "regexp/syntax" 7 + "strings" 8 + "testing" 9 + ) 10 + 11 + var opnames = map[syntax.Op]string{ 12 + syntax.OpNoMatch: "OpNoMatch", 13 + syntax.OpEmptyMatch: "OpEmptyMatch", 14 + syntax.OpLiteral: "OpLiteral", 15 + syntax.OpCharClass: "OpCharClass", 16 + syntax.OpAnyCharNotNL: "OpAnyCharNotNL", 17 + syntax.OpAnyChar: "OpAnyChar", 18 + syntax.OpBeginLine: "OpBeginLine", 19 + syntax.OpEndLine: "OpEndLine", 20 + syntax.OpBeginText: "OpBeginText", 21 + syntax.OpEndText: "OpEndText", 22 + syntax.OpWordBoundary: "OpWordBoundary", 23 + syntax.OpNoWordBoundary: "OpNoWordBoundary", 24 + syntax.OpCapture: "OpCapture", 25 + syntax.OpStar: "OpStar", 26 + syntax.OpPlus: "OpPlus", 27 + syntax.OpQuest: "OpQuest", 28 + syntax.OpRepeat: "OpRepeat", 29 + syntax.OpConcat: "OpConcat", 30 + syntax.OpAlternate: "OpAlternate", 31 + } 32 + 33 + func printRegexp(r *syntax.Regexp, lvl int) { 34 + log.Printf("%s%s ch: %d", strings.Repeat(" ", lvl), opnames[r.Op], len(r.Sub)) 35 + for _, s := range r.Sub { 36 + printRegexp(s, lvl+1) 37 + } 38 + } 39 + 40 + func TestRegexpParse(t *testing.T) { 41 + type testcase struct { 42 + in string 43 + want Query 44 + } 45 + 46 + cases := []testcase{ 47 + {"(foo|bar)baz.*bla", &AndQuery{[]Query{ 48 + &OrQuery{[]Query{ 49 + &SubstringQuery{Pattern: "foo"}, 50 + &SubstringQuery{Pattern: "bar"}, 51 + }}, 52 + &SubstringQuery{Pattern: "baz"}, 53 + &SubstringQuery{Pattern: "bla"}, 54 + }}}, 55 + 56 + {"^[a-z](People)+barrabas$", 57 + &AndQuery{[]Query{ 58 + &SubstringQuery{Pattern: "People"}, 59 + &SubstringQuery{Pattern: "barrabas"}, 60 + }}}, 61 + } 62 + 63 + for _, c := range cases { 64 + r, err := syntax.Parse(c.in, 0) 65 + if err != nil { 66 + t.Errorf("Parse(%q): %v", c.in, err) 67 + continue 68 + } 69 + 70 + got := regexpToQuery(r) 71 + if !reflect.DeepEqual(c.want, got) { 72 + t.Errorf("regexpToQuery(%q): got %v, want %v", c.in, got, c.want) 73 + } 74 + } 75 + }
+5 -5
search.go
··· 64 64 } 65 65 66 66 func (p *contentProvider) caseMatches(m *candidateMatch) bool { 67 - return m.caseMatches(p.caseBits(m.query.FileName)) 67 + return m.caseMatches(p.caseBits(m.fileName)) 68 68 } 69 69 70 70 func (p *contentProvider) matchContent(m *candidateMatch) bool { 71 - return m.matchContent(p.data(m.query.FileName)) 71 + return m.matchContent(p.data(m.fileName)) 72 72 } 73 73 74 74 func (p *contentProvider) fillMatch(m *candidateMatch) Match { 75 - if m.query.FileName { 75 + if m.fileName { 76 76 return Match{ 77 77 Offset: m.offset, 78 78 Line: p.data(true), 79 79 LineOff: int(m.offset), 80 - MatchLength: len(m.substrBytes), 80 + MatchLength: int(m.matchSz), 81 81 FileName: true, 82 82 } 83 83 } ··· 89 89 LineEnd: end, 90 90 LineNum: num, 91 91 LineOff: int(m.offset) - start, 92 - MatchLength: len(m.substrBytes), 92 + MatchLength: int(m.matchSz), 93 93 } 94 94 finalMatch.Line = toOriginal(p.data(false), p.caseBits(false), start, end) 95 95 finalMatch.Score = matchScore(&finalMatch)