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

Configure Feed

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

Always include trailing newline (#747)

The updates our "line model" to fix the edge cases that led to sourcegraph/sourcegraph#60605. In short, this changes the definition of a "line" to include its terminating newline (if it exists).

Before this, we had defined a "line" as starting at the byte after a newline (or the beginning of a file) and ending at the byte before a newline (or the end of the file).

The problem with that definition is that a newline that is the last byte in the file can never successfully be matched because we would trim that from the returned content, so any ranges that would match that trailing newline would be out of bounds in the result returned to the client. That's the reason behind the panics caused by #709, which was an attempt to formalize the "line does not include a trailing newline" definition.

So, instead, this redefines a line as ending at the byte after a newline (or the end of the file). This means that a regex can successfully and safely match a terminating newline.

+134 -144
+7 -2
api.go
··· 141 141 DebugScore string 142 142 143 143 // Content is a contiguous range of complete lines that fully contains Ranges. 144 + // Lines will always include their terminating newline (if it exists). 144 145 Content []byte 145 146 146 147 // Ranges is a set of matching ranges within this chunk. Each range is relative ··· 224 225 // LineMatch holds the matches within a single line in a file. 225 226 type LineMatch struct { 226 227 // The line in which a match was found. 227 - Line []byte 228 - LineStart int 228 + Line []byte 229 + // The byte offset of the first byte of the line. 230 + LineStart int 231 + // The byte offset of the first byte past the end of the line. 232 + // This is usually the byte after the terminating newline, but can also be 233 + // the end of the file if there is no terminating newline 229 234 LineEnd int 230 235 LineNumber int 231 236
+37 -58
contentprovider.go
··· 243 243 var result []LineMatch 244 244 for len(ms) > 0 { 245 245 m := ms[0] 246 - num, lineStart, lineEnd := p.newlines().atOffset(m.byteOffset) 246 + num := p.newlines().atOffset(m.byteOffset) 247 + lineStart := int(p.newlines().lineStart(num)) 248 + nextLineStart := int(p.newlines().lineStart(num + 1)) 247 249 248 250 var lineCands []*candidateMatch 249 251 ··· 251 253 252 254 for len(ms) > 0 { 253 255 m := ms[0] 254 - if int(m.byteOffset) <= lineEnd { 256 + if int(m.byteOffset) < nextLineStart { 255 257 endMatch = m.byteOffset + m.byteMatchSz 256 258 lineCands = append(lineCands, m) 257 259 ms = ms[1:] ··· 264 266 log.Panicf( 265 267 "%s %v infinite loop: num %d start,end %d,%d, offset %d", 266 268 p.id.fileName(p.idx), p.id.metaData, 267 - num, lineStart, lineEnd, 269 + num, lineStart, nextLineStart, 268 270 m.byteOffset) 269 271 } 270 272 ··· 273 275 // Due to merging matches, we may have a match that 274 276 // crosses a line boundary. Prevent confusion by 275 277 // taking lines until we pass the last match 276 - for lineEnd < len(data) && endMatch > uint32(lineEnd) { 277 - next := bytes.IndexByte(data[lineEnd+1:], '\n') 278 + for nextLineStart < len(data) && endMatch > uint32(nextLineStart) { 279 + next := bytes.IndexByte(data[nextLineStart:], '\n') 278 280 if next == -1 { 279 - lineEnd = len(data) 281 + nextLineStart = len(data) 280 282 } else { 281 283 // TODO(hanwen): test that checks "+1" part here. 282 - lineEnd += next + 1 284 + nextLineStart += next + 1 283 285 } 284 286 } 285 287 286 288 finalMatch := LineMatch{ 287 289 LineStart: lineStart, 288 - LineEnd: lineEnd, 290 + LineEnd: nextLineStart, 289 291 LineNumber: num, 290 292 } 291 - finalMatch.Line = data[lineStart:lineEnd] 293 + finalMatch.Line = data[lineStart:nextLineStart] 292 294 293 295 if numContextLines > 0 { 294 296 finalMatch.Before = p.newlines().getLines(data, num-numContextLines, num) ··· 340 342 for _, cm := range chunk.candidates { 341 343 startOffset := cm.byteOffset 342 344 endOffset := cm.byteOffset + cm.byteMatchSz 343 - startLine, startLineOffset, _ := newlines.atOffset(startOffset) 344 - endLine, endLineOffset, _ := newlines.atOffset(endOffset) 345 + startLine, endLine := newlines.offsetRangeToLineRange(startOffset, endOffset) 345 346 346 347 ranges = append(ranges, Range{ 347 348 Start: Location{ 348 349 ByteOffset: startOffset, 349 350 LineNumber: uint32(startLine), 350 - Column: columnHelper.get(startLineOffset, startOffset), 351 + Column: columnHelper.get(int(newlines.lineStart(startLine)), startOffset), 351 352 }, 352 353 End: Location{ 353 354 ByteOffset: endOffset, 354 355 LineNumber: uint32(endLine), 355 - Column: columnHelper.get(endLineOffset, endOffset), 356 + Column: columnHelper.get(int(newlines.lineStart(endLine)), endOffset), 356 357 }, 357 358 }) 358 359 } ··· 361 362 if firstLineNumber < 1 { 362 363 firstLineNumber = 1 363 364 } 364 - firstLineStart, _ := newlines.lineBounds(firstLineNumber) 365 + firstLineStart := newlines.lineStart(firstLineNumber) 365 366 366 367 chunkMatches = append(chunkMatches, ChunkMatch{ 367 368 Content: newlines.getLines(data, firstLineNumber, int(chunk.lastLine)+numContextLines+1), ··· 399 400 for _, m := range ms { 400 401 startOffset := m.byteOffset 401 402 endOffset := m.byteOffset + m.byteMatchSz 402 - firstLine, _, _ := newlines.atOffset(startOffset) 403 - lastLine, _, _ := newlines.atOffset(endOffset) 403 + firstLine, lastLine := newlines.offsetRangeToLineRange(startOffset, endOffset) 404 404 405 405 if len(chunks) > 0 && int(chunks[len(chunks)-1].lastLine)+numContextLines >= firstLine-numContextLines { 406 406 // If a new chunk created with the current candidateMatch would ··· 471 471 } 472 472 473 473 // atOffset returns the line containing the offset. If the offset lands on 474 - // the newline ending line M, we return M. The line is characterized 475 - // by its linenumber (base-1, byte index of line start, byte index of 476 - // line end). The line end is the index of a newline, or the filesize 477 - // (if matching the last line of the file.) 478 - func (nls newlines) atOffset(offset uint32) (lineNumber, lineStart, lineEnd int) { 474 + // the newline ending line M, we return M. 475 + func (nls newlines) atOffset(offset uint32) (lineNumber int) { 479 476 idx := sort.Search(len(nls.locs), func(n int) bool { 480 477 return nls.locs[n] >= offset 481 478 }) 482 - 483 - start, end := nls.lineBounds(idx + 1) 484 - return idx + 1, int(start), int(end) 479 + return idx + 1 485 480 } 486 481 487 - // lineBounds returns the byte offsets of the start and end of the 1-based 488 - // lineNumber. The end offset is exclusive and will not contain the line-ending 489 - // newline. If the line number is out of range of the lines in the file, start 490 - // and end will be clamped to [0,fileSize]. 491 - func (nls newlines) lineBounds(lineNumber int) (start, end uint32) { 482 + // lineStart returns the byte offset of the beginning of the given line. 483 + // lineNumber is 1-based. If lineNumber is out of range of the lines in the 484 + // file, the return value will be clamped to [0,fileSize]. 485 + func (nls newlines) lineStart(lineNumber int) uint32 { 492 486 // nls.locs[0] + 1 is the start of the 2nd line of data. 493 487 startIdx := lineNumber - 2 494 - endIdx := lineNumber - 1 495 488 496 489 if startIdx < 0 { 497 - start = 0 490 + return 0 498 491 } else if startIdx >= len(nls.locs) { 499 - start = nls.fileSize 492 + return nls.fileSize 500 493 } else { 501 - start = nls.locs[startIdx] + 1 494 + return nls.locs[startIdx] + 1 502 495 } 496 + } 503 497 504 - if endIdx < 0 { 505 - end = 0 506 - } else if endIdx >= len(nls.locs) { 507 - end = nls.fileSize 508 - } else { 509 - end = nls.locs[endIdx] 510 - } 511 - 512 - return start, end 498 + // offsetRangeToLineRange returns range of lines that fully contains the given byte range. 499 + // The inputs are 0-based byte offsets into the file representing the (exclusive) range [startOffset, endOffset). 500 + // The return values are 1-based line numbers representing the (inclusive) range [startLine, endLine]. 501 + func (nls newlines) offsetRangeToLineRange(startOffset, endOffset uint32) (startLine, endLine int) { 502 + startLine = nls.atOffset(startOffset) 503 + endLine = nls.atOffset( 504 + max(startOffset, max(endOffset, 1)-1), // clamp endOffset and prevent underflow 505 + ) 506 + return startLine, endLine 513 507 } 514 508 515 509 // getLines returns a slice of data containing the lines [low, high). ··· 519 513 return nil 520 514 } 521 515 522 - lowStart, _ := nls.lineBounds(low) 523 - _, highEnd := nls.lineBounds(high - 1) 524 - 525 - // Drop any trailing newline. Editors do not treat a trailing newline as 526 - // the start of a new line, so we should not either. lineBounds clamps to 527 - // len(data) when an out-of-bounds line is requested. 528 - // 529 - // As an example, if we request lines 1-5 from a file with contents 530 - // `one\ntwo\nthree\n`, we should return `one\ntwo\nthree` because those are 531 - // the three "lines" in the file, separated by newlines. 532 - if highEnd == uint32(len(data)) && bytes.HasSuffix(data, []byte{'\n'}) { 533 - highEnd = highEnd - 1 534 - lowStart = min(lowStart, highEnd) 535 - } 536 - 537 - return data[lowStart:highEnd] 516 + return data[nls.lineStart(low):nls.lineStart(high)] 538 517 } 539 518 540 519 const (
+25 -21
contentprovider_test.go
··· 34 34 for _, content := range contents { 35 35 t.Run("", func(t *testing.T) { 36 36 newLines := getNewlines(content) 37 - // Trim the last newline before splitting because a trailing newline does not constitute a new line 38 - lines := bytes.Split(bytes.TrimSuffix(content, []byte{'\n'}), []byte{'\n'}) 37 + lines := bytes.SplitAfter(content, []byte{'\n'}) 38 + if len(lines) > 0 && len(lines[len(lines)-1]) == 0 { 39 + // A trailing newline does not delimit an empty line at the end of a file 40 + lines = lines[:len(lines)-1] 41 + } 39 42 wantGetLines := func(low, high int) []byte { 40 43 low-- 41 44 high-- ··· 51 54 if high > len(lines) { 52 55 high = len(lines) 53 56 } 54 - return bytes.Join(lines[low:high], []byte{'\n'}) 57 + return bytes.Join(lines[low:high], nil) 55 58 } 56 59 57 60 for low := -1; low <= len(lines)+2; low++ { ··· 72 75 data []byte 73 76 offset uint32 74 77 lineNumber int 75 - lineStart int 76 - lineEnd int 78 + lineStart uint32 79 + lineEnd uint32 77 80 }{{ 78 81 data: []byte("0.2.4.\n7.9.11.\n"), 79 82 offset: 0, 80 - lineNumber: 1, lineStart: 0, lineEnd: 6, 83 + lineNumber: 1, lineStart: 0, lineEnd: 7, 81 84 }, { 82 85 data: []byte("0.2.4.\n7.9.11.\n"), 83 86 offset: 6, 84 - lineNumber: 1, lineStart: 0, lineEnd: 6, 87 + lineNumber: 1, lineStart: 0, lineEnd: 7, 85 88 }, { 86 89 data: []byte("0.2.4.\n7.9.11.\n"), 87 90 offset: 2, 88 - lineNumber: 1, lineStart: 0, lineEnd: 6, 91 + lineNumber: 1, lineStart: 0, lineEnd: 7, 89 92 }, { 90 93 data: []byte("0.2.4.\n7.9.11.\n"), 91 94 offset: 2, 92 - lineNumber: 1, lineStart: 0, lineEnd: 6, 95 + lineNumber: 1, lineStart: 0, lineEnd: 7, 93 96 }, { 94 97 data: []byte("0.2.4.\n7.9.11.\n"), 95 98 offset: 7, 96 - lineNumber: 2, lineStart: 7, lineEnd: 14, 99 + lineNumber: 2, lineStart: 7, lineEnd: 15, 97 100 }, { 98 101 data: []byte("0.2.4.\n7.9.11.\n"), 99 102 offset: 11, 100 - lineNumber: 2, lineStart: 7, lineEnd: 14, 103 + lineNumber: 2, lineStart: 7, lineEnd: 15, 101 104 }, { 102 105 data: []byte("0.2.4.\n7.9.11.\n"), 103 106 offset: 15, ··· 109 112 }, { 110 113 data: []byte("\n\n"), 111 114 offset: 0, 112 - lineNumber: 1, lineStart: 0, lineEnd: 0, 115 + lineNumber: 1, lineStart: 0, lineEnd: 1, 113 116 }, { 114 117 data: []byte("\n\n"), 115 118 offset: 1, 116 - lineNumber: 2, lineStart: 1, lineEnd: 1, 119 + lineNumber: 2, lineStart: 1, lineEnd: 2, 117 120 }, { 118 121 data: []byte("\n\n"), 119 122 offset: 3, ··· 127 130 for _, tt := range cases { 128 131 t.Run("", func(t *testing.T) { 129 132 nls := getNewlines(tt.data) 130 - gotLineNumber, gotLineStart, gotLineEnd := nls.atOffset(tt.offset) 133 + gotLineNumber := nls.atOffset(tt.offset) 131 134 if gotLineNumber != tt.lineNumber { 132 135 t.Fatalf("expected line number %d, got %d", tt.lineNumber, gotLineNumber) 133 136 } 134 - if gotLineStart != tt.lineStart { 137 + if gotLineStart := nls.lineStart(gotLineNumber); gotLineStart != tt.lineStart { 135 138 t.Fatalf("expected line start %d, got %d", tt.lineStart, gotLineStart) 136 139 } 137 - if gotLineEnd != tt.lineEnd { 140 + if gotLineEnd := nls.lineStart(gotLineNumber + 1); gotLineEnd != tt.lineEnd { 138 141 t.Fatalf("expected line end %d, got %d", tt.lineEnd, gotLineEnd) 139 142 } 140 143 }) ··· 150 153 }{{ 151 154 data: []byte("0.2.4.\n7.9.11.\n"), 152 155 lineNumber: 1, 153 - start: 0, end: 6, 156 + start: 0, end: 7, 154 157 }, { 155 158 data: []byte("0.2.4.\n7.9.11.\n"), 156 159 lineNumber: 2, 157 - start: 7, end: 14, 160 + start: 7, end: 15, 158 161 }, { 159 162 data: []byte("0.2.4.\n7.9.11.\n"), 160 163 lineNumber: 0, ··· 170 173 }, { 171 174 data: []byte("\n\n"), 172 175 lineNumber: 1, 173 - start: 0, end: 0, 176 + start: 0, end: 1, 174 177 }, { 175 178 data: []byte("\n\n"), 176 179 lineNumber: 2, 177 - start: 1, end: 1, 180 + start: 1, end: 2, 178 181 }, { 179 182 data: []byte("\n\n"), 180 183 lineNumber: 3, ··· 184 187 for _, tt := range cases { 185 188 t.Run("", func(t *testing.T) { 186 189 nls := getNewlines(tt.data) 187 - gotStart, gotEnd := nls.lineBounds(tt.lineNumber) 190 + gotStart := nls.lineStart(tt.lineNumber) 188 191 if gotStart != tt.start { 189 192 t.Fatalf("expected line start %d, got %d", tt.start, gotStart) 190 193 } 194 + gotEnd := nls.lineStart(tt.lineNumber + 1) 191 195 if gotEnd != tt.end { 192 196 t.Fatalf("expected line end %d, got %d", tt.end, gotEnd) 193 197 }
+8 -8
index_test.go
··· 201 201 202 202 func TestNewlines(t *testing.T) { 203 203 b := testIndexBuilder(t, nil, 204 + // -----------------------------------------012345-678901-234 204 205 Document{Name: "filename", Content: []byte("line1\nline2\nbla")}) 205 - // ---------------------------------------------012345-678901-234 206 206 207 207 t.Run("LineMatches", func(t *testing.T) { 208 208 sres := searchForTest(t, b, &query.Substring{Pattern: "ne2"}) ··· 216 216 LineOffset: 2, 217 217 MatchLength: 3, 218 218 }}, 219 - Line: []byte("line2"), 219 + Line: []byte("line2\n"), 220 220 LineStart: 6, 221 - LineEnd: 11, 221 + LineEnd: 12, 222 222 LineNumber: 2, 223 223 }}, 224 224 }} 225 225 226 - if !reflect.DeepEqual(matches, want) { 227 - t.Errorf("got %v, want %v", matches, want) 226 + if diff := cmp.Diff(matches, want); diff != "" { 227 + t.Fatal(diff) 228 228 } 229 229 }) 230 230 ··· 235 235 want := []FileMatch{{ 236 236 FileName: "filename", 237 237 ChunkMatches: []ChunkMatch{{ 238 - Content: []byte("line2"), 238 + Content: []byte("line2\n"), 239 239 ContentStart: Location{ 240 240 ByteOffset: 6, 241 241 LineNumber: 2, ··· 269 269 } 270 270 m := matches[0] 271 271 if len(m.LineMatches) != 2 { 272 - t.Fatalf("got %d line matches, want exactly two", len(m.LineMatches)) 272 + t.Fatalf("got %d line matches, want exactly two %#v", len(m.LineMatches), m.LineMatches) 273 273 } 274 274 }) 275 275 ··· 2452 2452 res := searchForTest(t, b, q) 2453 2453 2454 2454 // 4096 (content) + 2 (overhead: newlines or doc sections) 2455 - if got, want := res.Stats.ContentBytesLoaded, int64(4098); got != want { 2455 + if got, want := res.Stats.ContentBytesLoaded, int64(4100); got != want { 2456 2456 t.Errorf("got content I/O %d, want %d", got, want) 2457 2457 } 2458 2458
+1 -1
internal/e2e/e2e_rank_test.go
··· 311 311 chunks, hidden := splitAtIndex(f.ChunkMatches, chunkMatchesPerFile) 312 312 313 313 for _, m := range chunks { 314 - _, _ = fmt.Fprintf(w, "%d:%s%s\n", m.ContentStart.LineNumber, string(m.Content), addTabIfNonEmpty(m.DebugScore)) 314 + _, _ = fmt.Fprintf(w, "%d:%s%s\n", m.ContentStart.LineNumber, strings.TrimRight(string(m.Content), "\n"), addTabIfNonEmpty(m.DebugScore)) 315 315 } 316 316 317 317 if len(hidden) > 0 {
+6 -4
matchtree.go
··· 694 694 lines := make([]lineRange, 0, len(t.children[fewestChildren].(*substrMatchTree).current)) 695 695 prev := -1 696 696 for _, candidate := range t.children[fewestChildren].(*substrMatchTree).current { 697 - line, byteStart, byteEnd := cp.newlines().atOffset(candidate.byteOffset) 697 + line := cp.newlines().atOffset(candidate.byteOffset) 698 698 if line == prev { 699 699 continue 700 700 } 701 701 prev = line 702 + byteStart := int(cp.newlines().lineStart(line)) 703 + byteEnd := int(cp.newlines().lineStart(line + 1)) 702 704 lines = append(lines, lineRange{byteStart, byteEnd}) 703 705 } 704 706 ··· 724 726 children[j] = children[j][1:] 725 727 continue nextCandidate 726 728 } 727 - if bo <= lines[i].end { 729 + if bo < lines[i].end { 728 730 hits++ 729 731 continue nextChild 730 732 } 731 - // move the `lines` iterator forward until bo <= line.end 732 - for i < len(lines) && bo > lines[i].end { 733 + // move the `lines` iterator forward until bo < line.end 734 + for i < len(lines) && bo >= lines[i].end { 733 735 i++ 734 736 } 735 737 i--
+8 -8
testdata/golden/TestReadSearch/ctagsrepo_v16.00000.golden
··· 9 9 "Language": "go", 10 10 "LineMatches": [ 11 11 { 12 - "Line": "ZnVuYyBtYWluKCkgew==", 12 + "Line": "ZnVuYyBtYWluKCkgewo=", 13 13 "LineStart": 69, 14 - "LineEnd": 82, 14 + "LineEnd": 83, 15 15 "LineNumber": 10, 16 16 "Before": null, 17 17 "After": null, ··· 39 39 "Language": "go", 40 40 "LineMatches": [ 41 41 { 42 - "Line": "cGFja2FnZSBtYWlu", 42 + "Line": "cGFja2FnZSBtYWluCg==", 43 43 "LineStart": 0, 44 - "LineEnd": 12, 44 + "LineEnd": 13, 45 45 "LineNumber": 1, 46 46 "Before": null, 47 47 "After": null, ··· 69 69 "Language": "go", 70 70 "LineMatches": [ 71 71 { 72 - "Line": "CW51bSAgICAgPSA1", 72 + "Line": "CW51bSAgICAgPSA1Cg==", 73 73 "LineStart": 34, 74 - "LineEnd": 46, 74 + "LineEnd": 47, 75 75 "LineNumber": 6, 76 76 "Before": null, 77 77 "After": null, ··· 104 104 "Language": "go", 105 105 "LineMatches": [ 106 106 { 107 - "Line": "CW1lc3NhZ2UgPSAiaGVsbG8i", 107 + "Line": "CW1lc3NhZ2UgPSAiaGVsbG8iCg==", 108 108 "LineStart": 47, 109 - "LineEnd": 65, 109 + "LineEnd": 66, 110 110 "LineNumber": 7, 111 111 "Before": null, 112 112 "After": null,
+8 -8
testdata/golden/TestReadSearch/ctagsrepo_v17.00000.golden
··· 9 9 "Language": "go", 10 10 "LineMatches": [ 11 11 { 12 - "Line": "ZnVuYyBtYWluKCkgew==", 12 + "Line": "ZnVuYyBtYWluKCkgewo=", 13 13 "LineStart": 69, 14 - "LineEnd": 82, 14 + "LineEnd": 83, 15 15 "LineNumber": 10, 16 16 "Before": null, 17 17 "After": null, ··· 39 39 "Language": "go", 40 40 "LineMatches": [ 41 41 { 42 - "Line": "cGFja2FnZSBtYWlu", 42 + "Line": "cGFja2FnZSBtYWluCg==", 43 43 "LineStart": 0, 44 - "LineEnd": 12, 44 + "LineEnd": 13, 45 45 "LineNumber": 1, 46 46 "Before": null, 47 47 "After": null, ··· 69 69 "Language": "go", 70 70 "LineMatches": [ 71 71 { 72 - "Line": "CW51bSAgICAgPSA1", 72 + "Line": "CW51bSAgICAgPSA1Cg==", 73 73 "LineStart": 34, 74 - "LineEnd": 46, 74 + "LineEnd": 47, 75 75 "LineNumber": 6, 76 76 "Before": null, 77 77 "After": null, ··· 104 104 "Language": "go", 105 105 "LineMatches": [ 106 106 { 107 - "Line": "CW1lc3NhZ2UgPSAiaGVsbG8i", 107 + "Line": "CW1lc3NhZ2UgPSAiaGVsbG8iCg==", 108 108 "LineStart": 47, 109 - "LineEnd": 65, 109 + "LineEnd": 66, 110 110 "LineNumber": 7, 111 111 "Before": null, 112 112 "After": null,
+4 -4
testdata/golden/TestReadSearch/repo17_v17.00000.golden
··· 9 9 "Language": "Go", 10 10 "LineMatches": [ 11 11 { 12 - "Line": "ZnVuYyBtYWluKCkgew==", 12 + "Line": "ZnVuYyBtYWluKCkgewo=", 13 13 "LineStart": 69, 14 - "LineEnd": 82, 14 + "LineEnd": 83, 15 15 "LineNumber": 10, 16 16 "Before": null, 17 17 "After": null, ··· 39 39 "Language": "Go", 40 40 "LineMatches": [ 41 41 { 42 - "Line": "cGFja2FnZSBtYWlu", 42 + "Line": "cGFja2FnZSBtYWluCg==", 43 43 "LineStart": 0, 44 - "LineEnd": 12, 44 + "LineEnd": 13, 45 45 "LineNumber": 1, 46 46 "Before": null, 47 47 "After": null,
+4 -4
testdata/golden/TestReadSearch/repo2_v16.00000.golden
··· 9 9 "Language": "Go", 10 10 "LineMatches": [ 11 11 { 12 - "Line": "ZnVuYyBtYWluKCkgew==", 12 + "Line": "ZnVuYyBtYWluKCkgewo=", 13 13 "LineStart": 33, 14 - "LineEnd": 46, 14 + "LineEnd": 47, 15 15 "LineNumber": 7, 16 16 "Before": null, 17 17 "After": null, ··· 39 39 "Language": "Go", 40 40 "LineMatches": [ 41 41 { 42 - "Line": "cGFja2FnZSBtYWlu", 42 + "Line": "cGFja2FnZSBtYWluCg==", 43 43 "LineStart": 0, 44 - "LineEnd": 12, 44 + "LineEnd": 13, 45 45 "LineNumber": 1, 46 46 "Before": null, 47 47 "After": null,
+4 -4
testdata/golden/TestReadSearch/repo_v16.00000.golden
··· 9 9 "Language": "Go", 10 10 "LineMatches": [ 11 11 { 12 - "Line": "ZnVuYyBtYWluKCkgew==", 12 + "Line": "ZnVuYyBtYWluKCkgewo=", 13 13 "LineStart": 69, 14 - "LineEnd": 82, 14 + "LineEnd": 83, 15 15 "LineNumber": 10, 16 16 "Before": null, 17 17 "After": null, ··· 39 39 "Language": "Go", 40 40 "LineMatches": [ 41 41 { 42 - "Line": "cGFja2FnZSBtYWlu", 42 + "Line": "cGFja2FnZSBtYWluCg==", 43 43 "LineStart": 0, 44 - "LineEnd": 12, 44 + "LineEnd": 13, 45 45 "LineNumber": 1, 46 46 "Before": null, 47 47 "After": null,
+18 -21
web/e2e_test.go
··· 393 393 { 394 394 Pre: "f", 395 395 Match: "our", 396 - Post: "th", 396 + Post: "th\n", 397 397 }, 398 398 }, 399 399 }, ··· 431 431 { 432 432 Pre: "f", 433 433 Match: "our", 434 - Post: "th", 434 + Post: "th\n", 435 435 }, 436 436 }, 437 - Before: "second snippet\nthird thing", 438 - After: "fifth block\nsixth example", 437 + Before: "second snippet\nthird thing\n", 438 + After: "fifth block\nsixth example\n", 439 439 }, 440 440 }, 441 441 }, ··· 453 453 { 454 454 Pre: "", 455 455 Match: "one", 456 - Post: " line", 456 + Post: " line\n", 457 457 }, 458 458 }, 459 - After: "second snippet\nthird thing", 459 + After: "second snippet\nthird thing\n", 460 460 }, 461 461 }, 462 462 }, ··· 477 477 Post: "", 478 478 }, 479 479 }, 480 - Before: "fifth block\nsixth example", 480 + Before: "fifth block\nsixth example\n", 481 481 }, 482 482 }, 483 483 }, ··· 498 498 Post: "", 499 499 }, 500 500 }, 501 - Before: "one line\nsecond snippet\nthird thing\nfourth\nfifth block\nsixth example", 501 + Before: "one line\nsecond snippet\nthird thing\nfourth\nfifth block\nsixth example\n", 502 502 }, 503 503 }, 504 504 }, ··· 516 516 { 517 517 Pre: "", 518 518 Match: "one", 519 - Post: " line", 519 + Post: " line\n", 520 520 }, 521 521 }, 522 522 After: "second snippet\nthird thing\nfourth\nfifth block\nsixth example\nseventh", ··· 537 537 { 538 538 Pre: "\t", 539 539 Match: "trois", 540 + Post: "\n", 540 541 }, 541 542 }, 542 - Before: "un \n ", 543 - After: " \n", 543 + Before: "un \n \n", 544 + After: " \n\n", 544 545 }, 545 546 }, 546 547 }, ··· 558 559 { 559 560 Pre: "to carry ", 560 561 Match: "water", 561 - Post: " in the no later bla", 562 + Post: " in the no later bla\n", 562 563 }, 563 564 }, 564 - // Returns 3 instead of 4 new line characters since we swallow 565 - // the last new line in Before, Fragments and After. 566 - Before: "\n\n\n", 567 - // Returns 2 instead of 3 new line characters since a 568 - // trailing newline at the end of the file does not 569 - // constitue a new line. 570 - After: "\n\n", 565 + Before: "\n\n\n\n", 566 + After: "\n\n\n", 571 567 }, 572 568 }, 573 569 }, ··· 585 581 { 586 582 Pre: "", 587 583 Match: "pastures", 584 + Post: "\n", 588 585 }, 589 586 }, 590 - Before: "green", 591 - After: "", 587 + Before: "green\n", 588 + After: "\n", 592 589 }, 593 590 }, 594 591 },
+3
web/server.go
··· 73 73 } 74 74 return fmt.Sprintf("%s...(%d bytes skipped)...", post[:limit], len(post)-limit) 75 75 }, 76 + "TrimTrailingNewline": func(s string) string { 77 + return strings.TrimSuffix(s, "\n") 78 + }, 76 79 } 77 80 78 81 const defaultNumResults = 50
+1 -1
web/templates.go
··· 245 245 {{if gt .LineNum 0}} 246 246 <tr> 247 247 <td style="background-color: rgba(238, 238, 255, 0.6);"> 248 - <pre class="inline-pre"><span class="noselect">{{if .URL}}<a href="{{.URL}}">{{end}}<u>{{.LineNum}}</u>{{if .URL}}</a>{{end}}: </span>{{range .Fragments}}{{LimitPre 100 .Pre}}<b>{{.Match}}</b>{{LimitPost 100 .Post}}{{end}} {{if .ScoreDebug}}<i>({{.ScoreDebug}})</i>{{end}}</pre> 248 + <pre class="inline-pre"><span class="noselect">{{if .URL}}<a href="{{.URL}}">{{end}}<u>{{.LineNum}}</u>{{if .URL}}</a>{{end}}: </span>{{range .Fragments}}{{LimitPre 100 .Pre}}<b>{{.Match}}</b>{{LimitPost 100 (TrimTrailingNewline .Post)}}{{end}} {{if .ScoreDebug}}<i>({{.ScoreDebug}})</i>{{end}}</pre> 249 249 </td> 250 250 </tr> 251 251 {{end}}