1 #include "highlighters.hh"
2 
3 #include "assert.hh"
4 #include "buffer_utils.hh"
5 #include "changes.hh"
6 #include "command_manager.hh"
7 #include "context.hh"
8 #include "display_buffer.hh"
9 #include "face_registry.hh"
10 #include "highlighter_group.hh"
11 #include "line_modification.hh"
12 #include "option_types.hh"
13 #include "parameters_parser.hh"
14 #include "ranges.hh"
15 #include "regex.hh"
16 #include "register_manager.hh"
17 #include "string.hh"
18 #include "utf8.hh"
19 #include "utf8_iterator.hh"
20 #include "window.hh"
21 
22 #include <cstdio>
23 #include <limits>
24 
25 namespace Kakoune
26 {
27 
28 using Utf8Iterator = utf8::iterator<BufferIterator>;
29 
30 template<typename Func>
make_highlighter(Func func,HighlightPass pass=HighlightPass::Colorize)31 std::unique_ptr<Highlighter> make_highlighter(Func func, HighlightPass pass = HighlightPass::Colorize)
32 {
33     struct SimpleHighlighter : public Highlighter
34     {
35         SimpleHighlighter(Func func, HighlightPass pass)
36           : Highlighter{pass}, m_func{std::move(func)} {}
37 
38     private:
39         void do_highlight(HighlightContext context, DisplayBuffer& display_buffer, BufferRange range) override
40         {
41             m_func(context, display_buffer, range);
42         }
43         Func m_func;
44     };
45     return std::make_unique<SimpleHighlighter>(std::move(func), pass);
46 }
47 
48 template<typename T>
highlight_range(DisplayBuffer & display_buffer,BufferCoord begin,BufferCoord end,bool skip_replaced,T func)49 void highlight_range(DisplayBuffer& display_buffer,
50                      BufferCoord begin, BufferCoord end,
51                      bool skip_replaced, T func)
52 {
53     // tolerate begin > end as that can be triggered by wrong encodings
54     if (begin >= end or end <= display_buffer.range().begin
55                      or begin >= display_buffer.range().end)
56         return;
57 
58     for (auto& line : display_buffer.lines())
59     {
60         auto& range = line.range();
61         if (range.end <= begin or end < range.begin)
62             continue;
63 
64         for (auto atom_it = line.begin(); atom_it != line.end(); ++atom_it)
65         {
66             bool is_replaced = atom_it->type() == DisplayAtom::ReplacedRange;
67 
68             if (not atom_it->has_buffer_range() or
69                 (skip_replaced and is_replaced) or
70                 end <= atom_it->begin() or begin >= atom_it->end())
71                 continue;
72 
73             if (not is_replaced and begin > atom_it->begin())
74                 atom_it = ++line.split(atom_it, begin);
75 
76             if (not is_replaced and end < atom_it->end())
77             {
78                 atom_it = line.split(atom_it, end);
79                 func(*atom_it);
80                 ++atom_it;
81             }
82             else
83                 func(*atom_it);
84         }
85     }
86 }
87 
88 template<typename T>
replace_range(DisplayBuffer & display_buffer,BufferCoord begin,BufferCoord end,T func)89 void replace_range(DisplayBuffer& display_buffer,
90                    BufferCoord begin, BufferCoord end, T func)
91 {
92     // tolerate begin > end as that can be triggered by wrong encodings
93     if (begin > end or end < display_buffer.range().begin or begin > display_buffer.range().end)
94         return;
95 
96     auto& lines = display_buffer.lines();
97     auto first_it = std::lower_bound(lines.begin(), lines.end(), begin, [](const DisplayLine& l, const BufferCoord& c) { return l.range().end < c; });
98     if (first_it == lines.end())
99         return;
100 
101     auto first_atom_it = std::find_if(first_it->begin(), first_it->end(), [&begin](const DisplayAtom& a) { return a.has_buffer_range() and a.end() > begin; });
102     first_atom_it = first_it->split(begin);
103 
104     auto last_it = std::lower_bound(first_it, lines.end(), end, [](const DisplayLine& l, const BufferCoord& c) { return l.range().end < c; });
105 
106     if (first_it == last_it)
107     {
108         auto first_atom_idx = first_atom_it - first_it->begin();
109         auto end_atom_it = first_it->split(end);
110         first_atom_it = first_it->erase(first_it->begin() + first_atom_idx, end_atom_it);
111     }
112     else
113     {
114         first_atom_it = first_it->erase(first_atom_it, first_it->end());
115         if (last_it != lines.end())
116         {
117             auto end_atom_it = last_it->split(end);
118             end_atom_it = last_it->erase(last_it->begin(), end_atom_it);
119 
120             first_atom_it = first_it->insert(first_atom_it, end_atom_it, last_it->end());
121             ++last_it;
122         }
123         first_it = --lines.erase(first_it+1, last_it);
124     }
125 
126     func(*first_it, first_atom_it);
127 }
128 
apply_highlighter(HighlightContext context,DisplayBuffer & display_buffer,BufferCoord begin,BufferCoord end,Highlighter & highlighter)129 void apply_highlighter(HighlightContext context,
130                        DisplayBuffer& display_buffer,
131                        BufferCoord begin, BufferCoord end,
132                        Highlighter& highlighter)
133 {
134     if (begin == end)
135         return;
136 
137     using LineIterator = DisplayLineList::iterator;
138     LineIterator first_line;
139     Vector<size_t> insert_idx;
140     auto line_end = display_buffer.lines().end();
141 
142     DisplayBuffer region_display;
143     auto& region_lines = region_display.lines();
144     for (auto line_it = display_buffer.lines().begin(); line_it != line_end; ++line_it)
145     {
146         auto& line = *line_it;
147         auto& range = line.range();
148         if (range.end <= begin or end <= range.begin)
149             continue;
150 
151         if (region_lines.empty())
152             first_line = line_it;
153 
154         if (range.begin < begin or range.end > end)
155         {
156             size_t beg_idx = 0;
157             size_t end_idx = line.atoms().size();
158 
159             for (auto atom_it = line.begin(); atom_it != line.end(); ++atom_it)
160             {
161                 if (not atom_it->has_buffer_range() or end <= atom_it->begin() or begin >= atom_it->end())
162                     continue;
163 
164                 bool is_replaced = atom_it->type() == DisplayAtom::ReplacedRange;
165                 if (atom_it->begin() <= begin)
166                 {
167                     if (is_replaced or atom_it->begin() == begin)
168                         beg_idx = atom_it - line.begin();
169                     else
170                     {
171                         atom_it = ++line.split(atom_it, begin);
172                         beg_idx = atom_it - line.begin();
173                         ++end_idx;
174                     }
175                 }
176 
177                 if (atom_it->end() >= end)
178                 {
179                     if (is_replaced or atom_it->end() == end)
180                         end_idx = atom_it - line.begin() + 1;
181                     else
182                     {
183                         atom_it = ++line.split(atom_it, end);
184                         end_idx = atom_it - line.begin();
185                     }
186                 }
187             }
188             region_lines.emplace_back();
189             std::move(line.begin() + beg_idx, line.begin() + end_idx,
190                       std::back_inserter(region_lines.back()));
191             auto it = line.erase(line.begin() + beg_idx, line.begin() + end_idx);
192             insert_idx.push_back(it - line.begin());
193         }
194         else
195         {
196             insert_idx.push_back(0);
197             region_lines.push_back(std::move(line));
198         }
199     }
200 
201     if (region_display.lines().empty())
202         return;
203 
204     region_display.compute_range();
205     highlighter.highlight(context, region_display, {begin, end});
206 
207     for (size_t i = 0; i < region_lines.size(); ++i)
208     {
209         auto& line = *(first_line + i);
210         auto pos = line.begin() + insert_idx[i];
211         for (auto& atom : region_lines[i])
212             pos = ++line.insert(pos, std::move(atom));
213     }
214     display_buffer.compute_range();
215 }
216 
217 auto apply_face = [](const Face& face)
__anonee5bc8720402(const Face& face) 218 {
219     return [&face](DisplayAtom& atom) {
220         atom.face = merge_faces(atom.face, face);
221     };
222 };
223 
224 const HighlighterDesc fill_desc = {
225     "Fill the whole highlighted range with the given face",
226     {}
227 };
create_fill_highlighter(HighlighterParameters params,Highlighter *)228 static std::unique_ptr<Highlighter> create_fill_highlighter(HighlighterParameters params, Highlighter*)
229 {
230     if (params.size() != 1)
231         throw runtime_error("wrong parameter count");
232 
233     const String& facespec = params[0];
234     auto func = [facespec](HighlightContext context, DisplayBuffer& display_buffer, BufferRange range)
235     {
236         highlight_range(display_buffer, range.begin, range.end, false,
237                         apply_face(context.context.faces()[facespec]));
238     };
239     return make_highlighter(std::move(func));
240 }
241 
242 template<typename T>
243 struct BufferSideCache
244 {
BufferSideCacheKakoune::BufferSideCache245     BufferSideCache() : m_id{get_free_value_id()} {}
246 
getKakoune::BufferSideCache247     T& get(const Buffer& buffer)
248     {
249         Value& cache_val = buffer.values()[m_id];
250         if (not cache_val)
251             cache_val = Value(T{});
252         return cache_val.as<T>();
253     }
254 private:
255     ValueId m_id;
256 };
257 
258 using FacesSpec = Vector<std::pair<size_t, String>, MemoryDomain::Highlight>;
259 
260 const HighlighterDesc regex_desc = {
261     "Parameters: <regex> <capture num>:<face> <capture num>:<face>...\n"
262     "Highlights the matches for captures from the regex with the given faces",
263     {}
264 };
265 class RegexHighlighter : public Highlighter
266 {
267 public:
RegexHighlighter(Regex regex,FacesSpec faces)268     RegexHighlighter(Regex regex, FacesSpec faces)
269         : Highlighter{HighlightPass::Colorize},
270           m_regex{std::move(regex)},
271           m_faces{std::move(faces)}
272     {
273         ensure_first_face_is_capture_0();
274     }
275 
do_highlight(HighlightContext context,DisplayBuffer & display_buffer,BufferRange range)276     void do_highlight(HighlightContext context, DisplayBuffer& display_buffer, BufferRange range) override
277     {
278         auto overlaps = [](const BufferRange& lhs, const BufferRange& rhs) {
279             return lhs.begin < rhs.begin ? lhs.end > rhs.begin
280                                          : rhs.end > lhs.begin;
281         };
282 
283         if (not overlaps(display_buffer.range(), range))
284             return;
285 
286         const auto faces = m_faces | transform([&faces = context.context.faces()](auto&& spec) {
287                 return spec.second.empty() ? Face{} : faces[spec.second];
288             }) | gather<Vector<Face>>();
289 
290         const auto& matches = get_matches(context.context.buffer(), display_buffer.range(), range);
291         kak_assert(matches.size() % m_faces.size() == 0);
292         for (size_t m = 0; m < matches.size(); ++m)
293         {
294             auto& face = faces[m % faces.size()];
295             if (face == Face{})
296                 continue;
297 
298             highlight_range(display_buffer,
299                             matches[m].begin, matches[m].end,
300                             false, apply_face(face));
301         }
302     }
303 
reset(Regex regex,FacesSpec faces)304     void reset(Regex regex, FacesSpec faces)
305     {
306         m_regex = std::move(regex);
307         m_faces = std::move(faces);
308         ensure_first_face_is_capture_0();
309         ++m_regex_version;
310     }
311 
create(HighlighterParameters params,Highlighter *)312     static std::unique_ptr<Highlighter> create(HighlighterParameters params, Highlighter*)
313     {
314         if (params.size() < 2)
315             throw runtime_error("wrong parameter count");
316 
317         Regex re{params[0], RegexCompileFlags::Optimize};
318 
319         FacesSpec faces;
320         for (auto& spec : params.subrange(1))
321         {
322             auto colon = find(spec, ':');
323             if (colon == spec.end())
324                 throw runtime_error(format("wrong face spec: '{}' expected <capture>:<facespec>", spec));
325             const StringView capture_name{spec.begin(), colon};
326             const int capture = str_to_int_ifp(capture_name).value_or_compute([&] {
327                 return re.named_capture_index(capture_name);
328             });
329             if (capture < 0)
330                 throw runtime_error(format("capture name {} is neither a capture index, nor an existing capture name",
331                                            capture_name));
332             faces.emplace_back(capture, String{colon+1, spec.end()});
333         }
334 
335         return std::make_unique<RegexHighlighter>(std::move(re), std::move(faces));
336     }
337 
338 private:
339     // stores the range for each highlighted capture of each match
340     using MatchList = Vector<BufferRange, MemoryDomain::Highlight>;
341     struct Cache
342     {
343         size_t m_timestamp = -1;
344         size_t m_regex_version = -1;
345         struct RangeAndMatches { BufferRange range; MatchList matches; };
346         using RangeAndMatchesList = Vector<RangeAndMatches, MemoryDomain::Highlight>;
347         HashMap<BufferRange, RangeAndMatchesList, MemoryDomain::Highlight> m_matches;
348     };
349     BufferSideCache<Cache> m_cache;
350 
351     Regex     m_regex;
352     FacesSpec m_faces;
353 
354     size_t m_regex_version = 0;
355 
ensure_first_face_is_capture_0()356     void ensure_first_face_is_capture_0()
357     {
358         if (m_faces.empty())
359             return;
360 
361         std::sort(m_faces.begin(), m_faces.end(),
362                   [](const std::pair<size_t, String>& lhs,
363                      const std::pair<size_t, String>& rhs)
364                   { return lhs.first < rhs.first; });
365         if (m_faces[0].first != 0)
366             m_faces.emplace(m_faces.begin(), 0, String{});
367     }
368 
add_matches(const Buffer & buffer,MatchList & matches,BufferRange range)369     void add_matches(const Buffer& buffer, MatchList& matches, BufferRange range)
370     {
371         kak_assert(matches.size() % m_faces.size() == 0);
372         for (auto&& match : RegexIterator{get_iterator(buffer, range.begin),
373                                           get_iterator(buffer, range.end),
374                                           buffer.begin(), buffer.end(), m_regex,
375                                           match_flags(is_bol(range.begin),
376                                                       is_eol(buffer, range.end),
377                                                       is_bow(buffer, range.begin),
378                                                       is_eow(buffer, range.end))})
379         {
380             for (auto& face : m_faces)
381             {
382                 const auto& sub = match[face.first];
383                 matches.push_back({sub.first.coord(), sub.second.coord()});
384             }
385         }
386     }
387 
get_matches(const Buffer & buffer,BufferRange display_range,BufferRange buffer_range)388     const MatchList& get_matches(const Buffer& buffer, BufferRange display_range, BufferRange buffer_range)
389     {
390         Cache& cache = m_cache.get(buffer);
391 
392         if (cache.m_regex_version != m_regex_version or
393             cache.m_timestamp != buffer.timestamp() or
394             accumulate(cache.m_matches, (size_t)0, [](size_t c, auto&& m) { return c + m.value.size(); }) > 1000)
395         {
396             cache.m_matches.clear();
397             cache.m_timestamp = buffer.timestamp();
398             cache.m_regex_version = m_regex_version;
399         }
400 
401         auto& matches = cache.m_matches[buffer_range];
402 
403         const LineCount line_offset = 3;
404         BufferRange range{std::max<BufferCoord>(buffer_range.begin, display_range.begin.line - line_offset),
405                           std::min<BufferCoord>(buffer_range.end, display_range.end.line + line_offset)};
406 
407         auto it = std::upper_bound(matches.begin(), matches.end(), range.begin,
408                                    [](const BufferCoord& lhs, const Cache::RangeAndMatches& rhs)
409                                    { return lhs < rhs.range.end; });
410 
411         if (it == matches.end() or it->range.begin > range.end)
412         {
413             it = matches.insert(it, Cache::RangeAndMatches{range, {}});
414             add_matches(buffer, it->matches, range);
415         }
416         else if (it->matches.empty())
417         {
418             it->range = range;
419             add_matches(buffer, it->matches, range);
420         }
421         else
422         {
423             // Here we extend the matches, that is not strictly valid,
424             // but may work nicely with every reasonable regex, and
425             // greatly reduces regex parsing. To change if we encounter
426             // regex that do not work great with that.
427             BufferRange& old_range = it->range;
428             MatchList& matches = it->matches;
429 
430             // Thanks to the ensure_first_face_is_capture_0 method, we know
431             // these point to the first/last matches capture 0.
432             auto first_end = matches.begin()->end;
433             auto last_end = (matches.end() - m_faces.size())->end;
434 
435             // add regex matches from new begin to old first match end
436             if (range.begin < old_range.begin)
437             {
438                 matches.erase(matches.begin(), matches.begin() + m_faces.size());
439                 size_t pivot = matches.size();
440                 old_range.begin = range.begin;
441                 add_matches(buffer, matches, {range.begin, first_end});
442 
443                 std::rotate(matches.begin(), matches.begin() + pivot, matches.end());
444             }
445             // add regex matches from old last match begin to new end
446             if (old_range.end < range.end)
447             {
448                 old_range.end = range.end;
449                 add_matches(buffer, matches, {last_end, range.end});
450             }
451         }
452         return it->matches;
453     }
454 };
455 
456 template<typename RegexGetter, typename FaceGetter>
457 class DynamicRegexHighlighter : public Highlighter
458 {
459 public:
DynamicRegexHighlighter(RegexGetter regex_getter,FaceGetter face_getter)460     DynamicRegexHighlighter(RegexGetter regex_getter, FaceGetter face_getter)
461       : Highlighter{HighlightPass::Colorize},
462         m_regex_getter(std::move(regex_getter)),
463         m_face_getter(std::move(face_getter)),
464         m_highlighter(Regex{}, FacesSpec{}) {}
465 
do_highlight(HighlightContext context,DisplayBuffer & display_buffer,BufferRange range)466     void do_highlight(HighlightContext context, DisplayBuffer& display_buffer, BufferRange range) override
467     {
468         Regex regex = m_regex_getter(context.context);
469         FacesSpec face = regex.empty() ? FacesSpec{} : m_face_getter(context.context, regex);
470         if (regex != m_last_regex or face != m_last_face)
471         {
472             m_last_regex = std::move(regex);
473             m_last_face = face;
474             if (not m_last_regex.empty())
475                 m_highlighter.reset(m_last_regex, m_last_face);
476         }
477         if (not m_last_regex.empty() and not m_last_face.empty())
478             m_highlighter.highlight(context, display_buffer, range);
479     }
480 
481 private:
482     Regex       m_last_regex;
483     RegexGetter m_regex_getter;
484 
485     FacesSpec   m_last_face;
486     FaceGetter  m_face_getter;
487 
488     RegexHighlighter m_highlighter;
489 };
490 
491 const HighlighterDesc dynamic_regex_desc = {
492     "Parameters: <expr> <capture num>:<face> <capture num>:<face>...\n"
493     "Evaluate expression at every redraw to gather a regex",
494     {}
495 };
create_dynamic_regex_highlighter(HighlighterParameters params,Highlighter *)496 std::unique_ptr<Highlighter> create_dynamic_regex_highlighter(HighlighterParameters params, Highlighter*)
497 {
498     if (params.size() < 2)
499         throw runtime_error("wrong parameter count");
500 
501     Vector<std::pair<String, String>> faces;
502     for (auto& spec : params.subrange(1))
503     {
504         auto colon = find(spec, ':');
505         if (colon == spec.end())
506             throw runtime_error("wrong face spec: '" + spec +
507                                  "' expected <capture>:<facespec>");
508         faces.emplace_back(String{spec.begin(), colon}, String{colon+1, spec.end()});
509     }
510 
511     auto make_hl = [](auto& regex_getter, auto& face_getter) {
512         return std::make_unique<DynamicRegexHighlighter<std::decay_t<decltype(regex_getter)>,
513                                                         std::decay_t<decltype(face_getter)>>>(
514             std::move(regex_getter), std::move(face_getter));
515     };
516     auto get_face = [faces=std::move(faces)](const Context& context, const Regex& regex){
517         FacesSpec spec;
518         for (auto& face : faces)
519         {
520             const int capture = str_to_int_ifp(face.first).value_or_compute([&] {
521                 return regex.named_capture_index(face.first);
522             });
523             if (capture < 0)
524             {
525                 write_to_debug_buffer(format("Error while evaluating dynamic regex expression faces,"
526                                              " {} is neither a capture index nor a capture name",
527                                              face.first));
528                 return FacesSpec{};
529             }
530             spec.emplace_back(capture, face.second);
531         }
532         return spec;
533     };
534 
535     CommandParser parser{params[0]};
536     auto token = parser.read_token(true);
537     if (token and parser.done() and token->type == Token::Type::OptionExpand and
538         GlobalScope::instance().options()[token->content].is_of_type<Regex>())
539     {
540         auto get_regex = [option_name = token->content](const Context& context) {
541             return context.options()[option_name].get<Regex>();
542         };
543         return make_hl(get_regex, get_face);
544     }
545 
546     auto get_regex = [expr = params[0]](const Context& context){
547         try
548         {
549             auto re = expand(expr, context);
550             return re.empty() ? Regex{} : Regex{re};
551         }
552         catch (runtime_error& err)
553         {
554             write_to_debug_buffer(format("Error while evaluating dynamic regex expression: {}", err.what()));
555             return Regex{};
556         }
557     };
558     return make_hl(get_regex, get_face);
559 }
560 
561 const HighlighterDesc line_desc = {
562     "Parameters: <value string> <face>\n"
563     "Highlight the line given by evaluating <value string> with <face>",
564     {}
565 };
create_line_highlighter(HighlighterParameters params,Highlighter *)566 std::unique_ptr<Highlighter> create_line_highlighter(HighlighterParameters params, Highlighter*)
567 {
568     if (params.size() != 2)
569         throw runtime_error("wrong parameter count");
570 
571     auto func = [line_expr=params[0], facespec=params[1]]
572                 (HighlightContext context, DisplayBuffer& display_buffer, BufferRange)
573     {
574         LineCount line = -1;
575         try
576         {
577             line = str_to_int_ifp(expand(line_expr, context.context)).value_or(0) - 1;
578         }
579         catch (runtime_error& err)
580         {
581             write_to_debug_buffer(
582                 format("Error evaluating highlight line expression: {}", err.what()));
583         }
584 
585         if (line < 0)
586             return;
587 
588         auto it = find_if(display_buffer.lines(),
589                           [line](const DisplayLine& l)
590                           { return l.range().begin.line == line; });
591         if (it == display_buffer.lines().end())
592             return;
593 
594         auto face = context.context.faces()[facespec];
595         ColumnCount column = 0;
596         for (auto& atom : *it)
597         {
598             column += atom.length();
599             if (!atom.has_buffer_range())
600                 continue;
601 
602             kak_assert(atom.begin().line == line);
603             apply_face(face)(atom);
604         }
605         const ColumnCount remaining = context.context.window().dimensions().column - column;
606         if (remaining > 0)
607             it->push_back({ String{' ', remaining}, face });
608     };
609 
610     return make_highlighter(std::move(func));
611 }
612 
613 const HighlighterDesc column_desc = {
614     "Parameters: <value string> <face>\n"
615     "Highlight the column given by evaluating <value string> with <face>",
616     {}
617 };
create_column_highlighter(HighlighterParameters params,Highlighter *)618 std::unique_ptr<Highlighter> create_column_highlighter(HighlighterParameters params, Highlighter*)
619 {
620     if (params.size() != 2)
621         throw runtime_error("wrong parameter count");
622 
623     auto func = [col_expr=params[0], facespec=params[1]]
624                 (HighlightContext context, DisplayBuffer& display_buffer, BufferRange)
625     {
626         ColumnCount column = -1;
627         try
628         {
629             column = str_to_int_ifp(expand(col_expr, context.context)).value_or(0) - 1;
630         }
631         catch (runtime_error& err)
632         {
633             write_to_debug_buffer(
634                 format("Error evaluating highlight column expression: {}", err.what()));
635         }
636 
637         if (column < 0)
638             return;
639 
640         const auto face = context.context.faces()[facespec];
641         const auto win_column = context.setup.window_pos.column;
642         const auto target_col = column - win_column;
643         if (target_col < 0 or target_col >= context.setup.window_range.column)
644             return;
645 
646         for (auto& line : display_buffer.lines())
647         {
648             auto remaining_col = target_col;
649             bool found = false;
650             auto first_buf = find_if(line, [](auto& atom) { return atom.has_buffer_range(); });
651             for (auto atom_it = first_buf; atom_it != line.end(); ++atom_it)
652             {
653                 const auto atom_len = atom_it->length();
654                 if (remaining_col < atom_len)
655                 {
656                     if (remaining_col > 0)
657                         atom_it = ++line.split(atom_it, remaining_col);
658                     if (atom_it->length() > 1)
659                         atom_it = line.split(atom_it, 1_col);
660                     atom_it->face = merge_faces(atom_it->face, face);
661                     found = true;
662                     break;
663                 }
664                 remaining_col -= atom_len;
665             }
666             if (found)
667                 continue;
668 
669             if (remaining_col > 0)
670                 line.push_back({String{' ', remaining_col}, {}});
671             line.push_back({" ", face});
672         }
673     };
674 
675     return make_highlighter(std::move(func));
676 }
677 
678 const HighlighterDesc wrap_desc = {
679     "Parameters: [-word] [-indent] [-width <max_width>] [-marker <marker_text>]\n"
680     "Wrap lines to window width",
681     { {
682         { "word",   { false, "wrap at word boundaries instead of codepoint boundaries" } },
683         { "indent", { false, "preserve line indentation of the wrapped line" } },
684         { "width",  { true, "wrap at the given column instead of the window's width" } },
685         { "marker", { true, "insert the given text at the beginning of the wrapped line" } }, },
686         ParameterDesc::Flags::None, 0, 0
687     }
688 };
689 struct WrapHighlighter : Highlighter
690 {
WrapHighlighterKakoune::WrapHighlighter691     WrapHighlighter(ColumnCount max_width, bool word_wrap, bool preserve_indent, String marker)
692         : Highlighter{HighlightPass::Wrap}, m_word_wrap{word_wrap},
693           m_preserve_indent{preserve_indent}, m_max_width{max_width},
694           m_marker{std::move(marker)} {}
695 
696     static constexpr StringView ms_id = "wrap";
697 
698     struct SplitPos{ ByteCount byte; ColumnCount column; };
699 
do_highlightKakoune::WrapHighlighter700     void do_highlight(HighlightContext context, DisplayBuffer& display_buffer, BufferRange) override
701     {
702         if (contains(context.disabled_ids, ms_id))
703             return;
704 
705         const ColumnCount wrap_column = std::min(m_max_width, context.setup.window_range.column);
706         if (wrap_column <= 0)
707             return;
708 
709         const Buffer& buffer = context.context.buffer();
710         const auto& cursor = context.context.selections().main().cursor();
711         const int tabstop = context.context.options()["tabstop"].get<int>();
712         const LineCount win_height = context.context.window().dimensions().line;
713         const ColumnCount marker_len = zero_if_greater(m_marker.column_length(), wrap_column);
714         const Face face_marker = context.context.faces()["WrapMarker"];
715         for (auto it = display_buffer.lines().begin();
716              it != display_buffer.lines().end(); ++it)
717         {
718             const LineCount buf_line = it->range().begin.line;
719             const ByteCount line_length = buffer[buf_line].length();
720             const ColumnCount indent = m_preserve_indent ?
721                 zero_if_greater(line_indent(buffer, tabstop, buf_line), wrap_column) : 0_col;
722             const ColumnCount prefix_len = std::max(marker_len, indent);
723 
724             auto pos = next_split_pos(buffer, wrap_column, prefix_len, tabstop, buf_line, {0, 0});
725             if (pos.byte == line_length)
726                 continue;
727 
728             for (auto atom_it = it->begin();
729                  pos.byte != line_length and atom_it != it->end(); )
730             {
731                 const BufferCoord coord{buf_line, pos.byte};
732                 if (!atom_it->has_buffer_range() or
733                     coord < atom_it->begin() or coord >= atom_it->end())
734                 {
735                     ++atom_it;
736                     continue;
737                 }
738 
739                 auto& line = *it;
740 
741                 if (coord > atom_it->begin())
742                     atom_it = ++line.split(atom_it, coord);
743 
744                 DisplayLine new_line{ AtomList{ atom_it, line.end() } };
745                 line.erase(atom_it, line.end());
746 
747                 if (marker_len != 0)
748                     new_line.insert(new_line.begin(), {m_marker, face_marker});
749                 if (indent > marker_len)
750                 {
751                     auto it = new_line.insert(new_line.begin() + (marker_len > 0), {buffer, coord, coord});
752                     it->replace(String{' ', indent - marker_len});
753                 }
754 
755                 if (it+1 - display_buffer.lines().begin() == win_height)
756                 {
757                     if (cursor >= new_line.range().begin) // strip first lines if cursor is not visible
758                     {
759                         display_buffer.lines().erase(display_buffer.lines().begin(), display_buffer.lines().begin()+1);
760                         --it;
761                     }
762                     else
763                     {
764                         display_buffer.lines().erase(it+1, display_buffer.lines().end());
765                         return;
766                     }
767                 }
768                 it = display_buffer.lines().insert(it+1, new_line);
769 
770                 pos = next_split_pos(buffer, wrap_column - prefix_len, prefix_len, tabstop, buf_line, pos);
771                 atom_it = it->begin();
772             }
773         }
774     }
775 
do_compute_display_setupKakoune::WrapHighlighter776     void do_compute_display_setup(HighlightContext context, DisplaySetup& setup) const override
777     {
778         if (contains(context.disabled_ids, ms_id))
779             return;
780 
781         const ColumnCount wrap_column = std::min(setup.window_range.column, m_max_width);
782         if (wrap_column <= 0)
783             return;
784 
785         const Buffer& buffer = context.context.buffer();
786         const auto& cursor = context.context.selections().main().cursor();
787         const int tabstop = context.context.options()["tabstop"].get<int>();
788 
789         auto line_wrap_count = [&](LineCount line, ColumnCount prefix_len) {
790             LineCount count = 0;
791             const ByteCount line_length = buffer[line].length();
792             SplitPos pos{0, 0};
793             while (true)
794             {
795                 pos = next_split_pos(buffer, wrap_column - (pos.byte == 0 ? 0_col : prefix_len),
796                                      prefix_len, tabstop, line, pos);
797                 if (pos.byte == line_length)
798                     break;
799                 ++count;
800             }
801             return count;
802         };
803 
804         const auto win_height = context.context.window().dimensions().line;
805 
806         // Disable horizontal scrolling when using a WrapHighlighter
807         setup.window_pos.column = 0;
808         setup.window_range.line = 0;
809         setup.scroll_offset.column = 0;
810         setup.full_lines = true;
811 
812         const ColumnCount marker_len = zero_if_greater(m_marker.column_length(), wrap_column);
813 
814         for (auto buf_line = setup.window_pos.line, win_line = 0_line;
815              win_line < win_height or buf_line <= cursor.line;
816              ++buf_line, ++setup.window_range.line)
817         {
818             if (buf_line >= buffer.line_count())
819                 break;
820 
821             const ColumnCount indent = m_preserve_indent ?
822                 zero_if_greater(line_indent(buffer, tabstop, buf_line), wrap_column) : 0_col;
823             const ColumnCount prefix_len = std::max(marker_len, indent);
824 
825             if (buf_line == cursor.line)
826             {
827                 SplitPos pos{0, 0};
828                 for (LineCount count = 0; true; ++count)
829                 {
830                     auto next_pos = next_split_pos(buffer, wrap_column - (pos.byte != 0 ? prefix_len : 0_col),
831                                                    prefix_len, tabstop, buf_line, pos);
832                     if (next_pos.byte > cursor.column)
833                     {
834                         setup.cursor_pos = DisplayCoord{
835                             win_line + count,
836                             get_column(buffer, tabstop, cursor) -
837                             pos.column + (pos.byte != 0 ? indent : 0_col)
838                         };
839                         break;
840                     }
841                     pos = next_pos;
842                 }
843                 kak_assert(setup.cursor_pos.column >= 0 and setup.cursor_pos.column < setup.window_range.column);
844             }
845             const auto wrap_count = line_wrap_count(buf_line, prefix_len);
846             win_line += wrap_count + 1;
847 
848             // scroll window to keep cursor visible, and update range as lines gets removed
849             while (buf_line >= cursor.line and setup.window_pos.line < cursor.line and
850                    setup.cursor_pos.line + setup.scroll_offset.line >= win_height)
851             {
852                 auto remove_count = 1 + line_wrap_count(setup.window_pos.line, indent);
853                 ++setup.window_pos.line;
854                 --setup.window_range.line;
855                 setup.cursor_pos.line -= std::min(win_height, remove_count);
856                 win_line -= remove_count;
857                 kak_assert(setup.cursor_pos.line >= 0);
858             }
859         }
860     }
861 
fill_unique_idsKakoune::WrapHighlighter862     void fill_unique_ids(Vector<StringView>& unique_ids) const override
863     {
864         unique_ids.push_back(ms_id);
865     }
866 
next_split_posKakoune::WrapHighlighter867     SplitPos next_split_pos(const Buffer& buffer,  ColumnCount wrap_column, ColumnCount prefix_len,
868                             int tabstop, LineCount line, SplitPos current) const
869     {
870         const ColumnCount target_column = current.column + wrap_column;
871         StringView content = buffer[line];
872 
873         SplitPos pos = current;
874         SplitPos last_word_boundary = {0, 0};
875         SplitPos last_WORD_boundary = {0, 0};
876 
877         auto update_boundaries = [&](Codepoint cp) {
878             if (not m_word_wrap)
879                 return;
880             if (!is_word<Word>(cp))
881                 last_word_boundary = pos;
882             if (!is_word<WORD>(cp))
883                 last_WORD_boundary = pos;
884         };
885 
886         while (pos.byte < content.length() and pos.column < target_column)
887         {
888             if (content[pos.byte] == '\t')
889             {
890                 const ColumnCount next_column = (pos.column / tabstop + 1) * tabstop;
891                 if (next_column > target_column and pos.byte != current.byte) // the target column was in the tab
892                     break;
893                 pos.column = next_column;
894                 ++pos.byte;
895                 last_word_boundary = last_WORD_boundary = pos;
896             }
897             else
898             {
899                 const char* it = &content[pos.byte];
900                 const Codepoint cp = utf8::read_codepoint(it, content.end());
901                 const ColumnCount width = codepoint_width(cp);
902                 if (pos.column + width > target_column and pos.byte != current.byte) // the target column was in the char
903                 {
904                     update_boundaries(cp);
905                     break;
906                 }
907                 pos.column += width;
908                 pos.byte = (int)(it - content.begin());
909                 update_boundaries(cp);
910             }
911         }
912 
913         if (m_word_wrap and pos.byte < content.length())
914         {
915             auto find_split_pos = [&](SplitPos start_pos, auto is_word) -> Optional<SplitPos> {
916                 if (start_pos.byte == 0)
917                     return {};
918                 const char* it = &content[pos.byte];
919                 // split at current position if is a word boundary
920                 if (not is_word(utf8::codepoint(it, content.end()), {'_'}))
921                     return pos;
922                 // split at last word boundary if the word is shorter than our wrapping width
923                 ColumnCount word_length = pos.column - start_pos.column;
924                 while (it != content.end() and word_length <= (wrap_column - prefix_len))
925                 {
926                     const Codepoint cp = utf8::read_codepoint(it, content.end());
927                     if (not is_word(cp, {'_'}))
928                         return start_pos;
929                     word_length += codepoint_width(cp);
930                 }
931                 return {};
932             };
933             if (auto split = find_split_pos(last_WORD_boundary, is_word<WORD>))
934                 return *split;
935             if (auto split = find_split_pos(last_word_boundary, is_word<Word>))
936                 return *split;
937         }
938 
939         return pos;
940     }
941 
line_indentKakoune::WrapHighlighter942     static ColumnCount line_indent(const Buffer& buffer, int tabstop, LineCount line)
943     {
944         StringView l = buffer[line];
945         auto col = 0_byte;
946         while (is_horizontal_blank(l[col]))
947                ++col;
948         return get_column(buffer, tabstop, {line, col});
949     }
950 
createKakoune::WrapHighlighter951     static std::unique_ptr<Highlighter> create(HighlighterParameters params, Highlighter*)
952     {
953         ParametersParser parser(params, wrap_desc.params);
954         ColumnCount max_width = parser.get_switch("width").map(str_to_int)
955             .value_or(std::numeric_limits<int>::max());
956 
957         return std::make_unique<WrapHighlighter>(max_width, (bool)parser.get_switch("word"),
958                                                  (bool)parser.get_switch("indent"),
959                                                  parser.get_switch("marker").value_or("").str());
960     }
961 
zero_if_greaterKakoune::WrapHighlighter962     static ColumnCount zero_if_greater(ColumnCount val, ColumnCount max) { return val < max ? val : 0; };
963 
964     const bool m_word_wrap;
965     const bool m_preserve_indent;
966     const ColumnCount m_max_width;
967     const String m_marker;
968 };
969 
970 constexpr StringView WrapHighlighter::ms_id;
971 
972 struct TabulationHighlighter : Highlighter
973 {
TabulationHighlighterKakoune::TabulationHighlighter974     TabulationHighlighter() : Highlighter{HighlightPass::Move} {}
975 
do_highlightKakoune::TabulationHighlighter976     void do_highlight(HighlightContext context, DisplayBuffer& display_buffer, BufferRange) override
977     {
978         const ColumnCount tabstop = context.context.options()["tabstop"].get<int>();
979         const auto& buffer = context.context.buffer();
980         auto win_column = context.setup.window_pos.column;
981         for (auto& line : display_buffer.lines())
982         {
983             for (auto atom_it = line.begin(); atom_it != line.end(); ++atom_it)
984             {
985                 if (atom_it->type() != DisplayAtom::Range)
986                     continue;
987 
988                 auto begin = get_iterator(buffer, atom_it->begin());
989                 auto end = get_iterator(buffer, atom_it->end());
990                 for (BufferIterator it = begin; it != end; ++it)
991                 {
992                     if (*it == '\t')
993                     {
994                         if (it != begin)
995                             atom_it = ++line.split(atom_it, it.coord());
996                         if (it+1 != end)
997                             atom_it = line.split(atom_it, (it+1).coord());
998 
999                         const ColumnCount column = get_column(buffer, tabstop, it.coord());
1000                         const ColumnCount count = tabstop - (column % tabstop) -
1001                                                   std::max(win_column - column, 0_col);
1002                         atom_it->replace(String{' ', count});
1003                         break;
1004                     }
1005                 }
1006             }
1007         }
1008     }
1009 
do_compute_display_setupKakoune::TabulationHighlighter1010     void do_compute_display_setup(HighlightContext context, DisplaySetup& setup) const override
1011     {
1012         auto& buffer = context.context.buffer();
1013         // Ensure that a cursor on a tab character makes the full tab character visible
1014         auto cursor = context.context.selections().main().cursor();
1015         if (buffer.byte_at(cursor) != '\t')
1016             return;
1017 
1018         const ColumnCount tabstop = context.context.options()["tabstop"].get<int>();
1019         const ColumnCount column = get_column(buffer, tabstop, cursor);
1020         const ColumnCount width = tabstop - (column % tabstop);
1021         const ColumnCount win_end = setup.window_pos.column + setup.window_range.column;
1022         const ColumnCount offset = std::max(column + width - win_end, 0_col);
1023 
1024         setup.window_pos.column += offset;
1025         setup.cursor_pos.column -= offset;
1026     }
1027 };
1028 
1029 const HighlighterDesc show_whitespace_desc = {
1030     "Parameters: [-tab <separator>] [-tabpad <separator>] [-lf <separator>] [-spc <separator>] [-nbsp <separator>]\n"
1031     "Display whitespaces using symbols",
1032     { {
1033         { "tab",    { true, "replace tabulations with the given character" } },
1034         { "tabpad", { true, "append as many of the given character as is necessary to honor `tabstop`" } },
1035         { "spc",    { true, "replace spaces with the given character" } },
1036         { "lf",     { true, "replace line feeds with the given character" } },
1037         { "nbsp",   { true, "replace non-breakable spaces with the given character" } } },
1038         ParameterDesc::Flags::None, 0, 0
1039     }
1040 };
1041 struct ShowWhitespacesHighlighter : Highlighter
1042 {
ShowWhitespacesHighlighterKakoune::ShowWhitespacesHighlighter1043     ShowWhitespacesHighlighter(String tab, String tabpad, String spc, String lf, String nbsp)
1044       : Highlighter{HighlightPass::Move}, m_tab{std::move(tab)}, m_tabpad{std::move(tabpad)},
1045         m_spc{std::move(spc)}, m_lf{std::move(lf)}, m_nbsp{std::move(nbsp)}
1046     {}
1047 
createKakoune::ShowWhitespacesHighlighter1048     static std::unique_ptr<Highlighter> create(HighlighterParameters params, Highlighter*)
1049     {
1050         ParametersParser parser(params, show_whitespace_desc.params);
1051 
1052         auto get_param = [&](StringView param,  StringView fallback) {
1053             StringView value = parser.get_switch(param).value_or(fallback);
1054             if (value.char_length() != 1)
1055                 throw runtime_error{format("-{} expects a single character parameter", param)};
1056             return value.str();
1057         };
1058 
1059         return std::make_unique<ShowWhitespacesHighlighter>(
1060             get_param("tab", "→"), get_param("tabpad", " "), get_param("spc", "·"),
1061             get_param("lf", "¬"), get_param("nbsp", "⍽"));
1062     }
1063 
1064 private:
do_highlightKakoune::ShowWhitespacesHighlighter1065     void do_highlight(HighlightContext context, DisplayBuffer& display_buffer, BufferRange) override
1066     {
1067         const int tabstop = context.context.options()["tabstop"].get<int>();
1068         auto whitespaceface = context.context.faces()["Whitespace"];
1069         const auto& buffer = context.context.buffer();
1070         auto win_column = context.setup.window_pos.column;
1071         for (auto& line : display_buffer.lines())
1072         {
1073             for (auto atom_it = line.begin(); atom_it != line.end(); ++atom_it)
1074             {
1075                 if (atom_it->type() != DisplayAtom::Range)
1076                     continue;
1077 
1078                 auto begin = get_iterator(buffer, atom_it->begin());
1079                 auto end = get_iterator(buffer, atom_it->end());
1080                 for (BufferIterator it = begin; it != end; )
1081                 {
1082                     auto coord = it.coord();
1083                     Codepoint cp = utf8::read_codepoint(it, end);
1084                     if (cp == '\t' or cp == ' ' or cp == '\n' or cp == 0xA0 or cp == 0x202F)
1085                     {
1086                         if (coord != begin.coord())
1087                             atom_it = ++line.split(atom_it, coord);
1088                         if (it != end)
1089                             atom_it = line.split(atom_it, it.coord());
1090 
1091                         if (cp == '\t')
1092                         {
1093                             const ColumnCount column = get_column(buffer, tabstop, coord);
1094                             const ColumnCount count = tabstop - (column % tabstop) -
1095                                                       std::max(win_column - column, 0_col);
1096                             atom_it->replace(m_tab + String(m_tabpad[(CharCount)0], count - m_tab.column_length()));
1097                         }
1098                         else if (cp == ' ')
1099                             atom_it->replace(m_spc);
1100                         else if (cp == '\n')
1101                             atom_it->replace(m_lf);
1102                         else if (cp == 0xA0 or cp == 0x202F)
1103                             atom_it->replace(m_nbsp);
1104                         atom_it->face = merge_faces(atom_it->face, whitespaceface);
1105                         break;
1106                     }
1107                 }
1108             }
1109         }
1110     }
1111 
1112     const String m_tab, m_tabpad, m_spc, m_lf, m_nbsp;
1113 };
1114 
1115 const HighlighterDesc line_numbers_desc = {
1116     "Parameters: [-relative] [-hlcursor] [-separators <separator|separator:cursor|cursor:up:down>] [-min-digits <cols>]\n"
1117     "Display line numbers",
1118     { {
1119         { "relative", { false, "show line numbers relative to the main cursor line" } },
1120         { "separator", { true, "string to separate the line numbers column from the rest of the buffer (default '|')" } },
1121         { "cursor-separator", { true, "identical to -separator but applies only to the line of the cursor (default is the same value passed to -separator)" } },
1122         { "min-digits", { true, "use at least the given number of columns to display line numbers (default 2)" } },
1123         { "hlcursor", { false, "highlight the cursor line with a separate face" } } },
1124         ParameterDesc::Flags::None, 0, 0
1125     }
1126 };
1127 struct LineNumbersHighlighter : Highlighter
1128 {
LineNumbersHighlighterKakoune::LineNumbersHighlighter1129     LineNumbersHighlighter(bool relative, bool hl_cursor_line, String separator, String cursor_separator, int min_digits)
1130       : Highlighter{HighlightPass::Move},
1131         m_relative{relative},
1132         m_hl_cursor_line{hl_cursor_line},
1133         m_separator{std::move(separator)},
1134         m_cursor_separator{std::move(cursor_separator)},
1135         m_min_digits{min_digits} {}
1136 
createKakoune::LineNumbersHighlighter1137     static std::unique_ptr<Highlighter> create(HighlighterParameters params, Highlighter*)
1138     {
1139         ParametersParser parser(params, line_numbers_desc.params);
1140 
1141         StringView separator = parser.get_switch("separator").value_or("│");
1142         StringView cursor_separator = parser.get_switch("cursor-separator").value_or(separator);
1143 
1144         if (separator.length() > 10)
1145             throw runtime_error("separator length is limited to 10 bytes");
1146 
1147         if (cursor_separator.column_length() != separator.column_length())
1148             throw runtime_error("separator for active line should have the same length as 'separator'");
1149 
1150         int min_digits = parser.get_switch("min-digits").map(str_to_int).value_or(2);
1151         if (min_digits < 0)
1152             throw runtime_error("min digits must be positive");
1153         if (min_digits > 10)
1154             throw runtime_error("min digits is limited to 10");
1155 
1156         return std::make_unique<LineNumbersHighlighter>((bool)parser.get_switch("relative"), (bool)parser.get_switch("hlcursor"), separator.str(), cursor_separator.str(), min_digits);
1157     }
1158 
1159 private:
1160     static constexpr StringView ms_id = "line-numbers";
1161 
do_highlightKakoune::LineNumbersHighlighter1162     void do_highlight(HighlightContext context, DisplayBuffer& display_buffer, BufferRange) override
1163     {
1164         if (contains(context.disabled_ids, ms_id))
1165             return;
1166 
1167         const auto& faces = context.context.faces();
1168         const Face face = faces["LineNumbers"];
1169         const Face face_wrapped = faces["LineNumbersWrapped"];
1170         const Face face_absolute = faces["LineNumberCursor"];
1171         int digit_count = compute_digit_count(context.context);
1172 
1173         char format[16];
1174         format_to(format, "%{}d", digit_count);
1175         const int main_line = (int)context.context.selections().main().cursor().line + 1;
1176         int last_line = -1;
1177         for (auto& line : display_buffer.lines())
1178         {
1179             const int current_line = (int)line.range().begin.line + 1;
1180             const bool is_cursor_line = main_line == current_line;
1181             const int line_to_format = (m_relative and not is_cursor_line) ?
1182                                        current_line - main_line : current_line;
1183             char buffer[16];
1184             snprintf(buffer, 16, format, std::abs(line_to_format));
1185             const auto atom_face = last_line == current_line ? face_wrapped :
1186                 ((m_hl_cursor_line and is_cursor_line) ? face_absolute : face);
1187 
1188             const auto& separator = is_cursor_line && last_line != current_line
1189                                     ? m_cursor_separator : m_separator;
1190 
1191             line.insert(line.begin(), {buffer, atom_face});
1192             line.insert(line.begin() + 1, {separator, face});
1193 
1194             last_line = current_line;
1195         }
1196     }
1197 
do_compute_display_setupKakoune::LineNumbersHighlighter1198     void do_compute_display_setup(HighlightContext context, DisplaySetup& setup) const override
1199     {
1200         if (contains(context.disabled_ids, ms_id))
1201             return;
1202 
1203         ColumnCount width = compute_digit_count(context.context) + m_separator.column_length();
1204         setup.window_range.column -= std::min(width, setup.window_range.column);
1205     }
1206 
fill_unique_idsKakoune::LineNumbersHighlighter1207     void fill_unique_ids(Vector<StringView>& unique_ids) const override
1208     {
1209         unique_ids.push_back(ms_id);
1210     }
1211 
compute_digit_countKakoune::LineNumbersHighlighter1212     int compute_digit_count(const Context& context) const
1213     {
1214         int digit_count = 0;
1215         LineCount last_line = context.buffer().line_count();
1216         for (LineCount c = last_line; c > 0; c /= 10)
1217             ++digit_count;
1218         return std::max(digit_count, m_min_digits);
1219     }
1220 
1221     const bool m_relative;
1222     const bool m_hl_cursor_line;
1223     const String m_separator;
1224     const String m_cursor_separator;
1225     const int m_min_digits;
1226 };
1227 
1228 constexpr StringView LineNumbersHighlighter::ms_id;
1229 
1230 
1231 const HighlighterDesc show_matching_desc = {
1232     "Apply the MatchingChar face to the char matching the one under the cursor",
1233     {}
1234 };
show_matching_char(HighlightContext context,DisplayBuffer & display_buffer,BufferRange)1235 void show_matching_char(HighlightContext context, DisplayBuffer& display_buffer, BufferRange)
1236 {
1237     const Face face = context.context.faces()["MatchingChar"];
1238     const auto& matching_pairs = context.context.options()["matching_pairs"].get<Vector<Codepoint, MemoryDomain::Options>>();
1239     const auto range = display_buffer.range();
1240     const auto& buffer = context.context.buffer();
1241     for (auto& sel : context.context.selections())
1242     {
1243         auto pos = sel.cursor();
1244         if (pos < range.begin or pos >= range.end)
1245             continue;
1246 
1247         Utf8Iterator it{buffer.iterator_at(pos), buffer};
1248         auto match = find(matching_pairs, *it);
1249 
1250         if (match == matching_pairs.end())
1251             continue;
1252 
1253         int level = 0;
1254         if (((match - matching_pairs.begin()) % 2) == 0)
1255         {
1256             const Codepoint opening = *match;
1257             const Codepoint closing = *(match+1);
1258             while (it.base().coord() <= range.end)
1259             {
1260                 if (*it == opening)
1261                     ++level;
1262                 else if (*it == closing and --level == 0)
1263                 {
1264                     highlight_range(display_buffer, it.base().coord(), (it+1).base().coord(),
1265                                     false, apply_face(face));
1266                     break;
1267                 }
1268                 ++it;
1269             }
1270         }
1271         else if (pos > range.begin)
1272         {
1273             const Codepoint opening = *(match-1);
1274             const Codepoint closing = *match;
1275             while (true)
1276             {
1277                 if (*it == closing)
1278                     ++level;
1279                 else if (*it == opening and --level == 0)
1280                 {
1281                     highlight_range(display_buffer, it.base().coord(), (it+1).base().coord(),
1282                                     false, apply_face(face));
1283                     break;
1284                 }
1285                 if (it.base().coord() <= range.begin)
1286                     break;
1287                 --it;
1288             }
1289         }
1290     }
1291 }
1292 
create_matching_char_highlighter(HighlighterParameters params,Highlighter *)1293 std::unique_ptr<Highlighter> create_matching_char_highlighter(HighlighterParameters params, Highlighter*)
1294 {
1295     return make_highlighter(show_matching_char);
1296 }
1297 
highlight_selections(HighlightContext context,DisplayBuffer & display_buffer,BufferRange)1298 void highlight_selections(HighlightContext context, DisplayBuffer& display_buffer, BufferRange)
1299 {
1300     const auto& buffer = context.context.buffer();
1301     const auto& faces = context.context.faces();
1302     const Face sel_faces[6] = {
1303             faces["PrimarySelection"], faces["SecondarySelection"],
1304             faces["PrimaryCursor"],    faces["SecondaryCursor"],
1305             faces["PrimaryCursorEol"], faces["SecondaryCursorEol"],
1306     };
1307 
1308     const auto& selections = context.context.selections();
1309     for (size_t i = 0; i < selections.size(); ++i)
1310     {
1311         auto& sel = selections[i];
1312         const bool forward = sel.anchor() <= sel.cursor();
1313         BufferCoord begin = forward ? sel.anchor() : buffer.char_next(sel.cursor());
1314         BufferCoord end   = forward ? (BufferCoord)sel.cursor() : buffer.char_next(sel.anchor());
1315 
1316         const bool primary = (i == selections.main_index());
1317         highlight_range(display_buffer, begin, end, false,
1318                         apply_face(sel_faces[primary ? 0 : 1]));
1319     }
1320     for (size_t i = 0; i < selections.size(); ++i)
1321     {
1322         auto& sel = selections[i];
1323         const BufferCoord coord = sel.cursor();
1324         const bool primary = (i == selections.main_index());
1325         const bool eol = buffer[coord.line].length() - 1 == coord.column;
1326         highlight_range(display_buffer, coord, buffer.char_next(coord), false,
1327                         apply_face(sel_faces[2 + (eol ? 2 : 0) + (primary ? 0 : 1)]));
1328     }
1329 }
1330 
expand_unprintable(HighlightContext context,DisplayBuffer & display_buffer,BufferRange)1331 void expand_unprintable(HighlightContext context, DisplayBuffer& display_buffer, BufferRange)
1332 {
1333     const auto& buffer = context.context.buffer();
1334     auto error = context.context.faces()["Error"];
1335     for (auto& line : display_buffer.lines())
1336     {
1337         for (auto atom_it = line.begin(); atom_it != line.end(); ++atom_it)
1338         {
1339             if (atom_it->type() == DisplayAtom::Range)
1340             {
1341                 for (auto it  = get_iterator(buffer, atom_it->begin()),
1342                           end = get_iterator(buffer, atom_it->end()); it < end;)
1343                 {
1344                     auto coord = it.coord();
1345                     Codepoint cp = utf8::read_codepoint(it, end);
1346                     if (cp != '\n' and not iswprint((wchar_t)cp))
1347                     {
1348                         if (coord != atom_it->begin())
1349                             atom_it = ++line.split(atom_it, coord);
1350                         if (it.coord() < atom_it->end())
1351                             atom_it = line.split(atom_it, it.coord());
1352 
1353                         atom_it->replace("�");
1354                         atom_it->face = error;
1355                         break;
1356                     }
1357                 }
1358             }
1359         }
1360     }
1361 }
1362 
update_line_specs_ifn(const Buffer & buffer,LineAndSpecList & line_flags)1363 static void update_line_specs_ifn(const Buffer& buffer, LineAndSpecList& line_flags)
1364 {
1365     if (line_flags.prefix == buffer.timestamp())
1366         return;
1367 
1368     auto& lines = line_flags.list;
1369 
1370     auto modifs = compute_line_modifications(buffer, line_flags.prefix);
1371     auto ins_pos = lines.begin();
1372     for (auto it = lines.begin(); it != lines.end(); ++it)
1373     {
1374         auto& line = std::get<0>(*it); // that line is 1 based as it comes from user side
1375         auto modif_it = std::upper_bound(modifs.begin(), modifs.end(), line-1,
1376                                          [](const LineCount& l, const LineModification& c)
1377                                          { return l < c.old_line; });
1378         if (modif_it != modifs.begin())
1379         {
1380             auto& prev = *(modif_it-1);
1381             if (line-1 < prev.old_line + prev.num_removed)
1382                 continue; // line removed
1383 
1384             line += prev.diff();
1385         }
1386 
1387         if (ins_pos != it)
1388             *ins_pos = std::move(*it);
1389         ++ins_pos;
1390     }
1391     lines.erase(ins_pos, lines.end());
1392     line_flags.prefix = buffer.timestamp();
1393 }
1394 
option_update(LineAndSpecList & opt,const Context & context)1395 void option_update(LineAndSpecList& opt, const Context& context)
1396 {
1397     update_line_specs_ifn(context.buffer(), opt);
1398 }
1399 
option_list_postprocess(Vector<LineAndSpec,MemoryDomain::Options> & opt)1400 void option_list_postprocess(Vector<LineAndSpec, MemoryDomain::Options>& opt)
1401 {
1402     std::sort(opt.begin(), opt.end(),
1403               [](auto& lhs, auto& rhs)
1404               { return std::get<0>(lhs) < std::get<0>(rhs); });
1405 }
1406 
1407 const HighlighterDesc flag_lines_desc = {
1408     "Parameters: <face> <option name>\n"
1409     "Display flags specified in the line-spec option <option name> with <face>",
1410     {}
1411 };
1412 struct FlagLinesHighlighter : Highlighter
1413 {
FlagLinesHighlighterKakoune::FlagLinesHighlighter1414     FlagLinesHighlighter(String option_name, String default_face)
1415         : Highlighter{HighlightPass::Move},
1416           m_option_name{std::move(option_name)},
1417           m_default_face{std::move(default_face)} {}
1418 
createKakoune::FlagLinesHighlighter1419     static std::unique_ptr<Highlighter> create(HighlighterParameters params, Highlighter*)
1420     {
1421         if (params.size() != 2)
1422             throw runtime_error("wrong parameter count");
1423 
1424         const String& option_name = params[1];
1425         const String& default_face = params[0];
1426 
1427         // throw if wrong option type
1428         GlobalScope::instance().options()[option_name].get<LineAndSpecList>();
1429 
1430         return std::make_unique<FlagLinesHighlighter>(option_name, default_face);
1431     }
1432 
1433 private:
do_highlightKakoune::FlagLinesHighlighter1434     void do_highlight(HighlightContext context, DisplayBuffer& display_buffer, BufferRange) override
1435     {
1436         auto& line_flags = context.context.options()[m_option_name].get_mutable<LineAndSpecList>();
1437         const auto& buffer = context.context.buffer();
1438         update_line_specs_ifn(buffer, line_flags);
1439 
1440         auto def_face = context.context.faces()[m_default_face];
1441         Vector<DisplayLine> display_lines;
1442         auto& lines = line_flags.list;
1443         try
1444         {
1445             for (auto& line : lines)
1446             {
1447                 display_lines.push_back(parse_display_line(std::get<1>(line), context.context.faces()));
1448                 for (auto& atom : display_lines.back())
1449                     atom.face = merge_faces(def_face, atom.face);
1450             }
1451         }
1452         catch (runtime_error& err)
1453         {
1454             write_to_debug_buffer(format("Error while evaluating line flag: {}", err.what()));
1455             return;
1456         }
1457 
1458         ColumnCount width = 0;
1459         for (auto& l : display_lines)
1460              width = std::max(width, l.length());
1461         const DisplayAtom empty{String{' ', width}, def_face};
1462         for (auto& line : display_buffer.lines())
1463         {
1464             int line_num = (int)line.range().begin.line + 1;
1465             auto it = find_if(lines,
1466                               [&](const LineAndSpec& l)
1467                               { return std::get<0>(l) == line_num; });
1468             if (it == lines.end())
1469                 line.insert(line.begin(), empty);
1470             else
1471             {
1472                 DisplayLine& display_line = display_lines[it - lines.begin()];
1473                 DisplayAtom padding_atom{String(' ', width - display_line.length()), def_face};
1474                 auto it = std::copy(std::make_move_iterator(display_line.begin()),
1475                                     std::make_move_iterator(display_line.end()),
1476                                     std::inserter(line, line.begin()));
1477 
1478                 if (padding_atom.length() != 0)
1479                     *it++ = std::move(padding_atom);
1480             }
1481         }
1482     }
1483 
do_compute_display_setupKakoune::FlagLinesHighlighter1484     void do_compute_display_setup(HighlightContext context, DisplaySetup& setup) const override
1485     {
1486         auto& line_flags = context.context.options()[m_option_name].get_mutable<LineAndSpecList>();
1487         const auto& buffer = context.context.buffer();
1488         update_line_specs_ifn(buffer, line_flags);
1489 
1490         ColumnCount width = 0;
1491         try
1492         {
1493             for (auto& line : line_flags.list)
1494                 width = std::max(parse_display_line(std::get<1>(line), context.context.faces()).length(), width);
1495         }
1496         catch (runtime_error& err)
1497         {
1498             write_to_debug_buffer(format("Error while evaluating line flag: {}", err.what()));
1499             return;
1500         }
1501 
1502         setup.window_range.column -= std::min(width, setup.window_range.column);
1503     }
1504 
1505     String m_option_name;
1506     String m_default_face;
1507 };
1508 
is_empty(const InclusiveBufferRange & range)1509 bool is_empty(const InclusiveBufferRange& range)
1510 {
1511     return range.last < BufferCoord{0,0};
1512 }
1513 
option_to_string(InclusiveBufferRange range)1514 String option_to_string(InclusiveBufferRange range)
1515 {
1516     if (is_empty(range))
1517         return format("{}.{}+0", range.first.line+1, range.first.column+1);
1518     return format("{}.{},{}.{}",
1519                   range.first.line+1, range.first.column+1,
1520                   range.last.line+1, range.last.column+1);
1521 }
1522 
option_from_string(Meta::Type<InclusiveBufferRange>,StringView str)1523 InclusiveBufferRange option_from_string(Meta::Type<InclusiveBufferRange>, StringView str)
1524 {
1525     auto sep = find_if(str, [](char c){ return c == ',' or c == '+'; });
1526     auto dot_beg = find(StringView{str.begin(), sep}, '.');
1527     auto dot_end = find(StringView{sep, str.end()}, '.');
1528 
1529     if (sep == str.end() or dot_beg == sep or
1530         (*sep == ',' and dot_end == str.end()))
1531         throw runtime_error(format("'{}' does not follow <line>.<column>,<line>.<column> or <line>.<column>+<len> format", str));
1532 
1533     const BufferCoord first{str_to_int({str.begin(), dot_beg}) - 1,
1534                             str_to_int({dot_beg+1, sep}) - 1};
1535 
1536     if (first.line < 0 or first.column < 0)
1537         throw runtime_error("coordinates elements should be >= 1");
1538 
1539     if (*sep == '+')
1540     {
1541         auto len = str_to_int({sep+1, str.end()});
1542         return {first, len == 0 ? BufferCoord{-1,-1} : BufferCoord{first.line, first.column + len - 1}};
1543     }
1544 
1545     const BufferCoord last{str_to_int({sep+1, dot_end}) - 1, str_to_int({dot_end+1, str.end()}) - 1};
1546     if (last.line < 0 or last.column < 0)
1547         throw runtime_error("coordinates elements should be >= 1");
1548 
1549     return { std::min(first, last), std::max(first, last) };
1550 }
1551 
1552 template<typename OptionType, typename DerivedType, HighlightPass pass = HighlightPass::Colorize>
1553 struct OptionBasedHighlighter : Highlighter
1554 {
OptionBasedHighlighterKakoune::OptionBasedHighlighter1555     OptionBasedHighlighter(String option_name)
1556         : Highlighter{pass}
1557         , m_option_name{std::move(option_name)} {}
1558 
createKakoune::OptionBasedHighlighter1559     static std::unique_ptr<Highlighter> create(HighlighterParameters params, Highlighter*)
1560     {
1561         if (params.size() != 1)
1562             throw runtime_error("wrong parameter count");
1563 
1564         const String& option_name = params[0];
1565         // throw if wrong option type
1566         GlobalScope::instance().options()[option_name].get<OptionType>();
1567 
1568         return std::make_unique<DerivedType>(option_name);
1569     }
1570 
get_optionKakoune::OptionBasedHighlighter1571     OptionType& get_option(const HighlightContext& context) const
1572     {
1573         return context.context.options()[m_option_name].template get_mutable<OptionType>();
1574     }
1575 
1576 private:
1577     const String m_option_name;
1578 };
1579 
get_first(RangeAndString & r)1580 BufferCoord& get_first(RangeAndString& r) { return std::get<0>(r).first; }
get_last(RangeAndString & r)1581 BufferCoord& get_last(RangeAndString& r) { return std::get<0>(r).last; }
1582 
option_element_compare(RangeAndString const & lhs,RangeAndString const & rhs)1583 bool option_element_compare(RangeAndString const& lhs, RangeAndString const& rhs)
1584 {
1585     return std::get<0>(lhs).first == std::get<0>(rhs).first ?
1586         std::get<0>(lhs).last < std::get<0>(rhs).last
1587       : std::get<0>(lhs).first < std::get<0>(rhs).first;
1588 }
1589 
option_list_postprocess(Vector<RangeAndString,MemoryDomain::Options> & opt)1590 void option_list_postprocess(Vector<RangeAndString, MemoryDomain::Options>& opt)
1591 {
1592     std::sort(opt.begin(), opt.end(), option_element_compare);
1593 }
1594 
option_update(RangeAndStringList & opt,const Context & context)1595 void option_update(RangeAndStringList& opt, const Context& context)
1596 {
1597     update_ranges(context.buffer(), opt.prefix, opt.list);
1598 }
1599 
option_add_from_strings(Vector<RangeAndString,MemoryDomain::Options> & opt,ConstArrayView<String> strs)1600 bool option_add_from_strings(Vector<RangeAndString, MemoryDomain::Options>& opt, ConstArrayView<String> strs)
1601 {
1602     auto vec = option_from_strings(Meta::Type<Vector<RangeAndString, MemoryDomain::Options>>{}, strs);
1603     if (vec.empty())
1604         return false;
1605     auto middle = opt.insert(opt.end(),
1606                              std::make_move_iterator(vec.begin()),
1607                              std::make_move_iterator(vec.end()));
1608     std::sort(middle, opt.end(), option_element_compare);
1609     std::inplace_merge(opt.begin(), middle, opt.end(), option_element_compare);
1610     return true;
1611 }
1612 
1613 const HighlighterDesc ranges_desc = {
1614     "Parameters: <option name>\n"
1615     "Use the range-specs option given as parameter to highlight buffer\n"
1616     "each spec is interpreted as a face to apply to the range",
1617     {}
1618 };
1619 struct RangesHighlighter : OptionBasedHighlighter<RangeAndStringList, RangesHighlighter>
1620 {
1621     using RangesHighlighter::OptionBasedHighlighter::OptionBasedHighlighter;
1622 private:
do_highlightKakoune::RangesHighlighter1623     void do_highlight(HighlightContext context, DisplayBuffer& display_buffer, BufferRange) override
1624     {
1625         auto& buffer = context.context.buffer();
1626         auto& range_and_faces = get_option(context);
1627         update_ranges(buffer, range_and_faces.prefix, range_and_faces.list);
1628 
1629         for (auto& [range, face] : range_and_faces.list)
1630         {
1631             try
1632             {
1633                 if (buffer.is_valid(range.first) and (buffer.is_valid(range.last) and not buffer.is_end(range.last)))
1634                     highlight_range(display_buffer, range.first, buffer.char_next(range.last), false,
1635                                     apply_face(context.context.faces()[face]));
1636             }
1637             catch (runtime_error&)
1638             {}
1639         }
1640     }
1641 };
1642 
1643 const HighlighterDesc replace_ranges_desc = {
1644     "Parameters: <option name>\n"
1645     "Use the range-specs option given as parameter to highlight buffer\n"
1646     "each spec is interpreted as a display line to display in place of the range",
1647     {}
1648 };
1649 struct ReplaceRangesHighlighter : OptionBasedHighlighter<RangeAndStringList, ReplaceRangesHighlighter, HighlightPass::Move>
1650 {
1651     using ReplaceRangesHighlighter::OptionBasedHighlighter::OptionBasedHighlighter;
1652 private:
is_validKakoune::ReplaceRangesHighlighter1653     static bool is_valid(Buffer& buffer, BufferCoord c)
1654     {
1655         return c.line >= 0 and c.column >= 0 and c.line < buffer.line_count() and c.column <= buffer[c.line].length();
1656     }
1657 
is_fully_selectedKakoune::ReplaceRangesHighlighter1658     static bool is_fully_selected(const SelectionList& sels, const InclusiveBufferRange& range)
1659     {
1660         auto it = std::lower_bound(sels.begin(), sels.end(), range.first, [](const Selection& s, const BufferCoord& c) { return s.max() < c; });
1661         if (it == sels.end())
1662             return true;
1663         return it->min() > range.last or (it->min() <= range.first and it->max() >= range.last);
1664     }
1665 
do_highlightKakoune::ReplaceRangesHighlighter1666     void do_highlight(HighlightContext context, DisplayBuffer& display_buffer, BufferRange) override
1667     {
1668         auto& buffer = context.context.buffer();
1669         auto& sels = context.context.selections();
1670         auto& range_and_faces = get_option(context);
1671         update_ranges(buffer, range_and_faces.prefix, range_and_faces.list);
1672 
1673         for (auto& [range, spec] : range_and_faces.list)
1674         {
1675             try
1676             {
1677                 if (!is_valid(buffer, range.first) or (!is_empty(range) and !is_valid(buffer, range.last)) or !is_fully_selected(sels, range))
1678                     continue;
1679                 auto replacement = parse_display_line(spec, context.context.faces());
1680                 auto end = is_empty(range) ? range.first : buffer.char_next(range.last);
1681                 replace_range(display_buffer, range.first, end,
1682                               [&, range=BufferRange{range.first, end}]
1683                               (DisplayLine& line, DisplayLine::iterator pos){
1684                                   for (auto& atom : replacement)
1685                                   {
1686                                       atom.replace(range);
1687                                       pos = ++line.insert(pos, std::move(atom));
1688                                   }
1689                               });
1690             }
1691             catch (runtime_error&)
1692             {}
1693         }
1694     }
1695 
do_compute_display_setupKakoune::ReplaceRangesHighlighter1696     void do_compute_display_setup(HighlightContext context, DisplaySetup& setup) const override
1697     {
1698         auto& buffer = context.context.buffer();
1699         auto& sels = context.context.selections();
1700         auto& range_and_faces = get_option(context);
1701         update_ranges(buffer, range_and_faces.prefix, range_and_faces.list);
1702 
1703         for (auto& [range, spec] : range_and_faces.list)
1704         {
1705             if (!is_valid(buffer, range.first) or (!is_empty(range) and !is_valid(buffer, range.last)) or !is_fully_selected(sels, range))
1706                 continue;
1707 
1708             if (range.first.line < setup.window_pos.line and range.last.line >= setup.window_pos.line)
1709                 setup.window_pos.line = range.first.line;
1710 
1711             if (range.last.line >= setup.window_pos.line and
1712                 range.first.line <= setup.window_pos.line + setup.window_range.line and
1713                 range.first.line != range.last.line)
1714             {
1715                 auto removed_count = range.last.line - range.first.line;
1716                 setup.window_range.line += removed_count;
1717             }
1718         }
1719     }
1720 };
1721 
parse_passes(StringView str)1722 HighlightPass parse_passes(StringView str)
1723 {
1724     HighlightPass passes{};
1725     for (auto pass : str | split<StringView>('|'))
1726     {
1727         if (pass == "colorize")
1728             passes |= HighlightPass::Colorize;
1729         else if (pass == "move")
1730             passes |= HighlightPass::Move;
1731         else if (pass == "wrap")
1732             passes |= HighlightPass::Wrap;
1733         else
1734             throw runtime_error{format("invalid highlight pass: {}", pass)};
1735     }
1736     if (passes == HighlightPass{})
1737         throw runtime_error{"no passes specified"};
1738 
1739     return passes;
1740 }
1741 
1742 const HighlighterDesc higlighter_group_desc = {
1743     "Parameters: [-passes <passes>]\n"
1744     "Creates a group that can contain other highlighters",
1745     { {
1746         { "passes", { true, "flags(colorize|move|wrap) "
1747                             "kind of highlighters can be put in the group "
1748                             "(default colorize)" } } },
1749         ParameterDesc::Flags::SwitchesOnlyAtStart, 0, 0
1750     }
1751 };
create_highlighter_group(HighlighterParameters params,Highlighter *)1752 std::unique_ptr<Highlighter> create_highlighter_group(HighlighterParameters params, Highlighter*)
1753 {
1754     ParametersParser parser{params, higlighter_group_desc.params};
1755     HighlightPass passes = parse_passes(parser.get_switch("passes").value_or("colorize"));
1756 
1757     return std::make_unique<HighlighterGroup>(passes);
1758 }
1759 
1760 const HighlighterDesc ref_desc = {
1761     "Parameters: [-passes <passes>] <path>\n"
1762     "Reference the highlighter at <path> in shared highlighters",
1763     { {
1764         { "passes", { true, "flags(colorize|move|wrap) "
1765                             "kind of highlighters that can be referenced "
1766                             "(default colorize)" } } },
1767         ParameterDesc::Flags::SwitchesOnlyAtStart, 1, 1
1768     }
1769 };
1770 struct ReferenceHighlighter : Highlighter
1771 {
ReferenceHighlighterKakoune::ReferenceHighlighter1772     ReferenceHighlighter(HighlightPass passes, String name)
1773         : Highlighter{passes}, m_name{std::move(name)} {}
1774 
createKakoune::ReferenceHighlighter1775     static std::unique_ptr<Highlighter> create(HighlighterParameters params, Highlighter*)
1776     {
1777         ParametersParser parser{params, ref_desc.params};
1778         HighlightPass passes = parse_passes(parser.get_switch("passes").value_or("colorize"));
1779         return std::make_unique<ReferenceHighlighter>(passes, parser[0]);
1780     }
1781 
1782 private:
do_highlightKakoune::ReferenceHighlighter1783     void do_highlight(HighlightContext context, DisplayBuffer& display_buffer, BufferRange range) override
1784     {
1785         static Vector<std::pair<StringView, BufferRange>> running_refs;
1786         const std::pair<StringView, BufferRange> desc{m_name, range};
1787         if (contains(running_refs, desc))
1788             return write_to_debug_buffer(format("highlighting recursion detected with ref to {}", m_name));
1789 
1790         running_refs.push_back(desc);
1791         auto pop_desc = on_scope_end([] { running_refs.pop_back(); });
1792 
1793         try
1794         {
1795             SharedHighlighters::instance().get_child(m_name).highlight(context, display_buffer, range);
1796         }
1797         catch (child_not_found&)
1798         {}
1799     }
1800 
do_compute_display_setupKakoune::ReferenceHighlighter1801     void do_compute_display_setup(HighlightContext context, DisplaySetup& setup) const override
1802     {
1803         try
1804         {
1805             SharedHighlighters::instance().get_child(m_name).compute_display_setup(context, setup);
1806         }
1807         catch (child_not_found&)
1808         {}
1809     }
1810 
1811     const String m_name;
1812 };
1813 
1814 struct RegexMatch
1815 {
1816     LineCount line;
1817     ByteCount begin;
1818     ByteCount end;
1819     uint16_t capture_pos;
1820     uint16_t capture_len;
1821 
begin_coordKakoune::RegexMatch1822     BufferCoord begin_coord() const { return { line, begin }; }
end_coordKakoune::RegexMatch1823     BufferCoord end_coord() const { return { line, end }; }
emptyKakoune::RegexMatch1824     bool empty() const { return begin == end; }
1825 
captureKakoune::RegexMatch1826     StringView capture(const Buffer& buffer) const
1827     {
1828         if (capture_len == 0)
1829             return {};
1830         return buffer[line].substr(begin + capture_pos, ByteCount{capture_len});
1831     }
1832 };
1833 
1834 using RegexMatchList = Vector<RegexMatch, MemoryDomain::Regions>;
1835 
insert_matches(const Buffer & buffer,RegexMatchList & matches,const Regex & regex,bool capture,LineRange range)1836 void insert_matches(const Buffer& buffer, RegexMatchList& matches, const Regex& regex, bool capture, LineRange range)
1837 {
1838     size_t pivot = matches.size();
1839     capture = capture and regex.mark_count() > 0;
1840     ThreadedRegexVM<const char*, RegexMode::Forward | RegexMode::Search> vm{*regex.impl()};
1841     for (auto line = range.begin; line < range.end; ++line)
1842     {
1843         const StringView l = buffer[line];
1844         const auto flags = RegexExecFlags::NotEndOfLine; // buffer line already ends with \n
1845         for (auto&& m : RegexIterator{l.begin(), l.end(), vm, flags})
1846         {
1847             const bool with_capture = capture and m[1].matched and
1848                                       m[0].second - m[0].first < std::numeric_limits<uint16_t>::max();
1849             matches.push_back({
1850                 line,
1851                 (int)(m[0].first - l.begin()),
1852                 (int)(m[0].second - l.begin()),
1853                 (uint16_t)(with_capture ? m[1].first - m[0].first : 0),
1854                 (uint16_t)(with_capture ? m[1].second - m[1].first : 0)
1855             });
1856         }
1857     }
1858 
1859     auto pos = std::lower_bound(matches.begin(), matches.begin() + pivot, range.begin,
1860                                 [](const RegexMatch& m, LineCount l) { return m.line < l; });
1861     kak_assert(pos == matches.begin() + pivot or pos->line >= range.end); // We should not have had matches for range
1862 
1863     // Move new matches into position.
1864     std::rotate(pos, matches.begin() + pivot, matches.end());
1865 }
1866 
update_matches(const Buffer & buffer,ConstArrayView<LineModification> modifs,RegexMatchList & matches)1867 void update_matches(const Buffer& buffer, ConstArrayView<LineModification> modifs,
1868                     RegexMatchList& matches)
1869 {
1870     // remove out of date matches and update line for others
1871     auto ins_pos = matches.begin();
1872     for (auto it = ins_pos; it != matches.end(); ++it)
1873     {
1874         auto modif_it = std::upper_bound(modifs.begin(), modifs.end(), it->line,
1875                                          [](const LineCount& l, const LineModification& c)
1876                                          { return l < c.old_line; });
1877 
1878         if (modif_it != modifs.begin())
1879         {
1880             auto& prev = *(modif_it-1);
1881             if (it->line < prev.old_line + prev.num_removed)
1882                 continue; // match removed
1883 
1884             it->line += prev.diff();
1885         }
1886 
1887         kak_assert(buffer.is_valid(it->begin_coord()) or
1888                    buffer[it->line].length() == it->begin);
1889         kak_assert(buffer.is_valid(it->end_coord()) or
1890                    buffer[it->line].length() == it->end);
1891 
1892         if (ins_pos != it)
1893             *ins_pos = std::move(*it);
1894         ++ins_pos;
1895     }
1896     matches.erase(ins_pos, matches.end());
1897 }
1898 
1899 struct RegionMatches : UseMemoryDomain<MemoryDomain::Highlight>
1900 {
1901     RegexMatchList begin_matches;
1902     RegexMatchList end_matches;
1903     RegexMatchList recurse_matches;
1904 
compare_to_beginKakoune::RegionMatches1905     static bool compare_to_begin(const RegexMatch& lhs, BufferCoord rhs)
1906     {
1907         return lhs.begin_coord() < rhs;
1908     }
1909 
find_next_beginKakoune::RegionMatches1910     RegexMatchList::const_iterator find_next_begin(BufferCoord pos) const
1911     {
1912         return std::lower_bound(begin_matches.begin(), begin_matches.end(),
1913                                 pos, compare_to_begin);
1914     }
1915 
find_matching_endKakoune::RegionMatches1916     RegexMatchList::const_iterator find_matching_end(const Buffer& buffer, BufferCoord beg_pos, Optional<StringView> capture) const
1917     {
1918         auto end_it = end_matches.begin();
1919         auto rec_it = recurse_matches.begin();
1920         int recurse_level = 0;
1921         while (true)
1922         {
1923             end_it = std::lower_bound(end_it, end_matches.end(), beg_pos,
1924                                       compare_to_begin);
1925             rec_it = std::lower_bound(rec_it, recurse_matches.end(), beg_pos,
1926                                       compare_to_begin);
1927 
1928             if (end_it == end_matches.end())
1929                 return end_it;
1930 
1931             while (rec_it != recurse_matches.end() and
1932                    rec_it->end_coord() <= end_it->end_coord())
1933             {
1934                 if (not capture or rec_it->capture(buffer) == *capture)
1935                     ++recurse_level;
1936                 ++rec_it;
1937             }
1938 
1939             if (not capture or *capture == end_it->capture(buffer))
1940             {
1941                 if (recurse_level == 0)
1942                     return end_it;
1943                 --recurse_level;
1944             }
1945 
1946             if (beg_pos != end_it->end_coord())
1947                 beg_pos = end_it->end_coord();
1948             ++end_it;
1949         }
1950     }
1951 };
1952 
1953 const HighlighterDesc default_region_desc = {
1954     "Parameters: <delegate_type> <delegate_params>...\n"
1955     "Define the default region of a regions highlighter",
1956     {}
1957 };
1958 const HighlighterDesc region_desc = {
1959     "Parameters:  [-match-capture] [-recurse <recurse>] <opening> <closing> <type> <params>...\n"
1960     "Define a region for a regions highlighter, and apply the given delegate\n"
1961     "highlighter as defined by <type> and eventual <params>...\n"
1962     "The region starts at <begin> match and ends at the first <end>",
1963     { {
1964         { "match-capture", { false, "only consider region ending/recurse delimiters whose first capture group match the region beginning delimiter" } },
1965         { "recurse",       { true, "make the region end on the first ending delimiter that does not close the given parameter" } } },
1966         ParameterDesc::Flags::SwitchesOnlyAtStart | ParameterDesc::Flags::IgnoreUnknownSwitches,
1967         3
1968     }
1969 };
1970 const HighlighterDesc regions_desc = {
1971     "Holds child region highlighters and segments the buffer in ranges based on those regions\n"
1972     "definitions. The regions highlighter finds the next region to start by finding which\n"
1973     "of its child region has the leftmost starting point from current position. In between\n"
1974     "regions, the default-region child highlighter is applied (if such a child exists)",
1975     {}
1976 };
1977 struct RegionsHighlighter : public Highlighter
1978 {
1979 public:
RegionsHighlighterKakoune::RegionsHighlighter1980     RegionsHighlighter()
1981         : Highlighter{HighlightPass::Colorize} {}
1982 
do_highlightKakoune::RegionsHighlighter1983     void do_highlight(HighlightContext context, DisplayBuffer& display_buffer, BufferRange range) override
1984     {
1985         if (m_regions.empty())
1986             return;
1987 
1988         auto display_range = display_buffer.range();
1989         const auto& buffer = context.context.buffer();
1990         auto& regions = get_regions_for_range(buffer, range);
1991 
1992         auto begin = std::lower_bound(regions.begin(), regions.end(), display_range.begin,
1993                                       [](const Region& r, BufferCoord c) { return r.end < c; });
1994         auto end = std::lower_bound(begin, regions.end(), display_range.end,
1995                                     [](const Region& r, BufferCoord c) { return r.begin < c; });
1996         auto correct = [&](BufferCoord c) -> BufferCoord {
1997             if (not buffer.is_end(c) and buffer[c.line].length() == c.column)
1998                 return {c.line+1, 0};
1999             return c;
2000         };
2001 
2002         auto default_region_it = m_regions.find(m_default_region);
2003         const bool apply_default = default_region_it != m_regions.end();
2004 
2005         auto last_begin = (begin == regions.begin()) ?
2006                              range.begin : (begin-1)->end;
2007         kak_assert(begin <= end);
2008         for (; begin != end; ++begin)
2009         {
2010             if (apply_default and last_begin < begin->begin)
2011                 apply_highlighter(context, display_buffer,
2012                                   correct(last_begin), correct(begin->begin),
2013                                   *default_region_it->value);
2014 
2015             auto it = m_regions.find(begin->region);
2016             if (it == m_regions.end())
2017                 continue;
2018             apply_highlighter(context, display_buffer,
2019                               correct(begin->begin), correct(begin->end),
2020                               *it->value);
2021             last_begin = begin->end;
2022         }
2023         if (apply_default and last_begin < display_range.end)
2024             apply_highlighter(context, display_buffer,
2025                               correct(last_begin), range.end,
2026                               *default_region_it->value);
2027     }
2028 
has_childrenKakoune::RegionsHighlighter2029     bool has_children() const override { return true; }
2030 
get_childKakoune::RegionsHighlighter2031     Highlighter& get_child(StringView path) override
2032     {
2033         auto sep_it = find(path, '/');
2034         StringView id(path.begin(), sep_it);
2035         auto it = m_regions.find(id);
2036         if (it == m_regions.end())
2037             throw child_not_found(format("no such id: {}", id));
2038         if (sep_it == path.end())
2039             return *it->value;
2040         else
2041             return it->value->get_child({sep_it+1, path.end()});
2042     }
2043 
add_childKakoune::RegionsHighlighter2044     void add_child(String name, std::unique_ptr<Highlighter>&& hl, bool override) override
2045     {
2046         if (not dynamic_cast<RegionHighlighter*>(hl.get()))
2047             throw runtime_error{"only region highlighter can be added as child of a regions highlighter"};
2048         auto it = m_regions.find(name);
2049         if (not override and it != m_regions.end())
2050             throw runtime_error{format("duplicate id: '{}'", name)};
2051 
2052         std::unique_ptr<RegionHighlighter> region_hl{dynamic_cast<RegionHighlighter*>(hl.release())};
2053         if (region_hl->is_default())
2054         {
2055             if (not m_default_region.empty())
2056                 throw runtime_error{"default region already defined"};
2057             m_default_region = name;
2058         }
2059 
2060         if (it != m_regions.end())
2061             it->value = std::move(region_hl);
2062         else
2063             m_regions.insert({std::move(name), std::move(region_hl)});
2064         ++m_regions_timestamp;
2065     }
2066 
remove_childKakoune::RegionsHighlighter2067     void remove_child(StringView id) override
2068     {
2069         if (id == m_default_region)
2070             m_default_region = String{};
2071 
2072         m_regions.remove(id);
2073         ++m_regions_timestamp;
2074     }
2075 
complete_childKakoune::RegionsHighlighter2076     Completions complete_child(StringView path, ByteCount cursor_pos, bool group) const override
2077     {
2078         auto sep_it = find(path, '/');
2079         if (sep_it != path.end())
2080         {
2081             ByteCount offset = sep_it+1 - path.begin();
2082             Highlighter& hl = const_cast<RegionsHighlighter*>(this)->get_child({path.begin(), sep_it});
2083             return offset_pos(hl.complete_child(path.substr(offset), cursor_pos - offset, group), offset);
2084         }
2085 
2086         auto container = m_regions | transform(&decltype(m_regions)::Item::key);
2087         return { 0, 0, complete(path, cursor_pos, container) };
2088     }
2089 
createKakoune::RegionsHighlighter2090     static std::unique_ptr<Highlighter> create(HighlighterParameters params, Highlighter*)
2091     {
2092         if (not params.empty())
2093             throw runtime_error{"unexpected parameters"};
2094         return std::make_unique<RegionsHighlighter>();
2095     }
2096 
is_regionsKakoune::RegionsHighlighter2097     static bool is_regions(Highlighter* parent)
2098     {
2099         if (dynamic_cast<RegionsHighlighter*>(parent))
2100             return true;
2101         if (auto* region = dynamic_cast<RegionHighlighter*>(parent))
2102             return is_regions(&region->delegate());
2103         return false;
2104     }
2105 
create_regionKakoune::RegionsHighlighter2106     static std::unique_ptr<Highlighter> create_region(HighlighterParameters params, Highlighter* parent)
2107     {
2108         if (not is_regions(parent))
2109             throw runtime_error{"region highlighter can only be added to a regions parent"};
2110 
2111         ParametersParser parser{params, region_desc.params};
2112 
2113         const bool match_capture = (bool)parser.get_switch("match-capture");
2114         if (parser[0].empty() or parser[1].empty())
2115             throw runtime_error("begin and end must not be empty");
2116 
2117         const RegexCompileFlags flags = match_capture ?
2118             RegexCompileFlags::Optimize : RegexCompileFlags::NoSubs | RegexCompileFlags::Optimize;
2119 
2120         const auto& type = parser[2];
2121         auto& registry = HighlighterRegistry::instance();
2122         auto it = registry.find(type);
2123         if (it == registry.end())
2124             throw runtime_error(format("no such highlighter type: '{}'", type));
2125 
2126         Regex recurse;
2127         if (auto recurse_switch = parser.get_switch("recurse"))
2128             recurse = Regex{*recurse_switch, flags};
2129 
2130         auto delegate = it->value.factory(parser.positionals_from(3), nullptr);
2131         return std::make_unique<RegionHighlighter>(std::move(delegate), Regex{parser[0], flags}, Regex{parser[1], flags}, recurse, match_capture);
2132     }
2133 
create_default_regionKakoune::RegionsHighlighter2134     static std::unique_ptr<Highlighter> create_default_region(HighlighterParameters params, Highlighter* parent)
2135     {
2136         if (not is_regions(parent))
2137             throw runtime_error{"default-region highlighter can only be added to a regions parent"};
2138 
2139         static const ParameterDesc param_desc{ {}, ParameterDesc::Flags::SwitchesOnlyAtStart, 1 };
2140         ParametersParser parser{params, param_desc};
2141 
2142         auto delegate = HighlighterRegistry::instance()[parser[0]].factory(parser.positionals_from(1), nullptr);
2143         return std::make_unique<RegionHighlighter>(std::move(delegate));
2144     }
2145 
2146 private:
2147     struct Region
2148     {
2149         BufferCoord begin;
2150         BufferCoord end;
2151         StringView region;
2152     };
2153     using RegionList = Vector<Region, MemoryDomain::Highlight>;
2154 
2155     struct Cache
2156     {
2157         size_t buffer_timestamp = 0;
2158         size_t regions_timestamp = 0;
2159         LineRangeSet ranges;
2160         std::unique_ptr<RegionMatches[]> matches;
2161         HashMap<BufferRange, RegionList, MemoryDomain::Highlight> regions;
2162     };
2163 
2164     using RegionAndMatch = std::pair<size_t, RegexMatchList::const_iterator>;
2165 
2166     // find the begin closest to pos in all matches
find_next_beginKakoune::RegionsHighlighter2167     RegionAndMatch find_next_begin(const Cache& cache, BufferCoord pos) const
2168     {
2169         RegionAndMatch res{0, cache.matches[0].find_next_begin(pos)};
2170         for (size_t i = 1; i < m_regions.size(); ++i)
2171         {
2172             const auto& matches = cache.matches[i];
2173             auto it = matches.find_next_begin(pos);
2174             if (it != matches.begin_matches.end() and
2175                 (res.second == cache.matches[res.first].begin_matches.end() or
2176                  it->begin_coord() < res.second->begin_coord()))
2177                 res = RegionAndMatch{i, it};
2178         }
2179         return res;
2180     }
2181 
update_matchesKakoune::RegionsHighlighter2182     bool update_matches(Cache& cache, const Buffer& buffer, LineRange range)
2183     {
2184         const size_t buffer_timestamp = buffer.timestamp();
2185         if (cache.buffer_timestamp == 0 or
2186             cache.regions_timestamp != m_regions_timestamp)
2187         {
2188             cache.matches.reset(new RegionMatches[m_regions.size()]);
2189             for (size_t i = 0; i < m_regions.size(); ++i)
2190             {
2191                 cache.matches[i] = RegionMatches{};
2192                 m_regions.item(i).value->add_matches(buffer, range, cache.matches[i]);
2193             }
2194             cache.ranges.reset(range);
2195             cache.buffer_timestamp = buffer_timestamp;
2196             cache.regions_timestamp = m_regions_timestamp;
2197             return true;
2198         }
2199         else
2200         {
2201             bool modified = false;
2202             if (cache.buffer_timestamp != buffer_timestamp)
2203             {
2204                 auto modifs = compute_line_modifications(buffer, cache.buffer_timestamp);
2205                 for (size_t i = 0; i < m_regions.size(); ++i)
2206                 {
2207                     Kakoune::update_matches(buffer, modifs, cache.matches[i].begin_matches);
2208                     Kakoune::update_matches(buffer, modifs, cache.matches[i].end_matches);
2209                     Kakoune::update_matches(buffer, modifs, cache.matches[i].recurse_matches);
2210                 }
2211 
2212                 cache.ranges.update(modifs);
2213                 cache.buffer_timestamp = buffer_timestamp;
2214                 modified = true;
2215             }
2216 
2217             cache.ranges.add_range(range, [&, this](const LineRange& range) {
2218                 if (range.begin == range.end)
2219                     return;
2220                 for (size_t i = 0; i < m_regions.size(); ++i)
2221                     m_regions.item(i).value->add_matches(buffer, range, cache.matches[i]);
2222                 modified = true;
2223             });
2224             return modified;
2225         }
2226     }
2227 
get_regions_for_rangeKakoune::RegionsHighlighter2228     const RegionList& get_regions_for_range(const Buffer& buffer, BufferRange range)
2229     {
2230         Cache& cache = m_cache.get(buffer);
2231         if (update_matches(cache, buffer, {range.begin.line, std::min(buffer.line_count(), range.end.line + 1)}))
2232             cache.regions.clear();
2233 
2234         auto it = cache.regions.find(range);
2235         if (it != cache.regions.end())
2236             return it->value;
2237 
2238         RegionList& regions = cache.regions[range];
2239 
2240         for (auto begin = find_next_begin(cache, range.begin),
2241                   end = RegionAndMatch{ 0, cache.matches[0].begin_matches.end() };
2242              begin != end; )
2243         {
2244             const RegionMatches& matches = cache.matches[begin.first];
2245             auto& region = m_regions.item(begin.first);
2246             auto beg_it = begin.second;
2247             auto end_it = matches.find_matching_end(buffer, beg_it->end_coord(),
2248                                                     region.value->match_capture() ? beg_it->capture(buffer) : Optional<StringView>{});
2249 
2250             if (end_it == matches.end_matches.end() or end_it->end_coord() >= range.end) // region continue past range end
2251             {
2252                 auto begin_coord = beg_it->begin_coord();
2253                 if (begin_coord < range.end)
2254                     regions.push_back({begin_coord, range.end, region.key});
2255                 break;
2256             }
2257 
2258             auto end_coord = end_it->end_coord();
2259             regions.push_back({beg_it->begin_coord(), end_coord, region.key});
2260 
2261             // With empty begin and end matches (for example if the regexes
2262             // are /"\K/ and /(?=")/), that case can happen, and would
2263             // result in an infinite loop.
2264             if (end_coord == beg_it->begin_coord())
2265             {
2266                 kak_assert(beg_it->empty() and end_it->empty());
2267                 ++end_coord.column;
2268             }
2269             begin = find_next_begin(cache, end_coord);
2270         }
2271         return regions;
2272     }
2273 
2274     struct RegionHighlighter : public Highlighter
2275     {
RegionHighlighterKakoune::RegionsHighlighter::RegionHighlighter2276         RegionHighlighter(std::unique_ptr<Highlighter>&& delegate,
2277                           Regex begin, Regex end, Regex recurse,
2278                           bool match_capture)
2279             : Highlighter{delegate->passes()},
2280               m_delegate{std::move(delegate)},
2281               m_begin{std::move(begin)}, m_end{std::move(end)}, m_recurse{std::move(recurse)},
2282               m_match_capture{match_capture}
2283        {
2284        }
2285 
RegionHighlighterKakoune::RegionsHighlighter::RegionHighlighter2286         RegionHighlighter(std::unique_ptr<Highlighter>&& delegate)
2287             : Highlighter{delegate->passes()}, m_delegate{std::move(delegate)}, m_default{true}
2288        {
2289        }
2290 
has_childrenKakoune::RegionsHighlighter::RegionHighlighter2291         bool has_children() const override
2292         {
2293             return m_delegate->has_children();
2294         }
2295 
get_childKakoune::RegionsHighlighter::RegionHighlighter2296         Highlighter& get_child(StringView path) override
2297         {
2298             return m_delegate->get_child(path);
2299         }
2300 
add_childKakoune::RegionsHighlighter::RegionHighlighter2301         void add_child(String name, std::unique_ptr<Highlighter>&& hl, bool override) override
2302         {
2303             return m_delegate->add_child(name, std::move(hl), override);
2304         }
2305 
remove_childKakoune::RegionsHighlighter::RegionHighlighter2306         void remove_child(StringView id) override
2307         {
2308             return m_delegate->remove_child(id);
2309         }
2310 
complete_childKakoune::RegionsHighlighter::RegionHighlighter2311         Completions complete_child(StringView path, ByteCount cursor_pos, bool group) const override
2312         {
2313             return m_delegate->complete_child(path, cursor_pos, group);
2314         }
2315 
fill_unique_idsKakoune::RegionsHighlighter::RegionHighlighter2316         void fill_unique_ids(Vector<StringView>& unique_ids) const override
2317         {
2318             return m_delegate->fill_unique_ids(unique_ids);
2319         }
2320 
do_highlightKakoune::RegionsHighlighter::RegionHighlighter2321         void do_highlight(HighlightContext context, DisplayBuffer& display_buffer, BufferRange range) override
2322         {
2323             return m_delegate->highlight(context, display_buffer, range);
2324         }
2325 
add_matchesKakoune::RegionsHighlighter::RegionHighlighter2326         void add_matches(const Buffer& buffer, LineRange range, RegionMatches& matches) const
2327         {
2328             if (m_default)
2329                 return;
2330 
2331             Kakoune::insert_matches(buffer, matches.begin_matches, m_begin, m_match_capture, range);
2332             Kakoune::insert_matches(buffer, matches.end_matches, m_end, m_match_capture, range);
2333             if (not m_recurse.empty())
2334                 Kakoune::insert_matches(buffer, matches.recurse_matches, m_recurse, m_match_capture, range);
2335         }
2336 
match_captureKakoune::RegionsHighlighter::RegionHighlighter2337         bool match_capture() const { return m_match_capture; }
is_defaultKakoune::RegionsHighlighter::RegionHighlighter2338         bool is_default() const { return m_default; }
2339 
delegateKakoune::RegionsHighlighter::RegionHighlighter2340         Highlighter& delegate() { return *m_delegate; }
2341 
2342     private:
2343         std::unique_ptr<Highlighter> m_delegate;
2344 
2345         Regex m_begin;
2346         Regex m_end;
2347         Regex m_recurse;
2348         bool  m_match_capture = false;
2349         bool  m_default = false;
2350     };
2351 
2352     HashMap<String, std::unique_ptr<RegionHighlighter>, MemoryDomain::Highlight> m_regions;
2353     String m_default_region;
2354 
2355     size_t m_regions_timestamp = 0;
2356     BufferSideCache<Cache> m_cache;
2357 };
2358 
setup_builtin_highlighters(HighlighterGroup & group)2359 void setup_builtin_highlighters(HighlighterGroup& group)
2360 {
2361     group.add_child("tabulations"_str, std::make_unique<TabulationHighlighter>());
2362     group.add_child("unprintable"_str, make_highlighter(expand_unprintable));
2363     group.add_child("selections"_str,  make_highlighter(highlight_selections));
2364 }
2365 
register_highlighters()2366 void register_highlighters()
2367 {
2368     HighlighterRegistry& registry = HighlighterRegistry::instance();
2369 
2370     registry.insert({
2371         "column",
2372         { create_column_highlighter, &column_desc } });
2373     registry.insert({
2374         "default-region",
2375         { RegionsHighlighter::create_default_region, &default_region_desc } });
2376     registry.insert({
2377         "dynregex",
2378         { create_dynamic_regex_highlighter, &dynamic_regex_desc } });
2379     registry.insert({
2380         "fill",
2381         { create_fill_highlighter, &fill_desc } });
2382     registry.insert({
2383         "flag-lines",
2384         { FlagLinesHighlighter::create, &flag_lines_desc } });
2385     registry.insert({
2386         "group",
2387         { create_highlighter_group, &higlighter_group_desc } });
2388     registry.insert({
2389         "line",
2390         { create_line_highlighter, &line_desc } });
2391     registry.insert({
2392         "number-lines",
2393         { LineNumbersHighlighter::create, &line_numbers_desc } });
2394     registry.insert({
2395         "ranges",
2396         { RangesHighlighter::create, &ranges_desc } });
2397     registry.insert({
2398         "ref",
2399         { ReferenceHighlighter::create, &ref_desc } });
2400     registry.insert({
2401         "regex",
2402         { RegexHighlighter::create, &regex_desc } });
2403     registry.insert({
2404         "region",
2405         { RegionsHighlighter::create_region, &region_desc } });
2406     registry.insert({
2407         "regions",
2408         { RegionsHighlighter::create, &regions_desc } });
2409     registry.insert({
2410         "replace-ranges",
2411         { ReplaceRangesHighlighter::create, &replace_ranges_desc } });
2412     registry.insert({
2413         "show-matching",
2414         { create_matching_char_highlighter, &show_matching_desc } });
2415     registry.insert({
2416         "show-whitespaces",
2417         { ShowWhitespacesHighlighter::create, &show_whitespace_desc } });
2418     registry.insert({
2419         "wrap",
2420         { WrapHighlighter::create, &wrap_desc } });
2421 }
2422 
2423 }
2424