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(®ion->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, ®ex_desc } });
2403 registry.insert({
2404 "region",
2405 { RegionsHighlighter::create_region, ®ion_desc } });
2406 registry.insert({
2407 "regions",
2408 { RegionsHighlighter::create, ®ions_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