Much better algorithm to sync code lens index locations to the right buffer location

This commit is contained in:
Jacob Dufault 2017-04-16 01:09:12 -07:00
parent 4dcffeb3f3
commit 04a855ebc7
6 changed files with 177 additions and 601 deletions

View File

@ -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();
} }

View File

@ -290,13 +290,14 @@ optional<lsRange> GetLsRange(WorkingFile* working_file, const Range& location) {
lsPosition(location.end.line - 1, location.end.column - 1)); lsPosition(location.end.line - 1, location.end.column - 1));
} }
// TODO: Should we also consider location.end.line? optional<int> start = working_file->GetBufferLineFromDiskLine(location.start.line);
if (working_file->IsDeletedDiskLine(location.start.line)) optional<int> end = working_file->GetBufferLineFromDiskLine(location.end.line);
if (!start || !end)
return nullopt; return nullopt;
return lsRange( return lsRange(
lsPosition(working_file->GetBufferLineFromDiskLine(location.start.line) - 1, location.start.column - 1), lsPosition(*start - 1, location.start.column - 1),
lsPosition(working_file->GetBufferLineFromDiskLine(location.end.line) - 1, location.end.column - 1)); lsPosition(*end - 1, location.end.column - 1));
} }
lsDocumentUri GetLsDocumentUri(QueryDatabase* db, QueryFileId file_id, std::string* path) { lsDocumentUri GetLsDocumentUri(QueryDatabase* db, QueryFileId file_id, std::string* path) {
@ -871,8 +872,11 @@ void QueryDbMainLoop(
auto msg = static_cast<Ipc_TextDocumentDidSave*>(message.get()); auto msg = static_cast<Ipc_TextDocumentDidSave*>(message.get());
WorkingFile* working_file = working_files->GetFileByFilename(msg->params.textDocument.uri.GetPath()); WorkingFile* working_file = working_files->GetFileByFilename(msg->params.textDocument.uri.GetPath());
if (working_file) if (working_file) {
working_file->changes.clear(); // 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. // Send an index update request.
Index_DoIndex request(Index_DoIndex::Type::Update); Index_DoIndex request(Index_DoIndex::Type::Update);

View File

@ -1,12 +1,38 @@
#include "utils.h" #include "utils.h"
#include <algorithm>
#include <cassert> #include <cassert>
#include <cctype>
#include <functional>
#include <iostream> #include <iostream>
#include <fstream> #include <fstream>
#include <locale>
#include <sstream>
#include <unordered_map> #include <unordered_map>
#include <tinydir.h> #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 // See http://stackoverflow.com/a/2072890
bool EndsWith(const std::string& value, const std::string& ending) { bool EndsWith(const std::string& value, const std::string& ending) {
if (ending.size() > value.size()) if (ending.size() > value.size())
@ -133,6 +159,21 @@ std::vector<std::string> ReadLines(std::string filename) {
return result; 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) { std::unordered_map<std::string, std::string> ParseTestExpectation(std::string filename) {
bool in_output = false; bool in_output = false;

View File

@ -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. // 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> GetFilesInFolder(std::string folder, bool recursive, bool add_folder_to_path);
std::vector<std::string> ReadLines(std::string filename); 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); 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); void UpdateTestExpectation(const std::string& filename, const std::string& expectation, const std::string& actual);

View File

@ -1,6 +1,6 @@
#include "working_files.h" #include "working_files.h"
#include <doctest/doctest.h> #include "position.h"
namespace { namespace {
@ -17,142 +17,83 @@ int GetOffsetForPosition(lsPosition position, const std::string& content) {
return offset + position.character; 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 } // 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) void WorkingFile::SetIndexContent(const std::string& index_content) {
: filename(filename), content(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 WorkingFile::AsUnsavedFile() const {
CXUnsavedFile result; CXUnsavedFile result;
result.Filename = filename.c_str(); result.Filename = filename.c_str();
result.Contents = content.c_str(); result.Contents = buffer_content.c_str();
result.Length = content.size(); result.Length = (unsigned long)buffer_content.size();
return result; 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) { WorkingFile* WorkingFiles::GetFileByFilename(const std::string& filename) {
for (auto& file : files) { for (auto& file : files) {
if (file->filename == filename) if (file->filename == filename)
@ -168,7 +109,10 @@ void WorkingFiles::OnOpen(const Ipc_TextDocumentDidOpen::Params& open) {
// The file may already be open. // The file may already be open.
if (WorkingFile* file = GetFileByFilename(filename)) { if (WorkingFile* file = GetFileByFilename(filename)) {
file->version = open.textDocument.version; 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; 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 range or rangeLength are emitted we replace everything, per the spec.
if (diff.rangeLength == -1) { if (diff.rangeLength == -1) {
file->content = diff.text; file->buffer_content = diff.text;
file->OnBufferContentUpdated();
} }
else { else {
int start_offset = GetOffsetForPosition(diff.range.start, file->content); int start_offset = GetOffsetForPosition(diff.range.start, file->buffer_content);
int old_line_count = diff.range.end.line - diff.range.start.line; file->buffer_content.replace(file->buffer_content.begin() + start_offset,
int new_line_count = LineCount(diff.text.begin(), diff.text.end()); file->buffer_content.begin() + start_offset + diff.rangeLength,
// 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,
diff.text); diff.text);
file->OnBufferContentUpdated();
} }
} }
//std::cerr << std::endl << std::endl << "--------" << file->content << "--------" << std::endl << std::endl; //std::cerr << std::endl << std::endl << "--------" << file->content << "--------" << std::endl << std::endl;
@ -269,357 +169,3 @@ std::vector<CXUnsavedFile> WorkingFiles::AsUnsavedFiles() const {
result.push_back(file->AsUnsavedFile()); result.push_back(file->AsUnsavedFile());
return result; 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();

View File

@ -4,67 +4,37 @@
#include "utils.h" #include "utils.h"
#include <clang-c/Index.h> #include <clang-c/Index.h>
#include <optional.h>
#include <string> #include <string>
using std::experimental::optional;
using std::experimental::nullopt;
struct WorkingFile { 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; int version = 0;
std::string filename; 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; 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 { struct WorkingFiles {