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 fm = fm[:docLimit]
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 }
57 }
58
59 return fm, !done
60 }, true
61}
62
63func limitMatches(files []FileMatch, limit int, chunkMatches bool) ([]FileMatch, int) {
64 var limiter func(file *FileMatch, limit int) int
65 if chunkMatches {
66 limiter = limitChunkMatches
67 } else {
68 limiter = limitLineMatches
69 }
70 for i := range files {
71 limit = limiter(&files[i], limit)
72 if limit <= 0 {
73 return files[:i+1], 0
74 }
75 }
76 return files, limit
77}
78
79// Limit the number of ChunkMatches in the given FileMatch, returning the
80// remaining limit, if any.
81func limitChunkMatches(file *FileMatch, limit int) int {
82 for i := range file.ChunkMatches {
83 cm := &file.ChunkMatches[i]
84 if len(cm.Ranges) > limit {
85 // We potentially need to effect the limit upon 3 different fields:
86 // Ranges, SymbolInfo, and Content.
87
88 // Content is the most complicated: we need to remove the last N
89 // lines from it, where N is the difference between the line number
90 // of the end of the old last Range and that of the new last Range.
91 // This calculation is correct in the presence of both context lines
92 // and multiline Ranges, taking into account that Content never has
93 // a trailing newline.
94 n := cm.Ranges[len(cm.Ranges)-1].End.LineNumber - cm.Ranges[limit-1].End.LineNumber
95 if n > 0 {
96 for b := len(cm.Content) - 1; b >= 0; b-- {
97 if cm.Content[b] == '\n' {
98 n -= 1
99 }
100 if n == 0 {
101 cm.Content = cm.Content[:b]
102 break
103 }
104 }
105 if n > 0 {
106 // Should be impossible.
107 log.Panicf("Failed to find enough newlines when truncating Content, %d left over, %d ranges", n, len(cm.Ranges))
108 }
109 }
110
111 cm.Ranges = cm.Ranges[:limit]
112 if cm.SymbolInfo != nil {
113 // When non-nil, SymbolInfo is specified to have the same length
114 // as Ranges.
115 cm.SymbolInfo = cm.SymbolInfo[:limit]
116 }
117 }
118 if len(cm.Ranges) == limit {
119 file.ChunkMatches = file.ChunkMatches[:i+1]
120 limit = 0
121 break
122 }
123 limit -= len(cm.Ranges)
124 }
125 return limit
126}
127
128// Limit the number of LineMatches in the given FileMatch, returning the
129// remaining limit, if any.
130func limitLineMatches(file *FileMatch, limit int) int {
131 for i := range file.LineMatches {
132 lm := &file.LineMatches[i]
133 if len(lm.LineFragments) > limit {
134 lm.LineFragments = lm.LineFragments[:limit]
135 }
136 if len(lm.LineFragments) == limit {
137 file.LineMatches = file.LineMatches[:i+1]
138 limit = 0
139 break
140 }
141 limit -= len(lm.LineFragments)
142 }
143 return limit
144}