#include "working_files.h" #include "lex_utils.h" #include "position.h" #include #include #include namespace { lsPosition GetPositionForOffset(const std::string& content, int offset) { if (offset >= content.size()) offset = (int)content.size() - 1; lsPosition result; int i = 0; while (i < offset) { if (content[i] == '\n') { result.line += 1; result.character = 0; } else { result.character += 1; } ++i; } return result; } } // namespace WorkingFile::WorkingFile(const std::string& filename, const std::string& buffer_content) : filename(filename), buffer_content(buffer_content) { OnBufferContentUpdated(); // SetIndexContent gets called when the file is opened. } void WorkingFile::SetIndexContent(const std::string& index_content) { index_lines = ToLines(index_content, true /*trim_whitespace*/); // Build lookup buffer. index_lines_lookup.clear(); index_lines_lookup.reserve(index_lines.size()); for (int i = 0; i < index_lines.size(); ++i) { const std::string& index_line = index_lines[i]; auto it = index_lines_lookup.find(index_line); if (it == index_lines_lookup.end()) index_lines_lookup[index_line] = {i + 1}; else it->second.push_back(i + 1); } } void WorkingFile::OnBufferContentUpdated() { all_buffer_lines = ToLines(buffer_content, true /*trim_whitespace*/); // Build lookup buffer. all_buffer_lines_lookup.clear(); all_buffer_lines_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 = all_buffer_lines_lookup.find(buffer_line); if (it == all_buffer_lines_lookup.end()) all_buffer_lines_lookup[buffer_line] = {i + 1}; else it->second.push_back(i + 1); } } optional WorkingFile::GetBufferLineFromIndexLine(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. // TODO: reenable this assert once we are using the real indexed file. // assert(index_line >= 1 && index_line <= index_lines.size()); if (index_line < 1 || index_line > index_lines.size()) { loguru::Text stack = loguru::stacktrace(); LOG_S(WARNING) << "Bad index_line (got " << index_line << ", expected [1, " << index_lines.size() << "]) in " << filename << stack.c_str(); return nullopt; } // 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 = all_buffer_lines_lookup.find(index); if (buffer_it == all_buffer_lines_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; } optional WorkingFile::GetIndexLineFromBufferLine(int buffer_line) const { // See GetBufferLineFromIndexLine for additional comments. // Note: |index_line| and |buffer_line| are 1-based. // assert(buffer_line >= 1 && buffer_line < all_buffer_lines.size()); if (buffer_line < 1 || buffer_line > all_buffer_lines.size()) { loguru::Text stack = loguru::stacktrace(); LOG_S(WARNING) << "Bad buffer_line (got " << buffer_line << ", expected [1, " << all_buffer_lines.size() << "]) in " << filename << stack.c_str(); return nullopt; } // Find the line in the index file. We'll try to find the most similar line // in the index file and return the index for that. std::string buffer = all_buffer_lines[buffer_line - 1]; auto index_it = index_lines_lookup.find(buffer); if (index_it == index_lines_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(!index_it->second.empty()); int closest_dist = INT_MAX; int closest_index_line = INT_MIN; for (int index_line : index_it->second) { int dist = std::abs(buffer_line - index_line); if (dist <= closest_dist) { closest_dist = dist; closest_index_line = index_line; } } return closest_index_line; } optional WorkingFile::GetBufferLineContentFromIndexLine( int indexed_line, optional* out_buffer_line) const { optional buffer_line = GetBufferLineFromIndexLine(indexed_line); if (out_buffer_line) *out_buffer_line = buffer_line; if (!buffer_line) return nullopt; if (*buffer_line < 1 || *buffer_line >= all_buffer_lines.size()) { LOG_S(WARNING) << "GetBufferLineContentFromIndexLine buffer line lookup not" << " in all_buffer_lines"; return nullopt; } return all_buffer_lines[*buffer_line - 1]; } std::string WorkingFile::FindClosestCallNameInBuffer( lsPosition position, int* active_parameter, lsPosition* completion_position) const { *active_parameter = 0; int offset = GetOffsetForPosition(position, buffer_content); // If vscode auto-inserts closing ')' we will begin on ')' token in foo() // which will make the below algorithm think it's a nested call. if (offset > 0 && buffer_content[offset] == ')') --offset; // Scan back out of call context. int balance = 0; while (offset > 0) { char c = buffer_content[offset]; if (c == ')') ++balance; else if (c == '(') --balance; if (balance == 0 && c == ',') *active_parameter += 1; --offset; if (balance == -1) break; } if (offset < 0) return ""; // Scan back entire identifier. int start_offset = offset; while (offset > 0) { char c = buffer_content[offset - 1]; if (isalnum(c) == false && c != '_') break; --offset; } if (completion_position) *completion_position = GetPositionForOffset(buffer_content, offset); return buffer_content.substr(offset, start_offset - offset + 1); } lsPosition WorkingFile::FindStableCompletionSource( lsPosition position, bool* is_global_completion, std::string* existing_completion) const { *is_global_completion = true; int start_offset = GetOffsetForPosition(position, buffer_content); int offset = start_offset; while (offset > 0) { char c = buffer_content[offset - 1]; if (!isalnum(c) && c != '_') { // Global completion is everything except for dot (.), arrow (->), and // double colon (::) if (c == '.') *is_global_completion = false; if (offset > 2) { char pc = buffer_content[offset - 2]; if (pc == ':' && c == ':') *is_global_completion = false; else if (pc == '-' && c == '>') *is_global_completion = false; } break; } --offset; } *existing_completion = buffer_content.substr(offset, start_offset - offset); return GetPositionForOffset(buffer_content, offset); } CXUnsavedFile WorkingFile::AsUnsavedFile() const { CXUnsavedFile result; result.Filename = filename.c_str(); result.Contents = buffer_content.c_str(); result.Length = (unsigned long)buffer_content.size(); return result; } WorkingFile* WorkingFiles::GetFileByFilename(const std::string& filename) { std::lock_guard lock(files_mutex); return GetFileByFilenameNoLock(filename); } WorkingFile* WorkingFiles::GetFileByFilenameNoLock( const std::string& filename) { for (auto& file : files) { if (file->filename == filename) return file.get(); } return nullptr; } void WorkingFiles::DoAction(const std::function& action) { std::lock_guard lock(files_mutex); action(); } void WorkingFiles::DoActionOnFile( const std::string& filename, const std::function& action) { std::lock_guard lock(files_mutex); WorkingFile* file = GetFileByFilenameNoLock(filename); action(file); } WorkingFile* WorkingFiles::OnOpen(const lsTextDocumentItem& open) { std::lock_guard lock(files_mutex); std::string filename = open.uri.GetPath(); std::string content = open.text; // The file may already be open. if (WorkingFile* file = GetFileByFilenameNoLock(filename)) { file->version = open.version; file->buffer_content = content; file->OnBufferContentUpdated(); return file; } files.push_back(MakeUnique(filename, content)); return files[files.size() - 1].get(); } void WorkingFiles::OnChange(const lsTextDocumentDidChangeParams& change) { std::lock_guard lock(files_mutex); std::string filename = change.textDocument.uri.GetPath(); WorkingFile* file = GetFileByFilenameNoLock(filename); if (!file) { LOG_S(WARNING) << "Could not change " << filename << " because it was not open"; return; } file->version = change.textDocument.version; for (const lsTextDocumentContentChangeEvent& diff : change.contentChanges) { // Per the spec replace everything if the rangeLength and range are not set. // See https://github.com/Microsoft/language-server-protocol/issues/9. if (diff.rangeLength == -1 && diff.range.start == lsPosition::kZeroPosition && diff.range.end == lsPosition::kZeroPosition) { file->buffer_content = diff.text; file->OnBufferContentUpdated(); } else { int start_offset = GetOffsetForPosition(diff.range.start, file->buffer_content); int end_offset = GetOffsetForPosition(diff.range.end, file->buffer_content); int length = diff.rangeLength; if (length == -1) { length = end_offset - start_offset; } file->buffer_content.replace( file->buffer_content.begin() + start_offset, file->buffer_content.begin() + start_offset + length, diff.text); file->OnBufferContentUpdated(); } } } void WorkingFiles::OnClose(const lsTextDocumentItem& close) { std::lock_guard lock(files_mutex); std::string filename = close.uri.GetPath(); for (int i = 0; i < files.size(); ++i) { if (files[i]->filename == filename) { files.erase(files.begin() + i); return; } } LOG_S(WARNING) << "Could not close " << filename << " because it was not open"; } std::vector WorkingFiles::AsUnsavedFiles() { std::lock_guard lock(files_mutex); std::vector result; result.reserve(files.size()); for (auto& file : files) result.push_back(file->AsUnsavedFile()); return result; } lsPosition CharPos(const WorkingFile& file, char character, int character_offset = 0) { return CharPos(file.buffer_content, character, character_offset); } TEST_SUITE("WorkingFile") { TEST_CASE("simple call") { WorkingFile f("foo.cc", "abcd(1, 2"); int active_param = 0; REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, '('), &active_param) == "abcd"); REQUIRE(active_param == 0); REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, '1'), &active_param) == "abcd"); REQUIRE(active_param == 0); REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, ','), &active_param) == "abcd"); REQUIRE(active_param == 1); REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, ' '), &active_param) == "abcd"); REQUIRE(active_param == 1); REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, '2'), &active_param) == "abcd"); REQUIRE(active_param == 1); } TEST_CASE("nested call") { WorkingFile f("foo.cc", "abcd(efg(), 2"); int active_param = 0; REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, '('), &active_param) == "abcd"); REQUIRE(active_param == 0); REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, 'e'), &active_param) == "abcd"); REQUIRE(active_param == 0); REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, 'f'), &active_param) == "abcd"); REQUIRE(active_param == 0); REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, 'g'), &active_param) == "abcd"); REQUIRE(active_param == 0); REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, 'g', 1), &active_param) == "efg"); REQUIRE(active_param == 0); REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, 'g', 2), &active_param) == "efg"); REQUIRE(active_param == 0); REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, ','), &active_param) == "abcd"); REQUIRE(active_param == 1); REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, ' '), &active_param) == "abcd"); REQUIRE(active_param == 1); } TEST_CASE("auto-insert )") { WorkingFile f("foo.cc", "abc()"); int active_param = 0; REQUIRE(f.FindClosestCallNameInBuffer(CharPos(f, ')'), &active_param) == "abc"); REQUIRE(active_param == 0); } TEST_CASE("existing completion") { WorkingFile f("foo.cc", "zzz.asdf"); bool is_global_completion; std::string existing_completion; f.FindStableCompletionSource(CharPos(f, '.'), &is_global_completion, &existing_completion); REQUIRE(existing_completion == "zzz"); f.FindStableCompletionSource(CharPos(f, 'a', 1), &is_global_completion, &existing_completion); REQUIRE(existing_completion == "a"); f.FindStableCompletionSource(CharPos(f, 's', 1), &is_global_completion, &existing_completion); REQUIRE(existing_completion == "as"); f.FindStableCompletionSource(CharPos(f, 'd', 1), &is_global_completion, &existing_completion); REQUIRE(existing_completion == "asd"); f.FindStableCompletionSource(CharPos(f, 'f', 1), &is_global_completion, &existing_completion); REQUIRE(existing_completion == "asdf"); } TEST_CASE("existing completion underscore") { WorkingFile f("foo.cc", "ABC_DEF"); bool is_global_completion; std::string existing_completion; f.FindStableCompletionSource(CharPos(f, 'C'), &is_global_completion, &existing_completion); REQUIRE(existing_completion == "AB"); f.FindStableCompletionSource(CharPos(f, '_'), &is_global_completion, &existing_completion); REQUIRE(existing_completion == "ABC"); f.FindStableCompletionSource(CharPos(f, 'D'), &is_global_completion, &existing_completion); REQUIRE(existing_completion == "ABC_"); } }