#include "include_complete.h" #include "lex_utils.h" #include "message_handler.h" #include "query_utils.h" #include "queue_manager.h" #include #include #include namespace { optional FindIncludeLine(const std::vector& lines, const std::string& full_include_line) { // // This returns an include line. For example, // // #include // 0 // #include // 1 // // Given #include , this will return '1', which means that the // #include text should be inserted at the start of line 1. Inserting // at the start of a line allows insertion at both the top and bottom of the // document. // // If the include line is already in the document this returns nullopt. // optional last_include_line; optional best_include_line; // 1 => include line is gt content (ie, it should go after) // -1 => include line is lt content (ie, it should go before) int last_line_compare = 1; for (int line = 0; line < (int)lines.size(); ++line) { std::string text = Trim(lines[line]); if (!StartsWith(text, "#include")) { last_line_compare = 1; continue; } last_include_line = line; int current_line_compare = full_include_line.compare(text); if (current_line_compare == 0) return nullopt; if (last_line_compare == 1 && current_line_compare == -1) best_include_line = line; last_line_compare = current_line_compare; } if (best_include_line) return *best_include_line; // If |best_include_line| didn't match that means we likely didn't find an // include which was lt the new one, so put it at the end of the last include // list. if (last_include_line) return *last_include_line + 1; // No includes, use top of document. return 0; } optional GetImplementationFile(QueryDatabase* db, QueryFileId file_id, QueryFile* file) { for (SymbolRef sym : file->def->outline) { switch (sym.kind) { case SymbolKind::Func: { if (const auto* def = db->GetFunc(sym).AnyDef()) { // Note: we ignore the definition if it is in the same file (ie, // possibly a header). if (def->extent) { QueryFileId t = def->extent->file; if (t != file_id) return t; } } break; } case SymbolKind::Var: { const QueryVar::Def* def = db->GetVar(sym).AnyDef(); // Note: we ignore the definition if it is in the same file (ie, // possibly a header). if (def && def->extent) { QueryFileId t = def->extent->file; if (t != file_id) return t; } break; } default: break; } } // No associated definition, scan the project for a file in the same // directory with the same base-name. NormalizedPath original_path(file->def->path); std::string target_path = original_path.path; size_t last = target_path.find_last_of('.'); if (last != std::string::npos) { target_path = target_path.substr(0, last); } LOG_S(INFO) << "!! Looking for impl file that starts with " << target_path; for (auto& entry : db->usr_to_file) { const NormalizedPath& path = entry.first; // Do not consider header files for implementation files. // TODO: make file extensions configurable. if (EndsWith(path.path, ".h") || EndsWith(path.path, ".hpp")) continue; if (StartsWith(path.path, target_path) && path != original_path) { return entry.second; } } return nullopt; } void EnsureImplFile(QueryDatabase* db, QueryFileId file_id, optional& impl_uri, optional& impl_file_id) { if (!impl_uri.has_value()) { QueryFile& file = db->files[file_id.id]; assert(file.def); impl_file_id = GetImplementationFile(db, file_id, &file); if (!impl_file_id.has_value()) impl_file_id = file_id; QueryFile& impl_file = db->files[impl_file_id->id]; if (impl_file.def) impl_uri = lsDocumentUri::FromPath(impl_file.def->path); else impl_uri = lsDocumentUri::FromPath(file.def->path); } } optional BuildAutoImplementForFunction(QueryDatabase* db, WorkingFiles* working_files, WorkingFile* working_file, int default_line, QueryFileId decl_file_id, QueryFileId impl_file_id, QueryFunc& func) { const QueryFunc::Def* def = func.AnyDef(); assert(def); for (Use decl : func.declarations) { if (decl.file != decl_file_id) continue; optional ls_decl = GetLsRange(working_file, decl.range); if (!ls_decl) continue; optional type_name; optional same_file_insert_end; if (def->declaring_type) { QueryType& declaring_type = db->types[def->declaring_type->id]; if (const auto* def1 = declaring_type.AnyDef()) { type_name = std::string(def1->ShortName()); optional ls_type_extent = GetLsRange(working_file, def1->extent->range); if (ls_type_extent) { same_file_insert_end = ls_type_extent->end; same_file_insert_end->character += 1; // move past semicolon. } } } std::string insert_text; int newlines_after_name = 0; LexFunctionDeclaration(working_file->buffer_content, ls_decl->start, type_name, &insert_text, &newlines_after_name); if (!same_file_insert_end) { same_file_insert_end = ls_decl->end; same_file_insert_end->line += newlines_after_name; same_file_insert_end->character = 1000; } lsTextEdit edit; if (decl_file_id == impl_file_id) { edit.range.start = *same_file_insert_end; edit.range.end = *same_file_insert_end; edit.newText = "\n\n" + insert_text; } else { lsPosition best_pos; best_pos.line = default_line; int best_dist = INT_MAX; QueryFile& file = db->files[impl_file_id.id]; assert(file.def); for (SymbolRef sym : file.def->outline) { switch (sym.kind) { case SymbolKind::Func: { QueryFunc& sym_func = db->GetFunc(sym); const QueryFunc::Def* def1 = sym_func.AnyDef(); if (!def1 || !def1->extent) break; for (Use func_decl : sym_func.declarations) { if (func_decl.file == decl_file_id) { int dist = func_decl.range.start.line - decl.range.start.line; if (abs(dist) < abs(best_dist)) { optional def_loc = GetLsLocation(db, working_files, *def1->extent); if (!def_loc) continue; best_dist = dist; if (dist > 0) best_pos = def_loc->range.start; else best_pos = def_loc->range.end; } } } break; } case SymbolKind::Var: { // TODO: handle vars. break; } case SymbolKind::Invalid: case SymbolKind::File: case SymbolKind::Type: LOG_S(WARNING) << "Unexpected SymbolKind " << static_cast(sym.kind); break; } } edit.range.start = best_pos; edit.range.end = best_pos; if (best_dist < 0) edit.newText = "\n\n" + insert_text; else edit.newText = insert_text + "\n\n"; } return edit; } return nullopt; } struct Ipc_TextDocumentCodeAction : public RequestMessage { const static IpcId kIpcId = IpcId::TextDocumentCodeAction; // Contains additional diagnostic information about the context in which // a code action is run. struct lsCodeActionContext { // An array of diagnostics. std::vector diagnostics; }; // Params for the CodeActionRequest struct lsCodeActionParams { // The document in which the command was invoked. lsTextDocumentIdentifier textDocument; // The range for which the command was invoked. lsRange range; // Context carrying additional information. lsCodeActionContext context; }; lsCodeActionParams params; }; MAKE_REFLECT_STRUCT(Ipc_TextDocumentCodeAction::lsCodeActionContext, diagnostics); MAKE_REFLECT_STRUCT(Ipc_TextDocumentCodeAction::lsCodeActionParams, textDocument, range, context); MAKE_REFLECT_STRUCT(Ipc_TextDocumentCodeAction, id, params); REGISTER_IPC_MESSAGE(Ipc_TextDocumentCodeAction); struct Out_TextDocumentCodeAction : public lsOutMessage { struct CommandArgs { lsDocumentUri textDocumentUri; std::vector edits; }; using Command = lsCommand; lsRequestId id; std::vector result; }; MAKE_REFLECT_STRUCT_WRITER_AS_ARRAY(Out_TextDocumentCodeAction::CommandArgs, textDocumentUri, edits); MAKE_REFLECT_STRUCT(Out_TextDocumentCodeAction, jsonrpc, id, result); struct TextDocumentCodeActionHandler : BaseMessageHandler { void Run(Ipc_TextDocumentCodeAction* request) override { // NOTE: This code snippet will generate some FixIts for testing: // // struct origin { int x, int y }; // void foo() { // point origin = { // x: 0.0, // y: 0.0 // }; // } // QueryFileId file_id; QueryFile* file; if (!FindFileOrFail(db, project, request->id, request->params.textDocument.uri.GetPath(), &file, &file_id)) { return; } WorkingFile* working_file = working_files->GetFileByFilename( request->params.textDocument.uri.GetPath()); if (!working_file) { // TODO: send error response. LOG_S(WARNING) << "[error] textDocument/codeAction could not find working file"; return; } Out_TextDocumentCodeAction out; out.id = request->id; // TODO: auto-insert namespace? int default_line = (int)working_file->buffer_lines.size(); // Make sure to call EnsureImplFile before using these. We lazy load // them because computing the values could involve an entire project // scan. optional impl_uri; optional impl_file_id; std::vector syms = FindSymbolsAtLocation(working_file, file, request->params.range.start); for (SymbolRef sym : syms) { switch (sym.kind) { case SymbolKind::Type: { QueryType& type = db->GetType(sym); const QueryType::Def* def = type.AnyDef(); if (!def) break; int num_edits = 0; // Get implementation file. Out_TextDocumentCodeAction::Command command; EachDefinedEntity(db->funcs, def->funcs, [&](QueryFunc& func_def) { const QueryFunc::Def* def1 = func_def.AnyDef(); if (def1->extent) return; EnsureImplFile(db, file_id, impl_uri /*out*/, impl_file_id /*out*/); optional edit = BuildAutoImplementForFunction( db, working_files, working_file, default_line, file_id, *impl_file_id, func_def); if (!edit) return; ++num_edits; // Merge edits together if they are on the same line. // TODO: be smarter about newline merging? ie, don't end up // with foo()\n\n\n\nfoo(), we want foo()\n\nfoo()\n\n // if (!command.arguments.edits.empty() && command.arguments.edits[command.arguments.edits.size() - 1] .range.end.line == edit->range.start.line) { command.arguments.edits[command.arguments.edits.size() - 1] .newText += edit->newText; } else { command.arguments.edits.push_back(*edit); } }); if (command.arguments.edits.empty()) break; // If we're inserting at the end of the document, put a newline // before the insertion. if (command.arguments.edits[0].range.start.line >= default_line) command.arguments.edits[0].newText.insert(0, "\n"); command.arguments.textDocumentUri = *impl_uri; command.title = "Auto-Implement " + std::to_string(num_edits) + " methods on " + std::string(def->ShortName()); command.command = "cquery._autoImplement"; out.result.push_back(command); break; } case SymbolKind::Func: { QueryFunc& func = db->GetFunc(sym); const QueryFunc::Def* def = func.AnyDef(); if (!def || def->extent) break; EnsureImplFile(db, file_id, impl_uri /*out*/, impl_file_id /*out*/); // Get implementation file. Out_TextDocumentCodeAction::Command command; command.title = "Auto-Implement " + std::string(def->ShortName()); command.command = "cquery._autoImplement"; command.arguments.textDocumentUri = *impl_uri; optional edit = BuildAutoImplementForFunction( db, working_files, working_file, default_line, file_id, *impl_file_id, func); if (!edit) break; // If we're inserting at the end of the document, put a newline // before the insertion. if (edit->range.start.line >= default_line) edit->newText.insert(0, "\n"); command.arguments.edits.push_back(*edit); out.result.push_back(command); break; } default: break; } // Only show one auto-impl section. if (!out.result.empty()) break; } std::vector diagnostics; working_files->DoAction( [&]() { diagnostics = working_file->diagnostics_; }); for (lsDiagnostic& diag : diagnostics) { if (diag.range.start.line != request->params.range.start.line) continue; // For error diagnostics, provide an action to resolve an include. // TODO: find a way to index diagnostic contents so line numbers // don't get mismatched when actively editing a file. std::string_view include_query = LexIdentifierAroundPos(diag.range.start, working_file->buffer_content); if (diag.severity == lsDiagnosticSeverity::Error && !include_query.empty()) { const size_t kMaxResults = 20; std::unordered_set include_absolute_paths; // Find include candidate strings. for (size_t i = 0; i < db->symbols.size(); ++i) { if (include_absolute_paths.size() > kMaxResults) break; if (db->GetSymbolDetailedName(i).find(include_query) == std::string::npos) continue; Maybe decl_file_id = GetDeclarationFileForSymbol(db, db->symbols[i]); if (!decl_file_id) continue; QueryFile& decl_file = db->files[decl_file_id->id]; if (!decl_file.def) continue; include_absolute_paths.insert(decl_file.def->path); } // Build include strings. std::unordered_set include_insert_strings; include_insert_strings.reserve(include_absolute_paths.size()); for (const std::string& path : include_absolute_paths) { optional item = include_complete->FindCompletionItemForAbsolutePath(path); if (!item) continue; if (item->textEdit) include_insert_strings.insert(item->textEdit->newText); else if (!item->insertText.empty()) include_insert_strings.insert(item->insertText); else { // FIXME https://github.com/cquery-project/cquery/issues/463 LOG_S(WARNING) << "unable to determine insert string for include " "completion item"; } } // Build code action. if (!include_insert_strings.empty()) { Out_TextDocumentCodeAction::Command command; // Build edits. for (const std::string& include_insert_string : include_insert_strings) { lsTextEdit edit; optional include_line = FindIncludeLine( working_file->buffer_lines, include_insert_string); if (!include_line) continue; edit.range.start.line = *include_line; edit.range.end.line = *include_line; edit.newText = include_insert_string + "\n"; command.arguments.edits.push_back(edit); } // Setup metadata and send to client. if (include_insert_strings.size() == 1) command.title = "Insert " + *include_insert_strings.begin(); else command.title = "Pick one of " + std::to_string(command.arguments.edits.size()) + " includes to insert"; command.command = "cquery._insertInclude"; command.arguments.textDocumentUri = request->params.textDocument.uri; out.result.push_back(command); } } // clang does not provide accurate enough column reporting for // diagnostics to do good column filtering, so report all // diagnostics on the line. if (!diag.fixits_.empty()) { Out_TextDocumentCodeAction::Command command; command.title = "FixIt: " + diag.message; command.command = "cquery._applyFixIt"; command.arguments.textDocumentUri = request->params.textDocument.uri; command.arguments.edits = diag.fixits_; out.result.push_back(command); } } QueueManager::WriteStdout(IpcId::TextDocumentCodeAction, out); } }; REGISTER_MESSAGE_HANDLER(TextDocumentCodeActionHandler); TEST_SUITE("FindIncludeLine") { TEST_CASE("in document") { std::vector lines = { "#include ", // 0 "#include " // 1 }; REQUIRE(FindIncludeLine(lines, "#include ") == nullopt); } TEST_CASE("insert before") { std::vector lines = { "#include ", // 0 "#include " // 1 }; REQUIRE(FindIncludeLine(lines, "#include ") == 0); } TEST_CASE("insert middle") { std::vector lines = { "#include ", // 0 "#include " // 1 }; REQUIRE(FindIncludeLine(lines, "#include ") == 1); } TEST_CASE("insert after") { std::vector lines = { "#include ", // 0 "#include ", // 1 "", // 2 }; REQUIRE(FindIncludeLine(lines, "#include ") == 2); } TEST_CASE("ignore header") { std::vector lines = { "// FOOBAR", // 0 "// FOOBAR", // 1 "// FOOBAR", // 2 "// FOOBAR", // 3 "", // 4 "#include ", // 5 "#include ", // 6 "", // 7 }; REQUIRE(FindIncludeLine(lines, "#include ") == 5); REQUIRE(FindIncludeLine(lines, "#include ") == 6); REQUIRE(FindIncludeLine(lines, "#include ") == 7); } } } // namespace