diff --git a/src/command_line.cc b/src/command_line.cc index f8967a45..adfdc1fc 100644 --- a/src/command_line.cc +++ b/src/command_line.cc @@ -82,10 +82,21 @@ optional GetDefinitionSpellingOfSymbol(QueryableDatabase* db, return nullopt; } -lsRange GetLsRange(const Range& location) { +lsRange GetLsRange(WorkingFile* working_file, const Range& location) { + if (!working_file) { + return lsRange( + lsPosition(location.start.line - 1, location.start.column - 1), + lsPosition(location.end.line - 1, location.end.column - 1)); + } + return lsRange( - lsPosition(location.start.line - 1, location.start.column - 1), - lsPosition(location.end.line - 1, location.end.column - 1)); + lsPosition(working_file->GetBufferLineFromDiskLine(location.start.line) - 1, location.start.column - 1), + lsPosition(working_file->GetBufferLineFromDiskLine(location.end.line) - 1, location.end.column - 1)); +} + +lsDocumentUri GetLsDocumentUri(QueryableDatabase* db, QueryFileId file_id, std::string* path) { + *path = db->files[file_id.id].def.usr; + return lsDocumentUri::FromPath(*path); } lsDocumentUri GetLsDocumentUri(QueryableDatabase* db, QueryFileId file_id) { @@ -93,15 +104,16 @@ lsDocumentUri GetLsDocumentUri(QueryableDatabase* db, QueryFileId file_id) { return lsDocumentUri::FromPath(path); } -lsLocation GetLsLocation(QueryableDatabase* db, const QueryableLocation& location) { - return lsLocation( - GetLsDocumentUri(db, location.path), - GetLsRange(location.range)); +lsLocation GetLsLocation(QueryableDatabase* db, WorkingFiles* working_files, const QueryableLocation& location) { + std::string path; + lsDocumentUri uri = GetLsDocumentUri(db, location.path, &path); + lsRange range = GetLsRange(working_files->GetFileByFilename(path), location.range); + return lsLocation(uri, range); } // Returns a symbol. The symbol will have the location pointing to the // definition. -lsSymbolInformation GetSymbolInfo(QueryableDatabase* db, SymbolIdx symbol) { +lsSymbolInformation GetSymbolInfo(QueryableDatabase* db, WorkingFiles* working_files, SymbolIdx symbol) { lsSymbolInformation info; switch (symbol.kind) { @@ -117,7 +129,7 @@ lsSymbolInformation GetSymbolInfo(QueryableDatabase* db, SymbolIdx symbol) { info.name = def->def.qualified_name; info.kind = lsSymbolKind::Class; if (def->def.definition_extent.has_value()) - info.location = GetLsLocation(db, def->def.definition_extent.value()); + info.location = GetLsLocation(db, working_files, def->def.definition_extent.value()); break; } case SymbolKind::Func: { @@ -132,7 +144,7 @@ lsSymbolInformation GetSymbolInfo(QueryableDatabase* db, SymbolIdx symbol) { } if (def->def.definition_extent.has_value()) { - info.location = GetLsLocation(db, def->def.definition_extent.value()); + info.location = GetLsLocation(db, working_files, def->def.definition_extent.value()); } break; } @@ -142,7 +154,7 @@ lsSymbolInformation GetSymbolInfo(QueryableDatabase* db, SymbolIdx symbol) { info.kind = lsSymbolKind::Variable; if (def->def.definition_extent.has_value()) { - info.location = GetLsLocation(db, def->def.definition_extent.value()); + info.location = GetLsLocation(db, working_files, def->def.definition_extent.value()); } break; } @@ -158,15 +170,17 @@ lsSymbolInformation GetSymbolInfo(QueryableDatabase* db, SymbolIdx symbol) { void AddCodeLens( QueryableDatabase* db, + WorkingFiles* working_files, std::vector* result, QueryableLocation loc, + WorkingFile* working_file, const std::vector& uses, bool exclude_loc, bool only_interesting, const char* singular, const char* plural) { TCodeLens code_lens; - code_lens.range = GetLsRange(loc.range); + code_lens.range = GetLsRange(working_file, loc.range); code_lens.command = lsCommand(); code_lens.command->command = "superindex.showReferences"; code_lens.command->arguments.uri = GetLsDocumentUri(db, loc.path); @@ -179,7 +193,7 @@ void AddCodeLens( continue; if (only_interesting && !use.range.interesting) continue; - unique_uses.insert(GetLsLocation(db, use)); + unique_uses.insert(GetLsLocation(db, working_files, use)); } code_lens.command->arguments.locations.assign(unique_uses.begin(), unique_uses.end()); @@ -236,6 +250,28 @@ std::vector ToQueryableLocation(QueryableDatabase* db, const } // namespace + + + + + + + + + + + + + + + + + + + + + + struct Index_DoIndex { enum class Type { Import, @@ -539,6 +575,21 @@ void QueryDbMainLoop( case IpcId::TextDocumentDidChange: { auto msg = static_cast(message.get()); working_files->OnChange(msg->params); + + + // Send an index update request. + // TODO: we should only do this when we save. Figure out a way to handle code lens (dynamic offsets?) + +#if false + Index_DoIndex request(Index_DoIndex::Type::Update); + //WorkingFile* changed = working_files->GetFileByFilename(msg->params.textDocument.uri.GetPath()); + optional entry = project->FindCompilationEntryForFile(msg->params.textDocument.uri.GetPath()); + request.path = msg->params.textDocument.uri.GetPath(); + if (entry) + request.args = entry->args; + queue_do_index->Enqueue(std::move(request)); +#endif + //std::cerr << "Changing " << msg->params.textDocument.uri.GetPath() << std::endl; break; } @@ -586,7 +637,7 @@ void QueryDbMainLoop( ref.loc.range.start.column <= target_column && ref.loc.range.end.column >= target_column) { optional location = GetDefinitionSpellingOfSymbol(db, ref.idx); if (location) - response.result.push_back(GetLsLocation(db, location.value())); + response.result.push_back(GetLsLocation(db, working_files, location.value())); break; } } @@ -609,8 +660,8 @@ void QueryDbMainLoop( std::cerr << "File outline size is " << file->def.outline.size() << std::endl; for (SymbolRef ref : file->def.outline) { - lsSymbolInformation info = GetSymbolInfo(db, ref.idx); - info.location = GetLsLocation(db, ref.loc); + lsSymbolInformation info = GetSymbolInfo(db, working_files, ref.idx); + info.location = GetLsLocation(db, working_files, ref.loc); response.result.push_back(info); } @@ -631,6 +682,7 @@ void QueryDbMainLoop( std::cerr << "Unable to find file " << msg->params.textDocument.uri.GetPath() << std::endl; break; } + WorkingFile* working_file = working_files->GetFileByFilename(file->def.usr); for (SymbolRef ref : file->def.outline) { // NOTE: We OffsetColumn so that the code lens always show up in a @@ -640,15 +692,15 @@ void QueryDbMainLoop( switch (symbol.kind) { case SymbolKind::Type: { QueryableTypeDef& def = db->types[symbol.idx]; - AddCodeLens(db, &response.result, ref.loc.OffsetStartColumn(0), def.uses, + AddCodeLens(db, working_files, &response.result, ref.loc.OffsetStartColumn(0), working_file, def.uses, false /*exclude_loc*/, false /*only_interesting*/, "ref", "refs"); - AddCodeLens(db, &response.result, ref.loc.OffsetStartColumn(1), def.uses, + AddCodeLens(db, working_files, &response.result, ref.loc.OffsetStartColumn(1), working_file, def.uses, false /*exclude_loc*/, true /*only_interesting*/, "iref", "irefs"); - AddCodeLens(db, &response.result, ref.loc.OffsetStartColumn(2), ToQueryableLocation(db, def.derived), + AddCodeLens(db, working_files, &response.result, ref.loc.OffsetStartColumn(2), working_file, ToQueryableLocation(db, def.derived), false /*exclude_loc*/, false /*only_interesting*/, "derived", "derived"); - AddCodeLens(db, &response.result, ref.loc.OffsetStartColumn(3), ToQueryableLocation(db, def.instantiations), + AddCodeLens(db, working_files, &response.result, ref.loc.OffsetStartColumn(3), working_file, ToQueryableLocation(db, def.instantiations), false /*exclude_loc*/, false /*only_interesting*/, "instantiation", "instantiations"); break; } @@ -657,17 +709,17 @@ void QueryDbMainLoop( //AddCodeLens(&response.result, ref.loc.OffsetStartColumn(0), def.uses, // false /*exclude_loc*/, false /*only_interesting*/, "reference", // "references"); - AddCodeLens(db, &response.result, ref.loc.OffsetStartColumn(1), ToQueryableLocation(db, def.callers), + AddCodeLens(db, working_files, &response.result, ref.loc.OffsetStartColumn(1), working_file, ToQueryableLocation(db, def.callers), true /*exclude_loc*/, false /*only_interesting*/, "caller", "callers"); //AddCodeLens(&response.result, ref.loc.OffsetColumn(2), def.def.callees, // false /*exclude_loc*/, false /*only_interesting*/, "callee", "callees"); - AddCodeLens(db, &response.result, ref.loc.OffsetStartColumn(3), ToQueryableLocation(db, def.derived), + AddCodeLens(db, working_files, &response.result, ref.loc.OffsetStartColumn(3), working_file, ToQueryableLocation(db, def.derived), false /*exclude_loc*/, false /*only_interesting*/, "derived", "derived"); break; } case SymbolKind::Var: { QueryableVarDef& def = db->vars[symbol.idx]; - AddCodeLens(db, &response.result, ref.loc.OffsetStartColumn(0), def.uses, + AddCodeLens(db, working_files, &response.result, ref.loc.OffsetStartColumn(0), working_file, def.uses, true /*exclude_loc*/, false /*only_interesting*/, "reference", "references"); break; @@ -702,7 +754,7 @@ void QueryDbMainLoop( } if (db->qualified_names[i].find(query) != std::string::npos) - response.result.push_back(GetSymbolInfo(db, db->symbols[i])); + response.result.push_back(GetSymbolInfo(db, working_files, db->symbols[i])); } SendOutMessageToClient(language_client, response); @@ -827,7 +879,7 @@ void LanguageServerStdinLoop(IpcMessageQueue* ipc) { //response.result.capabilities.textDocumentSync->change = lsTextDocumentSyncKind::Full; //response.result.capabilities.textDocumentSync->willSave = true; //response.result.capabilities.textDocumentSync->willSaveWaitUntil = true; - response.result.capabilities.textDocumentSync = lsTextDocumentSyncKind::Full; // TODO: use incremental at some point + response.result.capabilities.textDocumentSync = lsTextDocumentSyncKind::Incremental; // TODO: use incremental at some point response.result.capabilities.completionProvider = lsCompletionOptions(); response.result.capabilities.completionProvider->resolveProvider = false; @@ -970,7 +1022,7 @@ int main(int argc, char** argv) { if (context.shouldExit()) return res; - RunTests(); + //RunTests(); return 0; } else if (options.find("--help") != options.end()) { diff --git a/src/query.h b/src/query.h index 7c74f38a..caa996f6 100644 --- a/src/query.h +++ b/src/query.h @@ -56,11 +56,11 @@ struct QueryableLocation { } }; -class QueryableFile; -class QueryableTypeDef; -class QueryableFuncDef; -class QueryableVarDef; -class QueryableDatabase; +struct QueryableFile; +struct QueryableTypeDef; +struct QueryableFuncDef; +struct QueryableVarDef; +struct QueryableDatabase; enum class SymbolKind { Invalid, File, Type, Func, Var }; struct SymbolIdx { diff --git a/src/working_files.cc b/src/working_files.cc index 942e88fc..f6ecde19 100644 --- a/src/working_files.cc +++ b/src/working_files.cc @@ -1,5 +1,55 @@ #include "working_files.h" +#include + +namespace { + +int GetOffsetForPosition(lsPosition position, const std::string& content) { + int offset = 0; + + int remaining_lines = position.line; + while (remaining_lines > 0) { + if (content[offset] == '\n') + --remaining_lines; + ++offset; + } + + 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 IsNextTokenNewline(const std::string& str, int start) { + while (start < str.size()) { + // Peek a token ahead for \r\n. We cannot check a token back because + // isalpha('\r') returns false. + if ((start + 1 < str.size()) && str[start] == '\r' && str[start + 1] == '\n') + return true; + + if (str[start] == '\n') + return true; + + if (!isalpha(str[start])) + return false; + + ++start; + } + + return true; +} + +} // namespace + +WorkingFile::Change::Change(int disk_line, int delta) : disk_line(disk_line), delta(delta) {} + WorkingFile::WorkingFile(const std::string& filename, const std::string& content) : filename(filename), content(content) { } @@ -12,6 +62,101 @@ CXUnsavedFile WorkingFile::AsUnsavedFile() const { 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) @@ -44,14 +189,61 @@ void WorkingFiles::OnChange(const Ipc_TextDocumentDidChange::Params& change) { // TODO: we should probably pay attention to versioning. for (const Ipc_TextDocumentDidChange::lsTextDocumentContentChangeEvent& diff : change.contentChanges) { + //std::cerr << "Applying rangeLength=" << diff.rangeLength; + // If range or rangeLength are emitted we replace everything, per the spec. if (diff.rangeLength == -1) { file->content = diff.text; } else { - file->content.replace(file->content.begin(), file->content.begin() + diff.rangeLength, diff.text); + int start_offset = GetOffsetForPosition(diff.range.start, file->content); + int previous_line_count = diff.range.end.line - diff.range.start.line; + int replace_line_count = LineCount(diff.text.begin(), diff.text.end()); + + int line_delta = replace_line_count - previous_line_count; + if (line_delta != 0) { + // language server stores line counts starting at 0, we start at 1 + int buffer_line = diff.range.start.line + 1; + + // If we're adding a newline but not actually shifting any of the + // contents of this line, we should mark the text prepended on the next + // line. + if (IsNextTokenNewline(file->content, start_offset)) + ++buffer_line; + + int disk_line = file->GetDiskLineFromBufferLine(buffer_line); + + bool found = false; + for (int i = 0; i < file->changes.size(); ++i) { + auto& change = file->changes[i]; + if (change.disk_line == disk_line) { + 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::cerr << "(changes.size()=" << file->changes.size() << ") Inserted delta=" << line_delta << " at disk_line=" << disk_line << " with buffer_line=" << buffer_line << std::endl; + //for (auto& change : file->changes) + // std::cerr << " disk_line=" << change.disk_line << " delta=" << change.delta << std::endl; + + std::sort(file->changes.begin(), file->changes.end(), + [](const WorkingFile::Change& a, const WorkingFile::Change& b) { + return a.disk_line < b.disk_line; + }); + } + + + file->content.replace(file->content.begin() + start_offset, + file->content.begin() + start_offset + diff.rangeLength, + diff.text); } } + //std::cerr << std::endl << std::endl << "--------" << file->content << "--------" << std::endl << std::endl; } void WorkingFiles::OnClose(const Ipc_TextDocumentDidClose::Params& close) { @@ -75,3 +267,294 @@ std::vector WorkingFiles::AsUnsavedFiles() const { 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_SUITE_END(); \ No newline at end of file diff --git a/src/working_files.h b/src/working_files.h index 3d5af6c4..1b4c2a06 100644 --- a/src/working_files.h +++ b/src/working_files.h @@ -1,18 +1,69 @@ #pragma once #include "language_server_api.h" +#include "utils.h" #include #include 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); + }; + std::string filename; std::string content; + std::vector changes; WorkingFile(const std::string& filename, const std::string& content); 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 {