fork of https://github.com/sourcegraph/zoekt
1package zoekt
2
3import "log"
4
5// SortAndTruncateFiles is a convenience around SortFiles and
6// DisplayTruncator. Given an aggregated files it will sort and then truncate
7// based on the search options.
8func SortAndTruncateFiles(files []FileMatch, opts *SearchOptions) []FileMatch {
9 SortFiles(files)
10 truncator, _ := NewDisplayTruncator(opts)
11 files, _ = truncator(files)
12 return files
13}
14
15// DisplayTruncator is a stateful function which enforces Document and Match
16// display limits by truncating and mutating before. hasMore is true until the
17// limits are exhausted. Once hasMore is false each subsequent call will
18// return an empty after and hasMore false.
19type DisplayTruncator func(before []FileMatch) (after []FileMatch, hasMore bool)
20
21// NewDisplayTruncator will return a DisplayTruncator which enforces the limits in
22// opts. If there are no limits to enforce, hasLimits is false and there is no
23// need to call DisplayTruncator.
24func NewDisplayTruncator(opts *SearchOptions) (_ DisplayTruncator, hasLimits bool) {
25 docLimit := opts.MaxDocDisplayCount
26 docLimited := docLimit > 0
27
28 matchLimit := opts.MaxMatchDisplayCount
29 matchLimited := matchLimit > 0
30
31 done := false
32
33 if !docLimited && !matchLimited {
34 return func(fm []FileMatch) ([]FileMatch, bool) {
35 return fm, true
36 }, false
37 }
38
39 return func(fm []FileMatch) ([]FileMatch, bool) {
40 if done {
41 return nil, false
42 }
43
44 if docLimited {
45 if len(fm) >= docLimit {
46 done = true
47 return fm[:docLimit], false
48 }
49 docLimit -= len(fm)
50 }
51
52 if matchLimited {
53 fm, matchLimit = limitMatches(fm, matchLimit, opts.ChunkMatches)
54 if matchLimit <= 0 {
55 done = true
56 return fm, false
57 }
58 }
59
60 return fm, true
61 }, true
62}
63
64func limitMatches(files []FileMatch, limit int, chunkMatches bool) ([]FileMatch, int) {
65 var limiter func(file *FileMatch, limit int) int
66 if chunkMatches {
67 limiter = limitChunkMatches
68 } else {
69 limiter = limitLineMatches
70 }
71 for i := range files {
72 limit = limiter(&files[i], limit)
73 if limit <= 0 {
74 return files[:i+1], 0
75 }
76 }
77 return files, limit
78}
79
80// Limit the number of ChunkMatches in the given FileMatch, returning the
81// remaining limit, if any.
82func limitChunkMatches(file *FileMatch, limit int) int {
83 for i := range file.ChunkMatches {
84 cm := &file.ChunkMatches[i]
85 if len(cm.Ranges) > limit {
86 // We potentially need to effect the limit upon 3 different fields:
87 // Ranges, SymbolInfo, and Content.
88
89 // Content is the most complicated: we need to remove the last N
90 // lines from it, where N is the difference between the line number
91 // of the end of the old last Range and that of the new last Range.
92 // This calculation is correct in the presence of both context lines
93 // and multiline Ranges, taking into account that Content never has
94 // a trailing newline.
95 n := cm.Ranges[len(cm.Ranges)-1].End.LineNumber - cm.Ranges[limit-1].End.LineNumber
96 if n > 0 {
97 for b := len(cm.Content) - 1; b >= 0; b-- {
98 if cm.Content[b] == '\n' {
99 n -= 1
100 }
101 if n == 0 {
102 cm.Content = cm.Content[:b]
103 break
104 }
105 }
106 if n > 0 {
107 // Should be impossible.
108 log.Panicf("Failed to find enough newlines when truncating Content, %d left over, %d ranges", n, len(cm.Ranges))
109 }
110 }
111
112 cm.Ranges = cm.Ranges[:limit]
113 if cm.SymbolInfo != nil {
114 // When non-nil, SymbolInfo is specified to have the same length
115 // as Ranges.
116 cm.SymbolInfo = cm.SymbolInfo[:limit]
117 }
118 }
119 if len(cm.Ranges) == limit {
120 file.ChunkMatches = file.ChunkMatches[:i+1]
121 limit = 0
122 break
123 }
124 limit -= len(cm.Ranges)
125 }
126 return limit
127}
128
129// Limit the number of LineMatches in the given FileMatch, returning the
130// remaining limit, if any.
131func limitLineMatches(file *FileMatch, limit int) int {
132 for i := range file.LineMatches {
133 lm := &file.LineMatches[i]
134 if len(lm.LineFragments) > limit {
135 lm.LineFragments = lm.LineFragments[:limit]
136 }
137 if len(lm.LineFragments) == limit {
138 file.LineMatches = file.LineMatches[:i+1]
139 limit = 0
140 break
141 }
142 limit -= len(lm.LineFragments)
143 }
144 return limit
145}