diff --git a/api.go b/api.go index 7fd6a318d..faa7705d4 100644 --- a/api.go +++ b/api.go @@ -141,6 +141,7 @@ type ChunkMatch struct { DebugScore string // Content is a contiguous range of complete lines that fully contains Ranges. + // Lines will always include their terminating newline (if it exists). Content []byte // Ranges is a set of matching ranges within this chunk. Each range is relative @@ -224,8 +225,12 @@ func (l *Location) sizeBytes() uint64 { // LineMatch holds the matches within a single line in a file. type LineMatch struct { // The line in which a match was found. - Line []byte - LineStart int + Line []byte + // The byte offset of the first byte of the line. + LineStart int + // The byte offset of the first byte past the end of the line. + // This is usually the byte after the terminating newline, but can also be + // the end of the file if there is no terminating newline LineEnd int LineNumber int diff --git a/contentprovider.go b/contentprovider.go index c3c9ef6e9..c66b1f471 100644 --- a/contentprovider.go +++ b/contentprovider.go @@ -243,7 +243,9 @@ func (p *contentProvider) fillContentMatches(ms []*candidateMatch, numContextLin var result []LineMatch for len(ms) > 0 { m := ms[0] - num, lineStart, lineEnd := p.newlines().atOffset(m.byteOffset) + num := p.newlines().atOffset(m.byteOffset) + lineStart := int(p.newlines().lineStart(num)) + nextLineStart := int(p.newlines().lineStart(num + 1)) var lineCands []*candidateMatch @@ -251,7 +253,7 @@ func (p *contentProvider) fillContentMatches(ms []*candidateMatch, numContextLin for len(ms) > 0 { m := ms[0] - if int(m.byteOffset) <= lineEnd { + if int(m.byteOffset) < nextLineStart { endMatch = m.byteOffset + m.byteMatchSz lineCands = append(lineCands, m) ms = ms[1:] @@ -264,7 +266,7 @@ func (p *contentProvider) fillContentMatches(ms []*candidateMatch, numContextLin log.Panicf( "%s %v infinite loop: num %d start,end %d,%d, offset %d", p.id.fileName(p.idx), p.id.metaData, - num, lineStart, lineEnd, + num, lineStart, nextLineStart, m.byteOffset) } @@ -273,22 +275,22 @@ func (p *contentProvider) fillContentMatches(ms []*candidateMatch, numContextLin // Due to merging matches, we may have a match that // crosses a line boundary. Prevent confusion by // taking lines until we pass the last match - for lineEnd < len(data) && endMatch > uint32(lineEnd) { - next := bytes.IndexByte(data[lineEnd+1:], '\n') + for nextLineStart < len(data) && endMatch > uint32(nextLineStart) { + next := bytes.IndexByte(data[nextLineStart:], '\n') if next == -1 { - lineEnd = len(data) + nextLineStart = len(data) } else { // TODO(hanwen): test that checks "+1" part here. - lineEnd += next + 1 + nextLineStart += next + 1 } } finalMatch := LineMatch{ LineStart: lineStart, - LineEnd: lineEnd, + LineEnd: nextLineStart, LineNumber: num, } - finalMatch.Line = data[lineStart:lineEnd] + finalMatch.Line = data[lineStart:nextLineStart] if numContextLines > 0 { finalMatch.Before = p.newlines().getLines(data, num-numContextLines, num) @@ -340,19 +342,18 @@ func (p *contentProvider) fillContentChunkMatches(ms []*candidateMatch, numConte for _, cm := range chunk.candidates { startOffset := cm.byteOffset endOffset := cm.byteOffset + cm.byteMatchSz - startLine, startLineOffset, _ := newlines.atOffset(startOffset) - endLine, endLineOffset, _ := newlines.atOffset(endOffset) + startLine, endLine := newlines.offsetRangeToLineRange(startOffset, endOffset) ranges = append(ranges, Range{ Start: Location{ ByteOffset: startOffset, LineNumber: uint32(startLine), - Column: columnHelper.get(startLineOffset, startOffset), + Column: columnHelper.get(int(newlines.lineStart(startLine)), startOffset), }, End: Location{ ByteOffset: endOffset, LineNumber: uint32(endLine), - Column: columnHelper.get(endLineOffset, endOffset), + Column: columnHelper.get(int(newlines.lineStart(endLine)), endOffset), }, }) } @@ -361,7 +362,7 @@ func (p *contentProvider) fillContentChunkMatches(ms []*candidateMatch, numConte if firstLineNumber < 1 { firstLineNumber = 1 } - firstLineStart, _ := newlines.lineBounds(firstLineNumber) + firstLineStart := newlines.lineStart(firstLineNumber) chunkMatches = append(chunkMatches, ChunkMatch{ Content: newlines.getLines(data, firstLineNumber, int(chunk.lastLine)+numContextLines+1), @@ -399,8 +400,7 @@ func chunkCandidates(ms []*candidateMatch, newlines newlines, numContextLines in for _, m := range ms { startOffset := m.byteOffset endOffset := m.byteOffset + m.byteMatchSz - firstLine, _, _ := newlines.atOffset(startOffset) - lastLine, _, _ := newlines.atOffset(endOffset) + firstLine, lastLine := newlines.offsetRangeToLineRange(startOffset, endOffset) if len(chunks) > 0 && int(chunks[len(chunks)-1].lastLine)+numContextLines >= firstLine-numContextLines { // If a new chunk created with the current candidateMatch would @@ -471,45 +471,39 @@ type newlines struct { } // atOffset returns the line containing the offset. If the offset lands on -// the newline ending line M, we return M. The line is characterized -// by its linenumber (base-1, byte index of line start, byte index of -// line end). The line end is the index of a newline, or the filesize -// (if matching the last line of the file.) -func (nls newlines) atOffset(offset uint32) (lineNumber, lineStart, lineEnd int) { +// the newline ending line M, we return M. +func (nls newlines) atOffset(offset uint32) (lineNumber int) { idx := sort.Search(len(nls.locs), func(n int) bool { return nls.locs[n] >= offset }) - - start, end := nls.lineBounds(idx + 1) - return idx + 1, int(start), int(end) + return idx + 1 } -// lineBounds returns the byte offsets of the start and end of the 1-based -// lineNumber. The end offset is exclusive and will not contain the line-ending -// newline. If the line number is out of range of the lines in the file, start -// and end will be clamped to [0,fileSize]. -func (nls newlines) lineBounds(lineNumber int) (start, end uint32) { +// lineStart returns the byte offset of the beginning of the given line. +// lineNumber is 1-based. If lineNumber is out of range of the lines in the +// file, the return value will be clamped to [0,fileSize]. +func (nls newlines) lineStart(lineNumber int) uint32 { // nls.locs[0] + 1 is the start of the 2nd line of data. startIdx := lineNumber - 2 - endIdx := lineNumber - 1 if startIdx < 0 { - start = 0 + return 0 } else if startIdx >= len(nls.locs) { - start = nls.fileSize + return nls.fileSize } else { - start = nls.locs[startIdx] + 1 - } - - if endIdx < 0 { - end = 0 - } else if endIdx >= len(nls.locs) { - end = nls.fileSize - } else { - end = nls.locs[endIdx] + return nls.locs[startIdx] + 1 } +} - return start, end +// offsetRangeToLineRange returns range of lines that fully contains the given byte range. +// The inputs are 0-based byte offsets into the file representing the (exclusive) range [startOffset, endOffset). +// The return values are 1-based line numbers representing the (inclusive) range [startLine, endLine]. +func (nls newlines) offsetRangeToLineRange(startOffset, endOffset uint32) (startLine, endLine int) { + startLine = nls.atOffset(startOffset) + endLine = nls.atOffset( + max(startOffset, max(endOffset, 1)-1), // clamp endOffset and prevent underflow + ) + return startLine, endLine } // getLines returns a slice of data containing the lines [low, high). @@ -519,22 +513,7 @@ func (nls newlines) getLines(data []byte, low, high int) []byte { return nil } - lowStart, _ := nls.lineBounds(low) - _, highEnd := nls.lineBounds(high - 1) - - // Drop any trailing newline. Editors do not treat a trailing newline as - // the start of a new line, so we should not either. lineBounds clamps to - // len(data) when an out-of-bounds line is requested. - // - // As an example, if we request lines 1-5 from a file with contents - // `one\ntwo\nthree\n`, we should return `one\ntwo\nthree` because those are - // the three "lines" in the file, separated by newlines. - if highEnd == uint32(len(data)) && bytes.HasSuffix(data, []byte{'\n'}) { - highEnd = highEnd - 1 - lowStart = min(lowStart, highEnd) - } - - return data[lowStart:highEnd] + return data[nls.lineStart(low):nls.lineStart(high)] } const ( diff --git a/contentprovider_test.go b/contentprovider_test.go index b8bbc253b..7a4024b5a 100644 --- a/contentprovider_test.go +++ b/contentprovider_test.go @@ -34,8 +34,11 @@ func TestGetLines(t *testing.T) { for _, content := range contents { t.Run("", func(t *testing.T) { newLines := getNewlines(content) - // Trim the last newline before splitting because a trailing newline does not constitute a new line - lines := bytes.Split(bytes.TrimSuffix(content, []byte{'\n'}), []byte{'\n'}) + lines := bytes.SplitAfter(content, []byte{'\n'}) + if len(lines) > 0 && len(lines[len(lines)-1]) == 0 { + // A trailing newline does not delimit an empty line at the end of a file + lines = lines[:len(lines)-1] + } wantGetLines := func(low, high int) []byte { low-- high-- @@ -51,7 +54,7 @@ func TestGetLines(t *testing.T) { if high > len(lines) { high = len(lines) } - return bytes.Join(lines[low:high], []byte{'\n'}) + return bytes.Join(lines[low:high], nil) } for low := -1; low <= len(lines)+2; low++ { @@ -72,32 +75,32 @@ func TestAtOffset(t *testing.T) { data []byte offset uint32 lineNumber int - lineStart int - lineEnd int + lineStart uint32 + lineEnd uint32 }{{ data: []byte("0.2.4.\n7.9.11.\n"), offset: 0, - lineNumber: 1, lineStart: 0, lineEnd: 6, + lineNumber: 1, lineStart: 0, lineEnd: 7, }, { data: []byte("0.2.4.\n7.9.11.\n"), offset: 6, - lineNumber: 1, lineStart: 0, lineEnd: 6, + lineNumber: 1, lineStart: 0, lineEnd: 7, }, { data: []byte("0.2.4.\n7.9.11.\n"), offset: 2, - lineNumber: 1, lineStart: 0, lineEnd: 6, + lineNumber: 1, lineStart: 0, lineEnd: 7, }, { data: []byte("0.2.4.\n7.9.11.\n"), offset: 2, - lineNumber: 1, lineStart: 0, lineEnd: 6, + lineNumber: 1, lineStart: 0, lineEnd: 7, }, { data: []byte("0.2.4.\n7.9.11.\n"), offset: 7, - lineNumber: 2, lineStart: 7, lineEnd: 14, + lineNumber: 2, lineStart: 7, lineEnd: 15, }, { data: []byte("0.2.4.\n7.9.11.\n"), offset: 11, - lineNumber: 2, lineStart: 7, lineEnd: 14, + lineNumber: 2, lineStart: 7, lineEnd: 15, }, { data: []byte("0.2.4.\n7.9.11.\n"), offset: 15, @@ -109,11 +112,11 @@ func TestAtOffset(t *testing.T) { }, { data: []byte("\n\n"), offset: 0, - lineNumber: 1, lineStart: 0, lineEnd: 0, + lineNumber: 1, lineStart: 0, lineEnd: 1, }, { data: []byte("\n\n"), offset: 1, - lineNumber: 2, lineStart: 1, lineEnd: 1, + lineNumber: 2, lineStart: 1, lineEnd: 2, }, { data: []byte("\n\n"), offset: 3, @@ -127,14 +130,14 @@ func TestAtOffset(t *testing.T) { for _, tt := range cases { t.Run("", func(t *testing.T) { nls := getNewlines(tt.data) - gotLineNumber, gotLineStart, gotLineEnd := nls.atOffset(tt.offset) + gotLineNumber := nls.atOffset(tt.offset) if gotLineNumber != tt.lineNumber { t.Fatalf("expected line number %d, got %d", tt.lineNumber, gotLineNumber) } - if gotLineStart != tt.lineStart { + if gotLineStart := nls.lineStart(gotLineNumber); gotLineStart != tt.lineStart { t.Fatalf("expected line start %d, got %d", tt.lineStart, gotLineStart) } - if gotLineEnd != tt.lineEnd { + if gotLineEnd := nls.lineStart(gotLineNumber + 1); gotLineEnd != tt.lineEnd { t.Fatalf("expected line end %d, got %d", tt.lineEnd, gotLineEnd) } }) @@ -150,11 +153,11 @@ func TestLineBounds(t *testing.T) { }{{ data: []byte("0.2.4.\n7.9.11.\n"), lineNumber: 1, - start: 0, end: 6, + start: 0, end: 7, }, { data: []byte("0.2.4.\n7.9.11.\n"), lineNumber: 2, - start: 7, end: 14, + start: 7, end: 15, }, { data: []byte("0.2.4.\n7.9.11.\n"), lineNumber: 0, @@ -170,11 +173,11 @@ func TestLineBounds(t *testing.T) { }, { data: []byte("\n\n"), lineNumber: 1, - start: 0, end: 0, + start: 0, end: 1, }, { data: []byte("\n\n"), lineNumber: 2, - start: 1, end: 1, + start: 1, end: 2, }, { data: []byte("\n\n"), lineNumber: 3, @@ -184,10 +187,11 @@ func TestLineBounds(t *testing.T) { for _, tt := range cases { t.Run("", func(t *testing.T) { nls := getNewlines(tt.data) - gotStart, gotEnd := nls.lineBounds(tt.lineNumber) + gotStart := nls.lineStart(tt.lineNumber) if gotStart != tt.start { t.Fatalf("expected line start %d, got %d", tt.start, gotStart) } + gotEnd := nls.lineStart(tt.lineNumber + 1) if gotEnd != tt.end { t.Fatalf("expected line end %d, got %d", tt.end, gotEnd) } diff --git a/index_test.go b/index_test.go index 9836032e6..8608b5bfc 100644 --- a/index_test.go +++ b/index_test.go @@ -201,8 +201,8 @@ func (s *memSeeker) Size() (uint32, error) { func TestNewlines(t *testing.T) { b := testIndexBuilder(t, nil, + // -----------------------------------------012345-678901-234 Document{Name: "filename", Content: []byte("line1\nline2\nbla")}) - // ---------------------------------------------012345-678901-234 t.Run("LineMatches", func(t *testing.T) { sres := searchForTest(t, b, &query.Substring{Pattern: "ne2"}) @@ -216,15 +216,15 @@ func TestNewlines(t *testing.T) { LineOffset: 2, MatchLength: 3, }}, - Line: []byte("line2"), + Line: []byte("line2\n"), LineStart: 6, - LineEnd: 11, + LineEnd: 12, LineNumber: 2, }}, }} - if !reflect.DeepEqual(matches, want) { - t.Errorf("got %v, want %v", matches, want) + if diff := cmp.Diff(matches, want); diff != "" { + t.Fatal(diff) } }) @@ -235,7 +235,7 @@ func TestNewlines(t *testing.T) { want := []FileMatch{{ FileName: "filename", ChunkMatches: []ChunkMatch{{ - Content: []byte("line2"), + Content: []byte("line2\n"), ContentStart: Location{ ByteOffset: 6, LineNumber: 2, @@ -269,7 +269,7 @@ func TestQueryNewlines(t *testing.T) { } m := matches[0] if len(m.LineMatches) != 2 { - t.Fatalf("got %d line matches, want exactly two", len(m.LineMatches)) + t.Fatalf("got %d line matches, want exactly two %#v", len(m.LineMatches), m.LineMatches) } }) @@ -2452,7 +2452,7 @@ func TestIOStats(t *testing.T) { res := searchForTest(t, b, q) // 4096 (content) + 2 (overhead: newlines or doc sections) - if got, want := res.Stats.ContentBytesLoaded, int64(4098); got != want { + if got, want := res.Stats.ContentBytesLoaded, int64(4100); got != want { t.Errorf("got content I/O %d, want %d", got, want) } diff --git a/internal/e2e/e2e_rank_test.go b/internal/e2e/e2e_rank_test.go index 547399bab..1e4cdca6e 100644 --- a/internal/e2e/e2e_rank_test.go +++ b/internal/e2e/e2e_rank_test.go @@ -311,7 +311,7 @@ func marshalMatches(w io.Writer, rq rankingQuery, q query.Q, files []zoekt.FileM chunks, hidden := splitAtIndex(f.ChunkMatches, chunkMatchesPerFile) for _, m := range chunks { - _, _ = fmt.Fprintf(w, "%d:%s%s\n", m.ContentStart.LineNumber, string(m.Content), addTabIfNonEmpty(m.DebugScore)) + _, _ = fmt.Fprintf(w, "%d:%s%s\n", m.ContentStart.LineNumber, strings.TrimRight(string(m.Content), "\n"), addTabIfNonEmpty(m.DebugScore)) } if len(hidden) > 0 { diff --git a/matchtree.go b/matchtree.go index 6706ecf0f..ce30f0980 100644 --- a/matchtree.go +++ b/matchtree.go @@ -694,11 +694,13 @@ func (t *andLineMatchTree) matches(cp *contentProvider, cost int, known map[matc lines := make([]lineRange, 0, len(t.children[fewestChildren].(*substrMatchTree).current)) prev := -1 for _, candidate := range t.children[fewestChildren].(*substrMatchTree).current { - line, byteStart, byteEnd := cp.newlines().atOffset(candidate.byteOffset) + line := cp.newlines().atOffset(candidate.byteOffset) if line == prev { continue } prev = line + byteStart := int(cp.newlines().lineStart(line)) + byteEnd := int(cp.newlines().lineStart(line + 1)) lines = append(lines, lineRange{byteStart, byteEnd}) } @@ -724,12 +726,12 @@ nextLine: children[j] = children[j][1:] continue nextCandidate } - if bo <= lines[i].end { + if bo < lines[i].end { hits++ continue nextChild } - // move the `lines` iterator forward until bo <= line.end - for i < len(lines) && bo > lines[i].end { + // move the `lines` iterator forward until bo < line.end + for i < len(lines) && bo >= lines[i].end { i++ } i-- diff --git a/testdata/golden/TestReadSearch/ctagsrepo_v16.00000.golden b/testdata/golden/TestReadSearch/ctagsrepo_v16.00000.golden index 2ec38c772..6c5faaacd 100644 --- a/testdata/golden/TestReadSearch/ctagsrepo_v16.00000.golden +++ b/testdata/golden/TestReadSearch/ctagsrepo_v16.00000.golden @@ -9,9 +9,9 @@ "Language": "go", "LineMatches": [ { - "Line": "ZnVuYyBtYWluKCkgew==", + "Line": "ZnVuYyBtYWluKCkgewo=", "LineStart": 69, - "LineEnd": 82, + "LineEnd": 83, "LineNumber": 10, "Before": null, "After": null, @@ -39,9 +39,9 @@ "Language": "go", "LineMatches": [ { - "Line": "cGFja2FnZSBtYWlu", + "Line": "cGFja2FnZSBtYWluCg==", "LineStart": 0, - "LineEnd": 12, + "LineEnd": 13, "LineNumber": 1, "Before": null, "After": null, @@ -69,9 +69,9 @@ "Language": "go", "LineMatches": [ { - "Line": "CW51bSAgICAgPSA1", + "Line": "CW51bSAgICAgPSA1Cg==", "LineStart": 34, - "LineEnd": 46, + "LineEnd": 47, "LineNumber": 6, "Before": null, "After": null, @@ -104,9 +104,9 @@ "Language": "go", "LineMatches": [ { - "Line": "CW1lc3NhZ2UgPSAiaGVsbG8i", + "Line": "CW1lc3NhZ2UgPSAiaGVsbG8iCg==", "LineStart": 47, - "LineEnd": 65, + "LineEnd": 66, "LineNumber": 7, "Before": null, "After": null, diff --git a/testdata/golden/TestReadSearch/ctagsrepo_v17.00000.golden b/testdata/golden/TestReadSearch/ctagsrepo_v17.00000.golden index de1f98e2f..d8054cc34 100644 --- a/testdata/golden/TestReadSearch/ctagsrepo_v17.00000.golden +++ b/testdata/golden/TestReadSearch/ctagsrepo_v17.00000.golden @@ -9,9 +9,9 @@ "Language": "go", "LineMatches": [ { - "Line": "ZnVuYyBtYWluKCkgew==", + "Line": "ZnVuYyBtYWluKCkgewo=", "LineStart": 69, - "LineEnd": 82, + "LineEnd": 83, "LineNumber": 10, "Before": null, "After": null, @@ -39,9 +39,9 @@ "Language": "go", "LineMatches": [ { - "Line": "cGFja2FnZSBtYWlu", + "Line": "cGFja2FnZSBtYWluCg==", "LineStart": 0, - "LineEnd": 12, + "LineEnd": 13, "LineNumber": 1, "Before": null, "After": null, @@ -69,9 +69,9 @@ "Language": "go", "LineMatches": [ { - "Line": "CW51bSAgICAgPSA1", + "Line": "CW51bSAgICAgPSA1Cg==", "LineStart": 34, - "LineEnd": 46, + "LineEnd": 47, "LineNumber": 6, "Before": null, "After": null, @@ -104,9 +104,9 @@ "Language": "go", "LineMatches": [ { - "Line": "CW1lc3NhZ2UgPSAiaGVsbG8i", + "Line": "CW1lc3NhZ2UgPSAiaGVsbG8iCg==", "LineStart": 47, - "LineEnd": 65, + "LineEnd": 66, "LineNumber": 7, "Before": null, "After": null, diff --git a/testdata/golden/TestReadSearch/repo17_v17.00000.golden b/testdata/golden/TestReadSearch/repo17_v17.00000.golden index 2d11f1f0f..452429bcf 100644 --- a/testdata/golden/TestReadSearch/repo17_v17.00000.golden +++ b/testdata/golden/TestReadSearch/repo17_v17.00000.golden @@ -9,9 +9,9 @@ "Language": "Go", "LineMatches": [ { - "Line": "ZnVuYyBtYWluKCkgew==", + "Line": "ZnVuYyBtYWluKCkgewo=", "LineStart": 69, - "LineEnd": 82, + "LineEnd": 83, "LineNumber": 10, "Before": null, "After": null, @@ -39,9 +39,9 @@ "Language": "Go", "LineMatches": [ { - "Line": "cGFja2FnZSBtYWlu", + "Line": "cGFja2FnZSBtYWluCg==", "LineStart": 0, - "LineEnd": 12, + "LineEnd": 13, "LineNumber": 1, "Before": null, "After": null, diff --git a/testdata/golden/TestReadSearch/repo2_v16.00000.golden b/testdata/golden/TestReadSearch/repo2_v16.00000.golden index 82a6112d8..cf70c4e0d 100644 --- a/testdata/golden/TestReadSearch/repo2_v16.00000.golden +++ b/testdata/golden/TestReadSearch/repo2_v16.00000.golden @@ -9,9 +9,9 @@ "Language": "Go", "LineMatches": [ { - "Line": "ZnVuYyBtYWluKCkgew==", + "Line": "ZnVuYyBtYWluKCkgewo=", "LineStart": 33, - "LineEnd": 46, + "LineEnd": 47, "LineNumber": 7, "Before": null, "After": null, @@ -39,9 +39,9 @@ "Language": "Go", "LineMatches": [ { - "Line": "cGFja2FnZSBtYWlu", + "Line": "cGFja2FnZSBtYWluCg==", "LineStart": 0, - "LineEnd": 12, + "LineEnd": 13, "LineNumber": 1, "Before": null, "After": null, diff --git a/testdata/golden/TestReadSearch/repo_v16.00000.golden b/testdata/golden/TestReadSearch/repo_v16.00000.golden index ed2532855..7f5f3c41c 100644 --- a/testdata/golden/TestReadSearch/repo_v16.00000.golden +++ b/testdata/golden/TestReadSearch/repo_v16.00000.golden @@ -9,9 +9,9 @@ "Language": "Go", "LineMatches": [ { - "Line": "ZnVuYyBtYWluKCkgew==", + "Line": "ZnVuYyBtYWluKCkgewo=", "LineStart": 69, - "LineEnd": 82, + "LineEnd": 83, "LineNumber": 10, "Before": null, "After": null, @@ -39,9 +39,9 @@ "Language": "Go", "LineMatches": [ { - "Line": "cGFja2FnZSBtYWlu", + "Line": "cGFja2FnZSBtYWluCg==", "LineStart": 0, - "LineEnd": 12, + "LineEnd": 13, "LineNumber": 1, "Before": null, "After": null, diff --git a/web/e2e_test.go b/web/e2e_test.go index 720712972..e0be04a71 100644 --- a/web/e2e_test.go +++ b/web/e2e_test.go @@ -393,7 +393,7 @@ func TestContextLines(t *testing.T) { { Pre: "f", Match: "our", - Post: "th", + Post: "th\n", }, }, }, @@ -431,11 +431,11 @@ func TestContextLines(t *testing.T) { { Pre: "f", Match: "our", - Post: "th", + Post: "th\n", }, }, - Before: "second snippet\nthird thing", - After: "fifth block\nsixth example", + Before: "second snippet\nthird thing\n", + After: "fifth block\nsixth example\n", }, }, }, @@ -453,10 +453,10 @@ func TestContextLines(t *testing.T) { { Pre: "", Match: "one", - Post: " line", + Post: " line\n", }, }, - After: "second snippet\nthird thing", + After: "second snippet\nthird thing\n", }, }, }, @@ -477,7 +477,7 @@ func TestContextLines(t *testing.T) { Post: "", }, }, - Before: "fifth block\nsixth example", + Before: "fifth block\nsixth example\n", }, }, }, @@ -498,7 +498,7 @@ func TestContextLines(t *testing.T) { Post: "", }, }, - Before: "one line\nsecond snippet\nthird thing\nfourth\nfifth block\nsixth example", + Before: "one line\nsecond snippet\nthird thing\nfourth\nfifth block\nsixth example\n", }, }, }, @@ -516,7 +516,7 @@ func TestContextLines(t *testing.T) { { Pre: "", Match: "one", - Post: " line", + Post: " line\n", }, }, After: "second snippet\nthird thing\nfourth\nfifth block\nsixth example\nseventh", @@ -537,10 +537,11 @@ func TestContextLines(t *testing.T) { { Pre: "\t", Match: "trois", + Post: "\n", }, }, - Before: "un \n ", - After: " \n", + Before: "un \n \n", + After: " \n\n", }, }, }, @@ -558,16 +559,11 @@ func TestContextLines(t *testing.T) { { Pre: "to carry ", Match: "water", - Post: " in the no later bla", + Post: " in the no later bla\n", }, }, - // Returns 3 instead of 4 new line characters since we swallow - // the last new line in Before, Fragments and After. - Before: "\n\n\n", - // Returns 2 instead of 3 new line characters since a - // trailing newline at the end of the file does not - // constitue a new line. - After: "\n\n", + Before: "\n\n\n\n", + After: "\n\n\n", }, }, }, @@ -585,10 +581,11 @@ func TestContextLines(t *testing.T) { { Pre: "", Match: "pastures", + Post: "\n", }, }, - Before: "green", - After: "", + Before: "green\n", + After: "\n", }, }, }, diff --git a/web/server.go b/web/server.go index a8b237215..6476ca69b 100644 --- a/web/server.go +++ b/web/server.go @@ -73,6 +73,9 @@ var Funcmap = template.FuncMap{ } return fmt.Sprintf("%s...(%d bytes skipped)...", post[:limit], len(post)-limit) }, + "TrimTrailingNewline": func(s string) string { + return strings.TrimSuffix(s, "\n") + }, } const defaultNumResults = 50 diff --git a/web/templates.go b/web/templates.go index ee9f7cbdc..2c5f4026a 100644 --- a/web/templates.go +++ b/web/templates.go @@ -245,7 +245,7 @@ document.onkeydown=function(e){ {{if gt .LineNum 0}} -
{{if .URL}}{{end}}{{.LineNum}}{{if .URL}}{{end}}: {{range .Fragments}}{{LimitPre 100 .Pre}}{{.Match}}{{LimitPost 100 .Post}}{{end}} {{if .ScoreDebug}}({{.ScoreDebug}}){{end}}
+
{{if .URL}}{{end}}{{.LineNum}}{{if .URL}}{{end}}: {{range .Fragments}}{{LimitPre 100 .Pre}}{{.Match}}{{LimitPost 100 (TrimTrailingNewline .Post)}}{{end}} {{if .ScoreDebug}}({{.ScoreDebug}}){{end}}
{{end}}