mirror of
https://github.com/MaskRay/ccls.git
synced 2024-11-22 07:35:08 +00:00
Much better algorithm to sync code lens index locations to the right buffer location
This commit is contained in:
parent
4dcffeb3f3
commit
04a855ebc7
28
foo/b.cc
28
foo/b.cc
@ -1,12 +1,24 @@
|
||||
namespace {
|
||||
//1
|
||||
//2
|
||||
|
||||
struct Foo2 {
|
||||
virtual void foobar() = 0;
|
||||
};
|
||||
|
||||
void x() {
|
||||
int bar = 3;
|
||||
|
||||
bar++;
|
||||
}
|
||||
|
||||
//3
|
||||
//4
|
||||
void b(); //7
|
||||
|
||||
|
||||
|
||||
void b(); //8
|
||||
//8
|
||||
//9
|
||||
//10
|
||||
//11
|
||||
//12
|
||||
|
||||
|
||||
//13
|
||||
void foo() {
|
||||
b();
|
||||
}
|
@ -290,13 +290,14 @@ optional<lsRange> GetLsRange(WorkingFile* working_file, const Range& location) {
|
||||
lsPosition(location.end.line - 1, location.end.column - 1));
|
||||
}
|
||||
|
||||
// TODO: Should we also consider location.end.line?
|
||||
if (working_file->IsDeletedDiskLine(location.start.line))
|
||||
optional<int> start = working_file->GetBufferLineFromDiskLine(location.start.line);
|
||||
optional<int> end = working_file->GetBufferLineFromDiskLine(location.end.line);
|
||||
if (!start || !end)
|
||||
return nullopt;
|
||||
|
||||
return lsRange(
|
||||
lsPosition(working_file->GetBufferLineFromDiskLine(location.start.line) - 1, location.start.column - 1),
|
||||
lsPosition(working_file->GetBufferLineFromDiskLine(location.end.line) - 1, location.end.column - 1));
|
||||
lsPosition(*start - 1, location.start.column - 1),
|
||||
lsPosition(*end - 1, location.end.column - 1));
|
||||
}
|
||||
|
||||
lsDocumentUri GetLsDocumentUri(QueryDatabase* db, QueryFileId file_id, std::string* path) {
|
||||
@ -337,7 +338,7 @@ lsSymbolInformation GetSymbolInfo(QueryDatabase* db, WorkingFiles* working_files
|
||||
}
|
||||
case SymbolKind::Func: {
|
||||
QueryFunc* func = symbol.ResolveFunc(db);
|
||||
|
||||
|
||||
info.name = func->def.detailed_name;
|
||||
if (func->def.declaring_type.has_value()) {
|
||||
info.kind = lsSymbolKind::Method;
|
||||
@ -422,13 +423,13 @@ lsWorkspaceEdit BuildWorkspaceEdit(QueryDatabase* db, WorkingFiles* working_file
|
||||
optional<lsLocation> ls_location = GetLsLocation(db, working_files, location);
|
||||
if (!ls_location)
|
||||
continue;
|
||||
|
||||
|
||||
if (path_to_edit.find(location.path) == path_to_edit.end()) {
|
||||
path_to_edit[location.path] = lsTextDocumentEdit();
|
||||
|
||||
const std::string& path = db->files[location.path.id].def.path;
|
||||
path_to_edit[location.path].textDocument.uri = lsDocumentUri::FromPath(path);
|
||||
|
||||
|
||||
WorkingFile* working_file = working_files->GetFileByFilename(path);
|
||||
if (working_file)
|
||||
path_to_edit[location.path].textDocument.version = working_file->version;
|
||||
@ -871,8 +872,11 @@ void QueryDbMainLoop(
|
||||
auto msg = static_cast<Ipc_TextDocumentDidSave*>(message.get());
|
||||
|
||||
WorkingFile* working_file = working_files->GetFileByFilename(msg->params.textDocument.uri.GetPath());
|
||||
if (working_file)
|
||||
working_file->changes.clear();
|
||||
if (working_file) {
|
||||
// TODO: Update working file indexed content when we actually get the
|
||||
// index update.
|
||||
working_file->SetIndexContent(working_file->buffer_content);
|
||||
}
|
||||
|
||||
// Send an index update request.
|
||||
Index_DoIndex request(Index_DoIndex::Type::Update);
|
||||
|
41
src/utils.cc
41
src/utils.cc
@ -1,12 +1,38 @@
|
||||
#include "utils.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cctype>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <locale>
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <tinydir.h>
|
||||
|
||||
namespace {
|
||||
|
||||
// See http://stackoverflow.com/a/217605
|
||||
// Trim from start (in place)
|
||||
void TrimStart(std::string& s) {
|
||||
s.erase(s.begin(), std::find_if(s.begin(), s.end(),
|
||||
std::not1(std::ptr_fun<int, int>(std::isspace))));
|
||||
}
|
||||
// Trim from end (in place)
|
||||
void TrimEnd(std::string& s) {
|
||||
s.erase(std::find_if(s.rbegin(), s.rend(),
|
||||
std::not1(std::ptr_fun<int, int>(std::isspace))).base(), s.end());
|
||||
}
|
||||
// Trim from both ends (in place)
|
||||
void Trim(std::string& s) {
|
||||
TrimStart(s);
|
||||
TrimEnd(s);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// See http://stackoverflow.com/a/2072890
|
||||
bool EndsWith(const std::string& value, const std::string& ending) {
|
||||
if (ending.size() > value.size())
|
||||
@ -133,6 +159,21 @@ std::vector<std::string> ReadLines(std::string filename) {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::string> ToLines(const std::string& content, bool trim_whitespace) {
|
||||
std::vector<std::string> result;
|
||||
|
||||
std::istringstream lines(content);
|
||||
|
||||
std::string line;
|
||||
while (getline(lines, line)) {
|
||||
if (trim_whitespace)
|
||||
Trim(line);
|
||||
result.push_back(line);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, std::string> ParseTestExpectation(std::string filename) {
|
||||
bool in_output = false;
|
||||
|
||||
|
@ -16,6 +16,9 @@ std::string ReplaceAll(const std::string& source, const std::string& from, const
|
||||
// Finds all files in the given folder. This is recursive.
|
||||
std::vector<std::string> GetFilesInFolder(std::string folder, bool recursive, bool add_folder_to_path);
|
||||
std::vector<std::string> ReadLines(std::string filename);
|
||||
std::vector<std::string> ToLines(const std::string& content, bool trim_whitespace);
|
||||
|
||||
|
||||
std::unordered_map<std::string, std::string> ParseTestExpectation(std::string filename);
|
||||
void UpdateTestExpectation(const std::string& filename, const std::string& expectation, const std::string& actual);
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
#include "working_files.h"
|
||||
|
||||
#include <doctest/doctest.h>
|
||||
#include "position.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@ -17,142 +17,83 @@ int GetOffsetForPosition(lsPosition position, const std::string& content) {
|
||||
return offset + position.character;
|
||||
}
|
||||
|
||||
int LineCount(std::string::const_iterator start, const std::string::const_iterator& end) {
|
||||
int count = 0;
|
||||
while (start != end) {
|
||||
if (*start == '\n')
|
||||
++count;
|
||||
++start;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
bool IsPreviousTokenNewline(const std::string& str, int start) {
|
||||
return start == 0 || str[start - 1] == '\n';
|
||||
}
|
||||
|
||||
int DetermineDiskLineForChange(WorkingFile* f, int start_offset, int buffer_line, int line_delta) {
|
||||
// If we're adding a newline that means we will introduce a new virtual newline
|
||||
// below. That's the case *except* if we are moving the current line down.
|
||||
if (!IsPreviousTokenNewline(f->content, start_offset)) {
|
||||
//std::cerr << " Applying newline workaround" << std::endl;
|
||||
++buffer_line;
|
||||
}
|
||||
|
||||
return f->GetDiskLineFromBufferLine(buffer_line);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
WorkingFile::Change::Change(int disk_line, int delta) : disk_line(disk_line), delta(delta) {}
|
||||
WorkingFile::WorkingFile(const std::string& filename, const std::string& buffer_content)
|
||||
: filename(filename), buffer_content(buffer_content) {
|
||||
OnBufferContentUpdated();
|
||||
// TODO: use cached index file contents
|
||||
SetIndexContent(buffer_content);
|
||||
}
|
||||
|
||||
WorkingFile::WorkingFile(const std::string& filename, const std::string& content)
|
||||
: filename(filename), content(content) {
|
||||
void WorkingFile::SetIndexContent(const std::string& index_content) {
|
||||
index_lines = ToLines(index_content, true /*trim_whitespace*/);
|
||||
}
|
||||
|
||||
void WorkingFile::OnBufferContentUpdated() {
|
||||
all_buffer_lines = ToLines(buffer_content, true /*trim_whitespace*/);
|
||||
|
||||
// Build lookup buffer.
|
||||
buffer_line_lookup.clear();
|
||||
buffer_line_lookup.reserve(all_buffer_lines.size());
|
||||
for (int i = 0; i < all_buffer_lines.size(); ++i) {
|
||||
const std::string& buffer_line = all_buffer_lines[i];
|
||||
|
||||
auto it = buffer_line_lookup.find(buffer_line);
|
||||
if (it == buffer_line_lookup.end())
|
||||
buffer_line_lookup[buffer_line] = { i + 1 };
|
||||
else
|
||||
it->second.push_back(i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
optional<int> WorkingFile::GetBufferLineFromDiskLine(int index_line) const {
|
||||
// The implementation is simple but works pretty well for most cases. We
|
||||
// lookup the line contents in the indexed file contents, and try to find the
|
||||
// most similar line in the current buffer file.
|
||||
//
|
||||
// Previously, this was implemented by tracking edits and by running myers
|
||||
// diff algorithm. They were complex implementations that did not work as
|
||||
// well.
|
||||
|
||||
// Note: |index_line| and |buffer_line| are 1-based.
|
||||
|
||||
assert(index_line >= 1 && index_line <= index_lines.size());
|
||||
|
||||
// Find the line in the cached index file. We'll try to find the most similar line
|
||||
// in the buffer and return the index for that.
|
||||
std::string index = index_lines[index_line - 1];
|
||||
auto buffer_it = buffer_line_lookup.find(index);
|
||||
if (buffer_it == buffer_line_lookup.end()) {
|
||||
// TODO: Use levenshtein distance to find the best match (but only to an
|
||||
// extent)
|
||||
return nullopt;
|
||||
}
|
||||
|
||||
// From all the identical lines, return the one which is closest to
|
||||
// |index_line|. There will usually only be one identical line.
|
||||
assert(!buffer_it->second.empty());
|
||||
int closest_dist = INT_MAX;
|
||||
int closest_buffer_line = INT_MIN;
|
||||
for (int buffer_line : buffer_it->second) {
|
||||
int dist = std::abs(buffer_line - index_line);
|
||||
if (dist <= closest_dist) {
|
||||
closest_dist = dist;
|
||||
closest_buffer_line = buffer_line;
|
||||
}
|
||||
}
|
||||
|
||||
return closest_buffer_line;
|
||||
}
|
||||
|
||||
CXUnsavedFile WorkingFile::AsUnsavedFile() const {
|
||||
CXUnsavedFile result;
|
||||
result.Filename = filename.c_str();
|
||||
result.Contents = content.c_str();
|
||||
result.Length = content.size();
|
||||
result.Contents = buffer_content.c_str();
|
||||
result.Length = (unsigned long)buffer_content.size();
|
||||
return result;
|
||||
}
|
||||
|
||||
int WorkingFile::GetDiskLineFromBufferLine(int buffer_line) const {
|
||||
int running_delta = 0;
|
||||
|
||||
// TODO: The disk line could come *after* the buffer line, ie, we insert into the start of the document.
|
||||
// change disk_line=1, delta=4 // insert 4 lines before line 1
|
||||
// buffer_line=5 maps to disk_line=1
|
||||
//
|
||||
|
||||
for (const auto& change : changes) {
|
||||
int buffer_start = running_delta + change.disk_line;
|
||||
int buffer_end = running_delta + change.disk_line + change.delta;
|
||||
|
||||
if (buffer_line < buffer_start)
|
||||
break;
|
||||
|
||||
if (buffer_line >= buffer_start && buffer_line < buffer_end) {
|
||||
//if (buffer_line == change.disk_line || change.disk_line == 1) return change.disk_line;
|
||||
return change.disk_line;
|
||||
}
|
||||
|
||||
running_delta += change.delta;
|
||||
}
|
||||
|
||||
return buffer_line - running_delta;
|
||||
}
|
||||
|
||||
int WorkingFile::GetBufferLineFromDiskLine(int disk_line) const {
|
||||
int buffer_line = disk_line;
|
||||
|
||||
for (const auto& change : changes) {
|
||||
if (disk_line >= change.disk_line) {
|
||||
assert(change.delta != 0);
|
||||
|
||||
if (change.delta > 0)
|
||||
buffer_line += change.delta;
|
||||
else {
|
||||
if (disk_line < (change.disk_line + (-change.delta)))
|
||||
buffer_line -= disk_line - change.disk_line + 1;
|
||||
else
|
||||
buffer_line += change.delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//std::cerr << "disk_line " << disk_line << " => buffer_line " << buffer_line << std::endl;// << " (line_deltas.size() = " << line_deltas.size() << ")" << std::endl;
|
||||
return buffer_line;
|
||||
}
|
||||
|
||||
bool WorkingFile::IsDeletedDiskLine(int disk_line) const {
|
||||
for (const auto& change : changes) {
|
||||
if (change.delta < 0 &&
|
||||
disk_line >= change.disk_line && disk_line < (change.disk_line + (-change.delta))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WorkingFile::IsBufferLineOnlyInBuffer(int buffer_line) const {
|
||||
//
|
||||
// 5: 2
|
||||
// 7: 3
|
||||
//
|
||||
// 1 -> 1
|
||||
// 2 -> 2
|
||||
// 3 -> 3
|
||||
// 4 -> 4
|
||||
// 5 -> 7
|
||||
// 6 -> 8
|
||||
// 7 -> 12
|
||||
// 8 -> 13
|
||||
//
|
||||
// lines 5,6 are virtual
|
||||
// lines 9,10,11 are virtual
|
||||
//
|
||||
//
|
||||
|
||||
int running_delta = 0;
|
||||
for (const auto& change : changes) {
|
||||
int buffer_start = change.disk_line + running_delta;
|
||||
int buffer_end = change.disk_line + running_delta + change.delta;
|
||||
|
||||
if (buffer_line < buffer_start)
|
||||
break;
|
||||
|
||||
if (buffer_line >= buffer_start && buffer_line < buffer_end)
|
||||
return true;
|
||||
|
||||
running_delta += change.delta;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
WorkingFile* WorkingFiles::GetFileByFilename(const std::string& filename) {
|
||||
for (auto& file : files) {
|
||||
if (file->filename == filename)
|
||||
@ -168,7 +109,10 @@ void WorkingFiles::OnOpen(const Ipc_TextDocumentDidOpen::Params& open) {
|
||||
// The file may already be open.
|
||||
if (WorkingFile* file = GetFileByFilename(filename)) {
|
||||
file->version = open.textDocument.version;
|
||||
file->content = content;
|
||||
// TODO: Load saved indexed content and not the initial buffer content.
|
||||
file->SetIndexContent(content);
|
||||
file->buffer_content = content;
|
||||
file->OnBufferContentUpdated();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -191,59 +135,15 @@ void WorkingFiles::OnChange(const Ipc_TextDocumentDidChange::Params& change) {
|
||||
|
||||
// If range or rangeLength are emitted we replace everything, per the spec.
|
||||
if (diff.rangeLength == -1) {
|
||||
file->content = diff.text;
|
||||
file->buffer_content = diff.text;
|
||||
file->OnBufferContentUpdated();
|
||||
}
|
||||
else {
|
||||
int start_offset = GetOffsetForPosition(diff.range.start, file->content);
|
||||
int old_line_count = diff.range.end.line - diff.range.start.line;
|
||||
int new_line_count = LineCount(diff.text.begin(), diff.text.end());
|
||||
|
||||
// TODO: this assert goes off. Figure out why.
|
||||
//assert(old_line_count == LineCount(file->content.begin() + start_offset, file->content.begin() + start_offset + diff.rangeLength + 1));
|
||||
|
||||
int line_delta = new_line_count - old_line_count;
|
||||
if (line_delta != 0) {
|
||||
//std::cerr << " diff.range.start.line=" << diff.range.start.line+1 << ", diff.range.start.character=" << diff.range.start.character << std::endl;
|
||||
//std::cerr << " diff.range.end.line=" << diff.range.end.line+1 << ", diff.range.end.character=" << diff.range.end.character << std::endl;
|
||||
//std::cerr << " old_line_count=" << old_line_count << ", new_line_count=" << new_line_count << std::endl;
|
||||
//std::cerr << " disk_line(diff.range.start.line)=" << file->GetDiskLineFromBufferLine(diff.range.start.line+1) << std::endl;
|
||||
//std::cerr << " disk_line(diff.range.end.line)=" << file->GetDiskLineFromBufferLine(diff.range.end.line+1) << std::endl;
|
||||
|
||||
// language server stores line counts starting at 0, we start at 1
|
||||
int buffer_line = diff.range.start.line + 1;
|
||||
int disk_line = DetermineDiskLineForChange(file, start_offset, buffer_line, line_delta);
|
||||
|
||||
bool found = false;
|
||||
for (int i = 0; i < file->changes.size(); ++i) {
|
||||
auto& change = file->changes[i];
|
||||
if (disk_line == change.disk_line ||
|
||||
(line_delta >= 1 && disk_line - 1 == change.disk_line && change.delta < 0) /* handles joining two lines and then resplitting them */ ) {
|
||||
change.delta += line_delta;
|
||||
found = true;
|
||||
}
|
||||
|
||||
if (change.delta == 0)
|
||||
file->changes.erase(file->changes.begin() + i);
|
||||
}
|
||||
if (!found)
|
||||
file->changes.push_back(WorkingFile::Change(disk_line, line_delta));
|
||||
|
||||
std::sort(file->changes.begin(), file->changes.end(),
|
||||
[](const WorkingFile::Change& a, const WorkingFile::Change& b) {
|
||||
return a.disk_line < b.disk_line;
|
||||
});
|
||||
|
||||
//std::cerr << " APPLIED" << std::endl;
|
||||
//std::cerr << " Inserted delta=" << line_delta << " at disk_line=" << disk_line << " with buffer_line=" << buffer_line << std::endl;
|
||||
//std::cerr << " changes.size()=" << file->changes.size() << std::endl;
|
||||
//for (auto& change : file->changes)
|
||||
// std::cerr << " disk_line=" << change.disk_line << " delta=" << change.delta << std::endl;
|
||||
}
|
||||
|
||||
|
||||
file->content.replace(file->content.begin() + start_offset,
|
||||
file->content.begin() + start_offset + diff.rangeLength,
|
||||
int start_offset = GetOffsetForPosition(diff.range.start, file->buffer_content);
|
||||
file->buffer_content.replace(file->buffer_content.begin() + start_offset,
|
||||
file->buffer_content.begin() + start_offset + diff.rangeLength,
|
||||
diff.text);
|
||||
file->OnBufferContentUpdated();
|
||||
}
|
||||
}
|
||||
//std::cerr << std::endl << std::endl << "--------" << file->content << "--------" << std::endl << std::endl;
|
||||
@ -268,358 +168,4 @@ std::vector<CXUnsavedFile> WorkingFiles::AsUnsavedFiles() const {
|
||||
for (auto& file : files)
|
||||
result.push_back(file->AsUnsavedFile());
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
TEST_SUITE("WorkingFiles");
|
||||
|
||||
TEST_CASE("buffer-to-disk no changes") {
|
||||
WorkingFile f("", "");
|
||||
|
||||
CHECK(f.GetDiskLineFromBufferLine(1) == 1);
|
||||
CHECK(f.GetDiskLineFromBufferLine(2) == 2);
|
||||
CHECK(f.GetDiskLineFromBufferLine(3) == 3);
|
||||
}
|
||||
|
||||
TEST_CASE("buffer-to-disk start add") {
|
||||
WorkingFile f("", "");
|
||||
|
||||
f.changes.push_back(WorkingFile::Change(1, 3));
|
||||
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(1) == true);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(2) == true);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(3) == true);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(4) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(5) == false);
|
||||
|
||||
CHECK(f.GetDiskLineFromBufferLine(1) == 1);
|
||||
CHECK(f.GetDiskLineFromBufferLine(2) == 1);
|
||||
CHECK(f.GetDiskLineFromBufferLine(3) == 1);
|
||||
CHECK(f.GetDiskLineFromBufferLine(4) == 1);
|
||||
CHECK(f.GetDiskLineFromBufferLine(5) == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("buffer-to-disk past-start add") {
|
||||
WorkingFile f("", "");
|
||||
|
||||
f.changes.push_back(WorkingFile::Change(2, 3));
|
||||
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(1) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(2) == true);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(3) == true);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(4) == true);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(5) == false);
|
||||
|
||||
CHECK(f.GetDiskLineFromBufferLine(1) == 1);
|
||||
CHECK(f.GetDiskLineFromBufferLine(2) == 2); // buffer-only
|
||||
CHECK(f.GetDiskLineFromBufferLine(3) == 2); // buffer-only
|
||||
CHECK(f.GetDiskLineFromBufferLine(4) == 2); // buffer-only
|
||||
CHECK(f.GetDiskLineFromBufferLine(5) == 2);
|
||||
CHECK(f.GetDiskLineFromBufferLine(6) == 3);
|
||||
}
|
||||
|
||||
TEST_CASE("buffer-to-disk start remove") {
|
||||
WorkingFile f("", "");
|
||||
|
||||
f.changes.push_back(WorkingFile::Change(1, -3));
|
||||
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(1) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(2) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(3) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(4) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(5) == false);
|
||||
|
||||
CHECK(f.GetDiskLineFromBufferLine(1) == 4);
|
||||
CHECK(f.GetDiskLineFromBufferLine(2) == 5);
|
||||
CHECK(f.GetDiskLineFromBufferLine(3) == 6);
|
||||
CHECK(f.GetDiskLineFromBufferLine(4) == 7);
|
||||
CHECK(f.GetDiskLineFromBufferLine(5) == 8);
|
||||
}
|
||||
|
||||
TEST_CASE("buffer-to-disk past-start remove") {
|
||||
WorkingFile f("", "");
|
||||
|
||||
f.changes.push_back(WorkingFile::Change(2, -3));
|
||||
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(1) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(2) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(3) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(4) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(5) == false);
|
||||
|
||||
CHECK(f.GetDiskLineFromBufferLine(1) == 1);
|
||||
CHECK(f.GetDiskLineFromBufferLine(2) == 5);
|
||||
CHECK(f.GetDiskLineFromBufferLine(3) == 6);
|
||||
CHECK(f.GetDiskLineFromBufferLine(4) == 7);
|
||||
CHECK(f.GetDiskLineFromBufferLine(5) == 8);
|
||||
}
|
||||
|
||||
TEST_CASE("buffer 1-1 mapping") {
|
||||
WorkingFile f("", "");
|
||||
|
||||
f.changes.push_back(WorkingFile::Change(2, 5));
|
||||
f.changes.push_back(WorkingFile::Change(3, -2));
|
||||
f.changes.push_back(WorkingFile::Change(4, 3));
|
||||
|
||||
for (int i = 1; i < 20; ++i) {
|
||||
// TODO: no good disk line from change starting at line 1
|
||||
|
||||
int disk1_line = f.GetDiskLineFromBufferLine(i);
|
||||
int buffer_line = f.GetBufferLineFromDiskLine(disk1_line);
|
||||
int disk2_line = f.GetDiskLineFromBufferLine(buffer_line);
|
||||
|
||||
CHECK(disk1_line == disk2_line);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("disk-to-buffer deleted disk lines") {
|
||||
WorkingFile f("", "");
|
||||
|
||||
f.changes.push_back(WorkingFile::Change(4, -3));
|
||||
|
||||
CHECK(f.GetBufferLineFromDiskLine(1) == 1);
|
||||
CHECK(f.GetBufferLineFromDiskLine(2) == 2);
|
||||
CHECK(f.GetBufferLineFromDiskLine(3) == 3);
|
||||
CHECK(f.GetBufferLineFromDiskLine(4) == 3);
|
||||
CHECK(f.GetBufferLineFromDiskLine(5) == 3);
|
||||
CHECK(f.GetBufferLineFromDiskLine(6) == 3);
|
||||
CHECK(f.GetBufferLineFromDiskLine(7) == 4);
|
||||
}
|
||||
|
||||
|
||||
TEST_CASE("disk-to-buffer mapping add remove") {
|
||||
WorkingFile f("", "");
|
||||
|
||||
f.changes.push_back(WorkingFile::Change(2, 5));
|
||||
f.changes.push_back(WorkingFile::Change(10, -2));
|
||||
|
||||
CHECK(f.GetBufferLineFromDiskLine(1) == 1);
|
||||
CHECK(f.GetBufferLineFromDiskLine(2) == 7);
|
||||
CHECK(f.GetBufferLineFromDiskLine(3) == 8);
|
||||
|
||||
CHECK(f.GetBufferLineFromDiskLine(8) == 13);
|
||||
CHECK(f.GetBufferLineFromDiskLine(9) == 14);
|
||||
CHECK(f.GetBufferLineFromDiskLine(10) == 14);
|
||||
CHECK(f.GetBufferLineFromDiskLine(11) == 14);
|
||||
CHECK(f.GetBufferLineFromDiskLine(12) == 15);
|
||||
}
|
||||
|
||||
TEST_CASE("disk-to-buffer mapping add") {
|
||||
WorkingFile f("", "");
|
||||
|
||||
f.changes.push_back(WorkingFile::Change(2, 5));
|
||||
|
||||
CHECK(f.GetDiskLineFromBufferLine(1) == 1);
|
||||
CHECK(f.GetDiskLineFromBufferLine(2) == 2);
|
||||
CHECK(f.GetDiskLineFromBufferLine(3) == 2);
|
||||
CHECK(f.GetDiskLineFromBufferLine(4) == 2);
|
||||
CHECK(f.GetDiskLineFromBufferLine(5) == 2);
|
||||
CHECK(f.GetDiskLineFromBufferLine(6) == 2);
|
||||
CHECK(f.GetDiskLineFromBufferLine(7) == 2);
|
||||
CHECK(f.GetDiskLineFromBufferLine(8) == 3);
|
||||
}
|
||||
|
||||
TEST_CASE("disk-to-buffer remove lines") {
|
||||
WorkingFile f("", "");
|
||||
|
||||
f.changes.push_back(WorkingFile::Change(2, -5));
|
||||
|
||||
CHECK(f.GetDiskLineFromBufferLine(1) == 1);
|
||||
CHECK(f.GetDiskLineFromBufferLine(2) == 7);
|
||||
CHECK(f.GetDiskLineFromBufferLine(3) == 8);
|
||||
CHECK(f.GetDiskLineFromBufferLine(4) == 9);
|
||||
}
|
||||
|
||||
|
||||
TEST_CASE("disk-to-buffer overlapping") {
|
||||
WorkingFile f("", "");
|
||||
|
||||
f.changes.push_back(WorkingFile::Change(1, 5));
|
||||
f.changes.push_back(WorkingFile::Change(2, -2));
|
||||
f.changes.push_back(WorkingFile::Change(3, 3));
|
||||
|
||||
CHECK(f.GetBufferLineFromDiskLine(1) == 6);
|
||||
CHECK(f.GetBufferLineFromDiskLine(2) == 6);
|
||||
CHECK(f.GetBufferLineFromDiskLine(3) == 9);
|
||||
CHECK(f.GetBufferLineFromDiskLine(4) == 10);
|
||||
CHECK(f.GetBufferLineFromDiskLine(5) == 11);
|
||||
CHECK(f.GetBufferLineFromDiskLine(6) == 12);
|
||||
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(1) == true);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(2) == true);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(3) == true);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(4) == true);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(5) == true);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(6) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(7) == true);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(8) == true);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(9) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(10) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(11) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(12) == false);
|
||||
}
|
||||
|
||||
TEST_CASE("is buffer line removing lines") {
|
||||
WorkingFile f("", "");
|
||||
|
||||
f.changes.push_back(WorkingFile::Change(2, -2));
|
||||
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(1) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(2) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(3) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(4) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(5) == false);
|
||||
}
|
||||
|
||||
TEST_CASE("is buffer line adding lines") {
|
||||
|
||||
//
|
||||
// 5: 2
|
||||
// 7: 3
|
||||
//
|
||||
// 1 -> 1
|
||||
// 2 -> 2
|
||||
// 3 -> 3
|
||||
// 4 -> 4
|
||||
// 5 -> 7
|
||||
// 6 -> 8
|
||||
// 7 -> 12
|
||||
// 8 -> 13
|
||||
//
|
||||
// lines 5,6 are virtual
|
||||
// lines 9,10,11 are virtual
|
||||
//
|
||||
|
||||
WorkingFile f("", "");
|
||||
|
||||
f.changes.push_back(WorkingFile::Change(5, 2));
|
||||
f.changes.push_back(WorkingFile::Change(7, 3));
|
||||
|
||||
CHECK(f.GetBufferLineFromDiskLine(1) == 1);
|
||||
CHECK(f.GetBufferLineFromDiskLine(2) == 2);
|
||||
CHECK(f.GetBufferLineFromDiskLine(3) == 3);
|
||||
CHECK(f.GetBufferLineFromDiskLine(4) == 4);
|
||||
CHECK(f.GetBufferLineFromDiskLine(5) == 7);
|
||||
CHECK(f.GetBufferLineFromDiskLine(6) == 8);
|
||||
CHECK(f.GetBufferLineFromDiskLine(7) == 12);
|
||||
CHECK(f.GetBufferLineFromDiskLine(8) == 13);
|
||||
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(1) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(2) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(3) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(4) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(5) == true);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(6) == true);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(7) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(8) == false);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(9) == true);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(10) == true);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(11) == true);
|
||||
CHECK(f.IsBufferLineOnlyInBuffer(12) == false);
|
||||
}
|
||||
|
||||
TEST_CASE("sanity") {
|
||||
/*
|
||||
(changes.size()=1) Inserted delta=1 at disk_line=4 with buffer_line=4
|
||||
disk_line=4 delta=1
|
||||
(changes.size()=1) Inserted delta=1 at disk_line=4 with buffer_line=5
|
||||
disk_line=4 delta=2
|
||||
(changes.size()=2) Inserted delta=1 at disk_line=3 with buffer_line=5
|
||||
disk_line=4 delta=2
|
||||
disk_line=3 delta=1
|
||||
*/
|
||||
|
||||
WorkingFile f("", "");
|
||||
|
||||
f.changes.push_back(WorkingFile::Change(4, 2));
|
||||
|
||||
CHECK(f.GetDiskLineFromBufferLine(5) == 4);
|
||||
}
|
||||
|
||||
|
||||
TEST_CASE("deleted_line start") {
|
||||
WorkingFile f("", "");
|
||||
|
||||
f.changes.push_back(WorkingFile::Change(1, -3));
|
||||
|
||||
CHECK(f.IsDeletedDiskLine(1) == true);
|
||||
CHECK(f.IsDeletedDiskLine(2) == true);
|
||||
CHECK(f.IsDeletedDiskLine(3) == true);
|
||||
CHECK(f.IsDeletedDiskLine(4) == false);
|
||||
}
|
||||
|
||||
TEST_CASE("deleted_line past-start") {
|
||||
WorkingFile f("", "");
|
||||
|
||||
f.changes.push_back(WorkingFile::Change(2, -3));
|
||||
|
||||
CHECK(f.IsDeletedDiskLine(1) == false);
|
||||
CHECK(f.IsDeletedDiskLine(2) == true);
|
||||
CHECK(f.IsDeletedDiskLine(3) == true);
|
||||
CHECK(f.IsDeletedDiskLine(4) == true);
|
||||
CHECK(f.IsDeletedDiskLine(5) == false);
|
||||
}
|
||||
|
||||
TEST_CASE("wip") {
|
||||
/*
|
||||
VERSION 4
|
||||
diff.range.start.line=2, diff.range.start.character=0
|
||||
diff.range.end.line= 3, diff.range.end.character= 0
|
||||
diff.text=""
|
||||
Applying newline workaround
|
||||
APPLIED
|
||||
Inserted delta=-1 at disk_line=4 with buffer_line=4
|
||||
changes.size()=1
|
||||
disk_line=4 delta=-1
|
||||
|
||||
|
||||
VERSION 7
|
||||
diff.range.start.line=2, diff.range.start.character=0
|
||||
diff.range.end.line= 2, diff.range.end.character= 0
|
||||
diff.text="daaa\n"
|
||||
Applying newline workaround
|
||||
APPLIED
|
||||
Inserted delta=1 at disk_line=5 with buffer_line=4
|
||||
changes.size()=2
|
||||
disk_line=4 delta=-1
|
||||
disk_line=5 delta=1
|
||||
*/
|
||||
|
||||
WorkingFile f("", "");
|
||||
|
||||
/*
|
||||
delete line 4
|
||||
buffer line 4 -> disk line 5
|
||||
*/
|
||||
|
||||
f.changes.push_back(WorkingFile::Change(4, -1));
|
||||
|
||||
CHECK(f.GetDiskLineFromBufferLine(4) == 5);
|
||||
}
|
||||
|
||||
TEST_CASE("DetermineDiskLineForChange") {
|
||||
// aaa
|
||||
// bbb
|
||||
WorkingFile f("", "aaa\nbbb");
|
||||
|
||||
// Adding a line so we have
|
||||
// aaa
|
||||
//
|
||||
// bbb
|
||||
CHECK(DetermineDiskLineForChange(&f, 3 /*start_offset*/, 1 /*buffer_line*/, 1 /*line_delta*/) == 2);
|
||||
|
||||
// Adding a line so we have
|
||||
//
|
||||
// aaa
|
||||
// bbb
|
||||
CHECK(DetermineDiskLineForChange(&f, 0 /*start_offset*/, 1 /*buffer_line*/, 1 /*line_delta*/) == 1);
|
||||
|
||||
// Adding a line so we have
|
||||
// a
|
||||
// aa
|
||||
// bbb
|
||||
CHECK(DetermineDiskLineForChange(&f, 1 /*start_offset*/, 1 /*buffer_line*/, 1 /*line_delta*/) == 2);
|
||||
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
@ -4,67 +4,37 @@
|
||||
#include "utils.h"
|
||||
|
||||
#include <clang-c/Index.h>
|
||||
#include <optional.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
using std::experimental::optional;
|
||||
using std::experimental::nullopt;
|
||||
|
||||
struct WorkingFile {
|
||||
struct Change {
|
||||
//
|
||||
// A change at disk_line=2, delta=3 means that we are prepending 3 lines
|
||||
// before disk_line=2. This means disk_line=2 is shifted to buffer_line=5.
|
||||
//
|
||||
// buffer_line=1 disk_line=1 type=disk
|
||||
// buffer_line=2 disk_line=2 type=virtual
|
||||
// buffer_line=3 disk_line=2 type=virtual
|
||||
// buffer_line=4 disk_line=2 type=virtual
|
||||
// buffer_line=5 disk_line=2 type=disk
|
||||
// buffer_line=6 disk_line=3 type=disk
|
||||
//
|
||||
//
|
||||
// A change at disk_line=2, delta=-3 means that we are removing 3 lines
|
||||
// starting at disk_line=2. This means disk_line=2,3,4 are removed.
|
||||
//
|
||||
// buffer_line=1 disk_line=1 type=disk
|
||||
// buffer_line=2 disk_line=5 type=disk
|
||||
// buffer_line=3 disk_line=6 type=disk
|
||||
//
|
||||
// There is no meaningful way to map disk_line=2,3,4 to buffer_line; at the
|
||||
// moment they are shifted to buffer_line=2. Ideally calling code checks
|
||||
// IsDeletedLine and does not emit the reference if that's the case.
|
||||
//
|
||||
|
||||
int disk_line;
|
||||
int delta;
|
||||
|
||||
Change(int disk_line, int delta);
|
||||
};
|
||||
|
||||
int version = 0;
|
||||
std::string filename;
|
||||
std::string content;
|
||||
std::vector<Change> changes;
|
||||
|
||||
WorkingFile(const std::string& filename, const std::string& content);
|
||||
std::string buffer_content;
|
||||
// Note: This assumes 0-based lines (1-based lines are normally assumed).
|
||||
std::vector<std::string> index_lines;
|
||||
// Note: This assumes 0-based lines (1-based lines are normally assumed).
|
||||
std::vector<std::string> all_buffer_lines;
|
||||
// Note: The items in the value entry are 1-based lines.s
|
||||
std::unordered_map<std::string, std::vector<int>> buffer_line_lookup;
|
||||
|
||||
WorkingFile(const std::string& filename, const std::string& buffer_content);
|
||||
|
||||
// This should be called when the indexed content has changed.
|
||||
void SetIndexContent(const std::string& index_content);
|
||||
// This should be called whenever |buffer_content| has changed.
|
||||
void OnBufferContentUpdated();
|
||||
|
||||
// Find the buffer-line which should be shown for |indexed_line|. This
|
||||
// accepts and returns 1-based lines.
|
||||
optional<int> GetBufferLineFromDiskLine(int indexed_line) const;
|
||||
|
||||
CXUnsavedFile AsUnsavedFile() const;
|
||||
|
||||
// TODO: Add IsDeletedLine(int disk_line) and avoid showing code lens info for that line.
|
||||
|
||||
// Returns the disk line prior to buffer_line.
|
||||
//
|
||||
// - If there is a change at the start of the document and a buffer_line in that change
|
||||
// is requested, 1 is returned.
|
||||
// - If buffer_line is contained within a change (ie, IsBufferLineOnlyInBuffer is true),
|
||||
// then the line immediately preceding that change is returned.
|
||||
//
|
||||
// Otherwise, the actual disk line is returned.
|
||||
int GetDiskLineFromBufferLine(int buffer_line) const;
|
||||
|
||||
int GetBufferLineFromDiskLine(int disk_line) const;
|
||||
|
||||
bool IsDeletedDiskLine(int disk_line) const;
|
||||
|
||||
bool IsBufferLineOnlyInBuffer(int buffer_line) const;
|
||||
};
|
||||
|
||||
struct WorkingFiles {
|
||||
|
Loading…
Reference in New Issue
Block a user