1 #include "working_files.h"
2 
3 #include "lex_utils.h"
4 #include "position.h"
5 
6 #include <doctest/doctest.h>
7 #include <loguru.hpp>
8 
9 #include <algorithm>
10 #include <climits>
11 #include <numeric>
12 
13 namespace {
14 
15 // When finding a best match of buffer line and index line, limit the max edit
16 // distance.
17 constexpr int kMaxDiff = 20;
18 // Don't align index line to buffer line if one of the lengths is larger than
19 // |kMaxColumnAlignSize|.
20 constexpr int kMaxColumnAlignSize = 200;
21 
GetPositionForOffset(const std::string & content,int offset)22 lsPosition GetPositionForOffset(const std::string& content, int offset) {
23   if (offset >= content.size())
24     offset = (int)content.size() - 1;
25 
26   lsPosition result;
27   int i = 0;
28   while (i < offset) {
29     if (content[i] == '\n') {
30       result.line += 1;
31       result.character = 0;
32     } else {
33       result.character += 1;
34     }
35     ++i;
36   }
37 
38   return result;
39 }
40 
41 // Computes the edit distance of strings [a,a+la) and [b,b+lb) with Eugene W.
42 // Myers' O(ND) diff algorithm.
43 // Costs: insertion=1, deletion=1, no substitution.
44 // If the distance is larger than threshold, returns threshould + 1.
MyersDiff(const char * a,int la,const char * b,int lb,int threshold)45 int MyersDiff(const char* a, int la, const char* b, int lb, int threshold) {
46   assert(threshold <= kMaxDiff);
47   static int v_static[2 * kMaxColumnAlignSize + 2];
48   const char *ea = a + la, *eb = b + lb;
49   // Strip prefix
50   for (; a < ea && b < eb && *a == *b; a++, b++) {
51   }
52   // Strip suffix
53   for (; a < ea && b < eb && ea[-1] == eb[-1]; ea--, eb--) {
54   }
55   la = int(ea - a);
56   lb = int(eb - b);
57   // If the sum of lengths exceeds what we can handle, return a lower bound.
58   if (la + lb > 2 * kMaxColumnAlignSize)
59     return std::min(abs(la - lb), threshold + 1);
60 
61   int* v = v_static + lb;
62   v[1] = 0;
63   for (int di = 0; di <= threshold; di++) {
64     int low = -di + 2 * std::max(0, di - lb),
65         high = di - 2 * std::max(0, di - la);
66     for (int i = low; i <= high; i += 2) {
67       int x = i == -di || (i != di && v[i - 1] < v[i + 1]) ? v[i + 1]
68                                                            : v[i - 1] + 1,
69           y = x - i;
70       while (x < la && y < lb && a[x] == b[y])
71         x++, y++;
72       v[i] = x;
73       if (x == la && y == lb)
74         return di;
75     }
76   }
77   return threshold + 1;
78 }
79 
MyersDiff(const std::string & a,const std::string & b,int threshold)80 int MyersDiff(const std::string& a, const std::string& b, int threshold) {
81   return MyersDiff(a.data(), a.size(), b.data(), b.size(), threshold);
82 }
83 
84 // Computes edit distance with O(N*M) Needleman-Wunsch algorithm
85 // and returns a distance vector where d[i] = cost of aligning a to b[0,i).
86 //
87 // Myers' diff algorithm is used to find best matching line while this one is
88 // used to align a single column because Myers' needs some twiddling to return
89 // distance vector.
EditDistanceVector(std::string a,std::string b)90 std::vector<int> EditDistanceVector(std::string a, std::string b) {
91   std::vector<int> d(b.size() + 1);
92   std::iota(d.begin(), d.end(), 0);
93   for (int i = 0; i < (int)a.size(); i++) {
94     int ul = d[0];
95     d[0] = i + 1;
96     for (int j = 0; j < (int)b.size(); j++) {
97       int t = d[j + 1];
98       d[j + 1] = a[i] == b[j] ? ul : std::min(d[j], d[j + 1]) + 1;
99       ul = t;
100     }
101   }
102   return d;
103 }
104 
105 // Find matching position of |a[column]| in |b|.
106 // This is actually a single step of Hirschberg's sequence alignment algorithm.
AlignColumn(const std::string & a,int column,std::string b,bool is_end)107 int AlignColumn(const std::string& a, int column, std::string b, bool is_end) {
108   int head = 0, tail = 0;
109   while (head < (int)a.size() && head < (int)b.size() && a[head] == b[head])
110     head++;
111   while (tail < (int)a.size() && tail < (int)b.size() &&
112          a[a.size() - 1 - tail] == b[b.size() - 1 - tail])
113     tail++;
114   if (column < head)
115     return column;
116   if ((int)a.size() - tail < column)
117     return column + b.size() - a.size();
118   if (std::max(a.size(), b.size()) - head - tail >= kMaxColumnAlignSize)
119     return std::min(column, (int)b.size());
120 
121   // b[head, b.size() - tail)
122   b = b.substr(head, b.size() - tail - head);
123 
124   // left[i] = cost of aligning a[head, column) to b[head, head + i)
125   std::vector<int> left = EditDistanceVector(a.substr(head, column - head), b);
126 
127   // right[i] = cost of aligning a[column, a.size() - tail) to b[head + i,
128   // b.size() - tail)
129   std::string a_rev = a.substr(column, a.size() - tail - column);
130   std::reverse(a_rev.begin(), a_rev.end());
131   std::reverse(b.begin(), b.end());
132   std::vector<int> right = EditDistanceVector(a_rev, b);
133   std::reverse(right.begin(), right.end());
134 
135   int best = 0, best_cost = INT_MAX;
136   for (size_t i = 0; i < left.size(); i++) {
137     int cost = left[i] + right[i];
138     if (is_end ? cost < best_cost : cost <= best_cost) {
139       best_cost = cost;
140       best = i;
141     }
142   }
143   return head + best;
144 }
145 
146 // Find matching buffer line of index_lines[line].
147 // By symmetry, this can also be used to find matching index line of a buffer
148 // line.
FindMatchingLine(const std::vector<std::string> & index_lines,const std::vector<int> & index_to_buffer,int line,int * column,const std::vector<std::string> & buffer_lines,bool is_end)149 optional<int> FindMatchingLine(const std::vector<std::string>& index_lines,
150                                const std::vector<int>& index_to_buffer,
151                                int line,
152                                int* column,
153                                const std::vector<std::string>& buffer_lines,
154                                bool is_end) {
155   // If this is a confident mapping, returns.
156   if (index_to_buffer[line] >= 0) {
157     int ret = index_to_buffer[line];
158     if (column)
159       *column =
160           AlignColumn(index_lines[line], *column, buffer_lines[ret], is_end);
161     return ret;
162   }
163 
164   // Find the nearest two confident lines above and below.
165   int up = line, down = line;
166   while (--up >= 0 && index_to_buffer[up] < 0) {
167   }
168   while (++down < int(index_to_buffer.size()) && index_to_buffer[down] < 0) {
169   }
170   up = up < 0 ? 0 : index_to_buffer[up];
171   down = down >= int(index_to_buffer.size()) ? int(buffer_lines.size()) - 1
172                                              : index_to_buffer[down];
173   if (up > down)
174     return nullopt;
175 
176   // Search for lines [up,down] and use Myers's diff algorithm to find the best
177   // match (least edit distance).
178   int best = up, best_dist = kMaxDiff + 1;
179   const std::string& needle = index_lines[line];
180   for (int i = up; i <= down; i++) {
181     int dist = MyersDiff(needle, buffer_lines[i], kMaxDiff);
182     if (dist < best_dist) {
183       best_dist = dist;
184       best = i;
185     }
186   }
187   if (column)
188     *column =
189         AlignColumn(index_lines[line], *column, buffer_lines[best], is_end);
190   return best;
191 }
192 
193 }  // namespace
194 
AsUnsavedFiles() const195 std::vector<CXUnsavedFile> WorkingFiles::Snapshot::AsUnsavedFiles() const {
196   std::vector<CXUnsavedFile> result;
197   result.reserve(files.size());
198   for (auto& file : files) {
199     CXUnsavedFile unsaved;
200     unsaved.Filename = file.filename.c_str();
201     unsaved.Contents = file.content.c_str();
202     unsaved.Length = (unsigned long)file.content.size();
203 
204     result.push_back(unsaved);
205   }
206   return result;
207 }
208 
WorkingFile(const AbsolutePath & filename,const std::string & buffer_content)209 WorkingFile::WorkingFile(const AbsolutePath& filename,
210                          const std::string& buffer_content)
211     : filename(filename), buffer_content(buffer_content) {
212   OnBufferContentUpdated();
213 
214   // SetIndexContent gets called when the file is opened.
215 }
216 
SetIndexContent(const std::string & index_content)217 void WorkingFile::SetIndexContent(const std::string& index_content) {
218   index_lines = ToLines(index_content, false /*trim_whitespace*/);
219 
220   index_to_buffer.clear();
221   buffer_to_index.clear();
222 }
223 
OnBufferContentUpdated()224 void WorkingFile::OnBufferContentUpdated() {
225   buffer_lines = ToLines(buffer_content, false /*trim_whitespace*/);
226 
227   index_to_buffer.clear();
228   buffer_to_index.clear();
229 }
230 
231 // Variant of Paul Heckel's diff algorithm to compute |index_to_buffer| and
232 // |buffer_to_index|.
233 // The core idea is that if a line is unique in both index and buffer,
234 // we are confident that the line appeared in index maps to the one appeared in
235 // buffer. And then using them as start points to extend upwards and downwards
236 // to align other identical lines (but not unique).
ComputeLineMapping()237 void WorkingFile::ComputeLineMapping() {
238   std::unordered_map<uint64_t, int> hash_to_unique;
239   std::vector<uint64_t> index_hashes(index_lines.size());
240   std::vector<uint64_t> buffer_hashes(buffer_lines.size());
241   index_to_buffer.resize(index_lines.size());
242   buffer_to_index.resize(buffer_lines.size());
243   hash_to_unique.reserve(
244       std::max(index_to_buffer.size(), buffer_to_index.size()));
245 
246   // For index line i, set index_to_buffer[i] to -1 if line i is duplicated.
247   int i = 0;
248   for (auto& line : index_lines) {
249     std::string trimmed = Trim(line);
250     uint64_t h = HashUsr(trimmed);
251     auto it = hash_to_unique.find(h);
252     if (it == hash_to_unique.end()) {
253       hash_to_unique[h] = i;
254       index_to_buffer[i] = i;
255     } else {
256       if (it->second >= 0)
257         index_to_buffer[it->second] = -1;
258       index_to_buffer[i] = it->second = -1;
259     }
260     index_hashes[i++] = h;
261   }
262 
263   // For buffer line i, set buffer_to_index[i] to -1 if line i is duplicated.
264   i = 0;
265   hash_to_unique.clear();
266   for (auto& line : buffer_lines) {
267     std::string trimmed = Trim(line);
268     uint64_t h = HashUsr(trimmed);
269     auto it = hash_to_unique.find(h);
270     if (it == hash_to_unique.end()) {
271       hash_to_unique[h] = i;
272       buffer_to_index[i] = i;
273     } else {
274       if (it->second >= 0)
275         buffer_to_index[it->second] = -1;
276       buffer_to_index[i] = it->second = -1;
277     }
278     buffer_hashes[i++] = h;
279   }
280 
281   // If index line i is the identical to buffer line j, and they are both
282   // unique, align them by pointing from_index[i] to j.
283   i = 0;
284   for (auto h : index_hashes) {
285     if (index_to_buffer[i] >= 0) {
286       auto it = hash_to_unique.find(h);
287       if (it != hash_to_unique.end() && it->second >= 0 &&
288           buffer_to_index[it->second] >= 0)
289         index_to_buffer[i] = it->second;
290       else
291         index_to_buffer[i] = -1;
292     }
293     i++;
294   }
295 
296   // Starting at unique lines, extend upwards and downwards.
297   for (i = 0; i < (int)index_hashes.size() - 1; i++) {
298     int j = index_to_buffer[i];
299     if (0 <= j && j + 1 < buffer_hashes.size() &&
300         index_hashes[i + 1] == buffer_hashes[j + 1])
301       index_to_buffer[i + 1] = j + 1;
302   }
303   for (i = (int)index_hashes.size(); --i > 0;) {
304     int j = index_to_buffer[i];
305     if (0 < j && index_hashes[i - 1] == buffer_hashes[j - 1])
306       index_to_buffer[i - 1] = j - 1;
307   }
308 
309   // |buffer_to_index| is a inverse mapping of |index_to_buffer|.
310   std::fill(buffer_to_index.begin(), buffer_to_index.end(), -1);
311   for (i = 0; i < (int)index_hashes.size(); i++)
312     if (index_to_buffer[i] >= 0)
313       buffer_to_index[index_to_buffer[i]] = i;
314 }
315 
GetBufferPosFromIndexPos(int line,int * column,bool is_end)316 optional<int> WorkingFile::GetBufferPosFromIndexPos(int line,
317                                                     int* column,
318                                                     bool is_end) {
319   // The implementation is simple but works pretty well for most cases. We
320   // lookup the line contents in the indexed file contents, and try to find the
321   // most similar line in the current buffer file.
322   //
323   // Previously, this was implemented by tracking edits and by running myers
324   // diff algorithm. They were complex implementations that did not work as
325   // well.
326 
327   // Note: |index_line| and |buffer_line| are 1-based.
328 
329   // TODO: reenable this assert once we are using the real indexed file.
330   // assert(index_line >= 1 && index_line <= index_lines.size());
331   if (line < 0 || line >= (int)index_lines.size()) {
332     loguru::Text stack = loguru::stacktrace();
333     LOG_S(WARNING) << "Bad index_line (got " << line << ", expected [0, "
334                    << index_lines.size() << ")) in " << filename
335                    << stack.c_str();
336     return nullopt;
337   }
338 
339   if (index_to_buffer.empty())
340     ComputeLineMapping();
341   return FindMatchingLine(index_lines, index_to_buffer, line, column,
342                           buffer_lines, is_end);
343 }
344 
GetIndexPosFromBufferPos(int line,int * column,bool is_end)345 optional<int> WorkingFile::GetIndexPosFromBufferPos(int line,
346                                                     int* column,
347                                                     bool is_end) {
348   // See GetBufferLineFromIndexLine for additional comments.
349   if (line < 0 || line >= (int)buffer_lines.size())
350     return nullopt;
351 
352   if (buffer_to_index.empty())
353     ComputeLineMapping();
354   return FindMatchingLine(buffer_lines, buffer_to_index, line, column,
355                           index_lines, is_end);
356 }
357 
FindClosestCallNameInBuffer(lsPosition position,int * active_parameter,lsPosition * completion_position) const358 std::string WorkingFile::FindClosestCallNameInBuffer(
359     lsPosition position,
360     int* active_parameter,
361     lsPosition* completion_position) const {
362   *active_parameter = 0;
363 
364   int offset = GetOffsetForPosition(position, buffer_content);
365 
366   // If vscode auto-inserts closing ')' we will begin on ')' token in foo()
367   // which will make the below algorithm think it's a nested call.
368   if (offset > 0 && buffer_content[offset] == ')')
369     --offset;
370 
371   // Scan back out of call context.
372   int balance = 0;
373   while (offset > 0) {
374     char c = buffer_content[offset];
375     if (c == ')')
376       ++balance;
377     else if (c == '(')
378       --balance;
379 
380     if (balance == 0 && c == ',')
381       *active_parameter += 1;
382 
383     --offset;
384 
385     if (balance == -1)
386       break;
387   }
388 
389   if (offset < 0)
390     return "";
391 
392   // Scan back entire identifier.
393   int start_offset = offset;
394   while (offset > 0) {
395     char c = buffer_content[offset - 1];
396     if (isalnum(c) == false && c != '_')
397       break;
398     --offset;
399   }
400 
401   if (completion_position)
402     *completion_position = GetPositionForOffset(buffer_content, offset);
403 
404   return buffer_content.substr(offset, start_offset - offset + 1);
405 }
406 
FindStableCompletionSource(lsPosition position,bool * is_global_completion,std::string * existing_completion,lsPosition * replace_end_position) const407 lsPosition WorkingFile::FindStableCompletionSource(
408     lsPosition position,
409     bool* is_global_completion,
410     std::string* existing_completion,
411     lsPosition* replace_end_position) const {
412   *is_global_completion = true;
413 
414   int start_offset = GetOffsetForPosition(position, buffer_content);
415   int offset = start_offset;
416 
417   while (offset > 0) {
418     char c = buffer_content[offset - 1];
419     if (!isalnum(c) && c != '_') {
420       // Global completion is everything except for dot (.), arrow (->), and
421       // double colon (::)
422       if (c == '.')
423         *is_global_completion = false;
424       if (offset > 2) {
425         char pc = buffer_content[offset - 2];
426         if (pc == ':' && c == ':')
427           *is_global_completion = false;
428         else if (pc == '-' && c == '>')
429           *is_global_completion = false;
430       }
431 
432       break;
433     }
434     --offset;
435   }
436 
437   *replace_end_position = position;
438   int end_offset = start_offset;
439   while (end_offset < buffer_content.size()) {
440     char c = buffer_content[end_offset];
441     if (!isalnum(c) && c != '_')
442       break;
443     ++end_offset;
444     // We know that replace_end_position and position are on the same line.
445     ++replace_end_position->character;
446   }
447 
448   *existing_completion = buffer_content.substr(offset, start_offset - offset);
449   return GetPositionForOffset(buffer_content, offset);
450 }
451 
GetFileByFilename(const AbsolutePath & filename)452 WorkingFile* WorkingFiles::GetFileByFilename(const AbsolutePath& filename) {
453   std::lock_guard<std::mutex> lock(files_mutex);
454   return GetFileByFilenameNoLock(filename);
455 }
456 
GetFileByFilenameNoLock(const AbsolutePath & filename)457 WorkingFile* WorkingFiles::GetFileByFilenameNoLock(
458     const AbsolutePath& filename) {
459   for (auto& file : files) {
460     if (file->filename == filename)
461       return file.get();
462   }
463   return nullptr;
464 }
465 
DoAction(const std::function<void ()> & action)466 void WorkingFiles::DoAction(const std::function<void()>& action) {
467   std::lock_guard<std::mutex> lock(files_mutex);
468   action();
469 }
470 
DoActionOnFile(const AbsolutePath & filename,const std::function<void (WorkingFile * file)> & action)471 void WorkingFiles::DoActionOnFile(
472     const AbsolutePath& filename,
473     const std::function<void(WorkingFile* file)>& action) {
474   std::lock_guard<std::mutex> lock(files_mutex);
475   WorkingFile* file = GetFileByFilenameNoLock(filename);
476   action(file);
477 }
478 
OnOpen(const lsTextDocumentItem & open)479 WorkingFile* WorkingFiles::OnOpen(const lsTextDocumentItem& open) {
480   std::lock_guard<std::mutex> lock(files_mutex);
481 
482   AbsolutePath filename = open.uri.GetAbsolutePath();
483   std::string content = open.text;
484 
485   // The file may already be open.
486   if (WorkingFile* file = GetFileByFilenameNoLock(filename)) {
487     file->version = open.version;
488     file->buffer_content = content;
489     file->OnBufferContentUpdated();
490     return file;
491   }
492 
493   files.push_back(std::make_unique<WorkingFile>(filename, content));
494   return files[files.size() - 1].get();
495 }
496 
OnChange(const lsTextDocumentDidChangeParams & change)497 void WorkingFiles::OnChange(const lsTextDocumentDidChangeParams& change) {
498   std::lock_guard<std::mutex> lock(files_mutex);
499 
500   AbsolutePath filename = change.textDocument.uri.GetAbsolutePath();
501   WorkingFile* file = GetFileByFilenameNoLock(filename);
502   if (!file) {
503     LOG_S(WARNING) << "Could not change " << filename
504                    << " because it was not open";
505     return;
506   }
507 
508   if (change.textDocument.version)
509     file->version = *change.textDocument.version;
510 
511   for (const lsTextDocumentContentChangeEvent& diff : change.contentChanges) {
512     // Per the spec replace everything if the rangeLength and range are not set.
513     // See https://github.com/Microsoft/language-server-protocol/issues/9.
514     if (!diff.range) {
515       file->buffer_content = diff.text;
516       file->OnBufferContentUpdated();
517     } else {
518       int start_offset =
519           GetOffsetForPosition(diff.range->start, file->buffer_content);
520       // Ignore TextDocumentContentChangeEvent.rangeLength which causes trouble
521       // when UTF-16 surrogate pairs are used.
522       int end_offset =
523           GetOffsetForPosition(diff.range->end, file->buffer_content);
524       file->buffer_content.replace(file->buffer_content.begin() + start_offset,
525                                    file->buffer_content.begin() + end_offset,
526                                    diff.text);
527       file->OnBufferContentUpdated();
528     }
529   }
530 }
531 
OnClose(const lsTextDocumentIdentifier & close)532 void WorkingFiles::OnClose(const lsTextDocumentIdentifier& close) {
533   std::lock_guard<std::mutex> lock(files_mutex);
534 
535   AbsolutePath filename = close.uri.GetAbsolutePath();
536 
537   for (int i = 0; i < files.size(); ++i) {
538     if (files[i]->filename == filename) {
539       files.erase(files.begin() + i);
540       return;
541     }
542   }
543 
544   LOG_S(WARNING) << "Could not close " << filename
545                  << " because it was not open";
546 }
547 
AsSnapshot(const std::vector<std::string> & filter_paths)548 WorkingFiles::Snapshot WorkingFiles::AsSnapshot(
549     const std::vector<std::string>& filter_paths) {
550   std::lock_guard<std::mutex> lock(files_mutex);
551 
552   Snapshot result;
553   result.files.reserve(files.size());
554   for (const auto& file : files) {
555     if (filter_paths.empty() ||
556         FindAnyPartial(file->filename.path, filter_paths))
557       result.files.push_back({file->filename.path, file->buffer_content});
558   }
559   return result;
560 }
561 
CharPos(const WorkingFile & file,char character,int character_offset=0)562 lsPosition CharPos(const WorkingFile& file,
563                    char character,
564                    int character_offset = 0) {
565   return CharPos(file.buffer_content, character, character_offset);
566 }
567 
568 TEST_SUITE("WorkingFile") {
569   TEST_CASE("simple call") {
570     WorkingFile f(AbsolutePath::BuildDoNotUse("foo.cc"), "abcd(1, 2");
571     int active_param = 0;
572     REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, '('), &active_param) ==
573             "abcd");
574     REQUIRE(active_param == 0);
575     REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, '1'), &active_param) ==
576             "abcd");
577     REQUIRE(active_param == 0);
578     REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, ','), &active_param) ==
579             "abcd");
580     REQUIRE(active_param == 1);
581     REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, ' '), &active_param) ==
582             "abcd");
583     REQUIRE(active_param == 1);
584     REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, '2'), &active_param) ==
585             "abcd");
586     REQUIRE(active_param == 1);
587   }
588 
589   TEST_CASE("nested call") {
590     WorkingFile f(AbsolutePath::BuildDoNotUse("foo.cc"), "abcd(efg(), 2");
591     int active_param = 0;
592     REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, '('), &active_param) ==
593             "abcd");
594     REQUIRE(active_param == 0);
595     REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, 'e'), &active_param) ==
596             "abcd");
597     REQUIRE(active_param == 0);
598     REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, 'f'), &active_param) ==
599             "abcd");
600     REQUIRE(active_param == 0);
601     REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, 'g'), &active_param) ==
602             "abcd");
603     REQUIRE(active_param == 0);
604     REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, 'g', 1), &active_param) ==
605             "efg");
606     REQUIRE(active_param == 0);
607     REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, 'g', 2), &active_param) ==
608             "efg");
609     REQUIRE(active_param == 0);
610     REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, ','), &active_param) ==
611             "abcd");
612     REQUIRE(active_param == 1);
613     REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, ' '), &active_param) ==
614             "abcd");
615     REQUIRE(active_param == 1);
616   }
617 
618   TEST_CASE("auto-insert )") {
619     WorkingFile f(AbsolutePath::BuildDoNotUse("foo.cc"), "abc()");
620     int active_param = 0;
621     REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, ')'), &active_param) ==
622             "abc");
623     REQUIRE(active_param == 0);
624   }
625 
626   TEST_CASE("existing completion") {
627     WorkingFile f(AbsolutePath::BuildDoNotUse("foo.cc"), "zzz.asdf ");
628     bool is_global_completion;
629     std::string existing_completion;
630     lsPosition end_pos;
631 
632     f.FindStableCompletionSource(CharPos(f, '.'), &is_global_completion,
633                                  &existing_completion, &end_pos);
634     REQUIRE(existing_completion == "zzz");
635     REQUIRE(end_pos.line == CharPos(f, '.').line);
636     REQUIRE(end_pos.character == CharPos(f, '.').character);
637     f.FindStableCompletionSource(CharPos(f, 'a', 1), &is_global_completion,
638                                  &existing_completion, &end_pos);
639     REQUIRE(existing_completion == "a");
640     REQUIRE(end_pos.line == CharPos(f, ' ').line);
641     REQUIRE(end_pos.character == CharPos(f, ' ').character);
642     f.FindStableCompletionSource(CharPos(f, 's', 1), &is_global_completion,
643                                  &existing_completion, &end_pos);
644     REQUIRE(existing_completion == "as");
645     REQUIRE(end_pos.line == CharPos(f, ' ').line);
646     REQUIRE(end_pos.character == CharPos(f, ' ').character);
647     f.FindStableCompletionSource(CharPos(f, 'd', 1), &is_global_completion,
648                                  &existing_completion, &end_pos);
649     REQUIRE(existing_completion == "asd");
650     REQUIRE(end_pos.line == CharPos(f, ' ').line);
651     REQUIRE(end_pos.character == CharPos(f, ' ').character);
652     f.FindStableCompletionSource(CharPos(f, 'f', 1), &is_global_completion,
653                                  &existing_completion, &end_pos);
654     REQUIRE(existing_completion == "asdf");
655     REQUIRE(end_pos.line == CharPos(f, ' ').line);
656     REQUIRE(end_pos.character == CharPos(f, ' ').character);
657   }
658 
659   TEST_CASE("existing completion underscore") {
660     WorkingFile f(AbsolutePath::BuildDoNotUse("foo.cc"), "ABC_DEF ");
661     bool is_global_completion;
662     std::string existing_completion;
663     lsPosition end_pos;
664 
665     f.FindStableCompletionSource(CharPos(f, 'C'), &is_global_completion,
666                                  &existing_completion, &end_pos);
667     REQUIRE(existing_completion == "AB");
668     REQUIRE(end_pos.line == CharPos(f, ' ').line);
669     REQUIRE(end_pos.character == CharPos(f, ' ').character);
670     f.FindStableCompletionSource(CharPos(f, '_'), &is_global_completion,
671                                  &existing_completion, &end_pos);
672     REQUIRE(existing_completion == "ABC");
673     REQUIRE(end_pos.line == CharPos(f, ' ').line);
674     REQUIRE(end_pos.character == CharPos(f, ' ').character);
675     f.FindStableCompletionSource(CharPos(f, 'D'), &is_global_completion,
676                                  &existing_completion, &end_pos);
677     REQUIRE(existing_completion == "ABC_");
678     REQUIRE(end_pos.line == CharPos(f, ' ').line);
679     REQUIRE(end_pos.character == CharPos(f, ' ').character);
680   }
681 }
682