Add #include auto-complete.

This commit is contained in:
Jacob Dufault 2017-05-21 00:37:53 -07:00
parent 7a79532fff
commit 94bd6fc301
8 changed files with 501 additions and 102 deletions

View File

@ -28,7 +28,7 @@ be productive with cquery. Here's a list of implemented features:
* diagnostics
* code actions (clang FixIts)
* darken/fade code disabled by preprocessor
* goto definition, document links on include to jump to file
* #include auto-complete and quick-jump (goto definition, document links)
# Setup - build cquery, install extension, setup project

View File

@ -10,6 +10,10 @@
-IC:/Users/jacob/Desktop/superindex/indexer/third_party/sparsehash/src
-IC:/Users/jacob/Desktop/superindex/indexer/third_party/sparsepp
-IC:/Program Files/LLVM/include
-IC:/Program Files (x86)/Windows Kits/10/Include/10.0.14393.0
-IC:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/include
# OSX
#-I/Users/jdufault/Personal/super-clang-index/third_party
#-I/Users/jdufault/Personal/super-clang-index/third_party/doctest

View File

@ -9,6 +9,7 @@
#include "options.h"
#include "project.h"
#include "platform.h"
#include "standard_includes.h"
#include "test.h"
#include "timer.h"
#include "threaded_queue.h"
@ -21,6 +22,7 @@
#include <fstream>
#include <functional>
#include <iostream>
#include <iterator>
#include <string>
#include <sstream>
#include <unordered_map>
@ -177,11 +179,70 @@ bool ShouldDisplayIpcTiming(IpcId id) {
std::string BaseName(const std::string& path) {
int i = path.size() - 1;
while (i > 0) {
char c = path[i - 1];
if (c == '/' || c == '\\')
break;
--i;
}
return path.substr(i);
}
int TrimCommonPathPrefix(const std::string& result, const std::string& trimmer) {
size_t i = 0;
while (i < result.size() && i < trimmer.size()) {
char a = result[i];
char b = trimmer[i];
#if defined(_WIN32)
a = tolower(a);
b = tolower(b);
#endif
if (a != b)
break;
++i;
}
if (i == trimmer.size())
return i;
return 0;
}
// Returns true iff angle brackets should be used.
bool TrimPath(Project* project, const std::string& project_root, std::string& insert_path) {
int start = 0;
bool angle = false;
int len = TrimCommonPathPrefix(insert_path, project_root);
if (len > start)
start = len;
for (auto& include_dir : project->quote_include_directories) {
len = TrimCommonPathPrefix(insert_path, include_dir);
if (len > start)
start = len;
}
for (auto& include_dir : project->angle_include_directories) {
len = TrimCommonPathPrefix(insert_path, include_dir);
if (len > start) {
start = len;
angle = true;
}
}
insert_path = insert_path.substr(start);
return angle;
}
bool ShouldRunIncludeCompletion(const std::string& line) {
size_t start = 0;
while (start < line.size() && isspace(line[start]))
++start;
return start < line.size() && line[start] == '#';
}
@ -1477,6 +1538,11 @@ bool QueryDbMainLoop(
config->cacheDirectory += '/';
MakeDirectoryRecursive(config->cacheDirectory);
// Set project root.
config->projectRoot = NormalizePath(request->params.rootUri->GetPath());
if (config->projectRoot.empty() || config->projectRoot[config->projectRoot.size() - 1] != '/')
config->projectRoot += '/';
// Start indexer threads.
int indexer_count = std::max<int>(std::thread::hardware_concurrency(), 2) - 1;
if (config->indexerCount > 0)
@ -1520,7 +1586,7 @@ bool QueryDbMainLoop(
response.result.capabilities.completionProvider->resolveProvider = false;
// vscode doesn't support trigger character sequences, so we use ':' for '::' and '>' for '->'.
// See https://github.com/Microsoft/language-server-protocol/issues/138.
response.result.capabilities.completionProvider->triggerCharacters = { ".", ":", ">" };
response.result.capabilities.completionProvider->triggerCharacters = { ".", ":", ">", "#" };
response.result.capabilities.signatureHelpProvider = lsSignatureHelpOptions();
// NOTE: If updating signature help tokens make sure to also update
@ -1772,6 +1838,78 @@ bool QueryDbMainLoop(
lsTextDocumentPositionParams& params = msg->params;
WorkingFile* file = working_files->GetFileByFilename(params.textDocument.uri.GetPath());
// TODO: We should scan include directories to add any missing paths
std::string buffer_line = file->all_buffer_lines[params.position.line];
if (ShouldRunIncludeCompletion(buffer_line)) {
Out_TextDocumentComplete complete_response;
complete_response.id = msg->id;
complete_response.result.isIncomplete = false;
for (const char* stl_header : kStandardLibraryIncludes) {
lsCompletionItem item;
item.label = "#include <" + std::string(stl_header) + ">";
item.insertTextFormat = lsInsertTextFormat::PlainText;
item.kind = lsCompletionItemKind::Module;
// Replace the entire existing content.
item.textEdit = lsTextEdit();
item.textEdit->range.start.line = params.position.line;
item.textEdit->range.start.character = 0;
item.textEdit->range.end.line = params.position.line;
item.textEdit->range.end.character = buffer_line.size();
item.textEdit->newText = item.label;
complete_response.result.items.push_back(item);
}
for (optional<QueryFile>& file : db->files) {
if (!file)
continue;
// TODO: codify list of file extensions somewhere
if (EndsWith(file->def.path, ".c") ||
EndsWith(file->def.path, ".cc") ||
EndsWith(file->def.path, ".cpp"))
continue;
lsCompletionItem item;
// Standard library headers are handled differently.
std::string base_name = BaseName(file->def.path);
if (std::find(std::begin(kStandardLibraryIncludes), std::end(kStandardLibraryIncludes), base_name) != std::end(kStandardLibraryIncludes))
continue;
// Trim the path so it is relative to an include directory.
std::string insert_path = file->def.path;
std::string start_quote = "\"";
std::string end_quote = "\"";
if (TrimPath(project, config->projectRoot, insert_path)) {
start_quote = "<";
end_quote = ">";
}
item.label = "#include " + start_quote + insert_path + end_quote;
item.insertTextFormat = lsInsertTextFormat::PlainText;
item.kind = lsCompletionItemKind::File;
// Replace the entire existing content.
item.textEdit = lsTextEdit();
item.textEdit->range.start.line = params.position.line;
item.textEdit->range.start.character = 0;
item.textEdit->range.end.line = params.position.line;
item.textEdit->range.end.character = buffer_line.size();
item.textEdit->newText = item.label;
complete_response.result.items.push_back(item);
}
complete_response.Write(std::cerr);
ipc->SendOutMessageToClient(IpcId::TextDocumentCompletion, complete_response);
}
else {
if (file)
params.position = file->FindStableCompletionSource(params.position);
@ -1814,6 +1952,7 @@ bool QueryDbMainLoop(
else {
completion_manager->CodeComplete(params, std::move(callback));
}
}
break;
}
@ -1957,7 +2096,6 @@ bool QueryDbMainLoop(
for (const IndexInclude& include : file->def.includes) {
if (include.line == target_line) {
lsLocation result;
std::cerr << "!! resolved to " << include.resolved_path << std::endl;
result.uri = lsDocumentUri::FromPath(include.resolved_path);
response.result.push_back(result);
break;
@ -2158,7 +2296,6 @@ bool QueryDbMainLoop(
}
}
response.Write(std::cerr);
ipc->SendOutMessageToClient(IpcId::TextDocumentDocumentLink, response);
break;
}

View File

@ -56,6 +56,9 @@ void Reflect(Reader& visitor, lsRequestId& id);
struct IndexerConfig {
// Root directory of the project. **Not serialized**
std::string projectRoot;
std::string cacheDirectory;
NonElidedVector<std::string> whitelist;
NonElidedVector<std::string> blacklist;
@ -397,6 +400,19 @@ struct lsTextDocumentPositionParams {
};
MAKE_REFLECT_STRUCT(lsTextDocumentPositionParams, textDocument, position);
struct lsTextEdit {
// The range of the text document to be manipulated. To insert
// text into a document create a range where start === end.
lsRange range;
// The string to be inserted. For delete operations use an
// empty string.
std::string newText;
bool operator==(const lsTextEdit& that);
};
MAKE_REFLECT_STRUCT(lsTextEdit, range, newText);
// Defines whether the insert text in a completion item should be interpreted as
// plain text or a snippet.
enum class lsInsertTextFormat {
@ -479,7 +495,7 @@ struct lsCompletionItem {
//
// *Note:* The range of the edit must be a single line range and it must contain the position at which completion
// has been requested.
// TextEdit textEdit;
optional<lsTextEdit> textEdit;
// An optional array of additional text edits that are applied when
// selecting this completion. Edits must not overlap with the main edit
@ -502,7 +518,8 @@ MAKE_REFLECT_STRUCT(lsCompletionItem,
documentation,
sortText,
insertText,
insertTextFormat);
insertTextFormat,
textEdit);
struct lsTextDocumentItem {
@ -521,19 +538,6 @@ struct lsTextDocumentItem {
};
MAKE_REFLECT_STRUCT(lsTextDocumentItem, uri, languageId, version, text);
struct lsTextEdit {
// The range of the text document to be manipulated. To insert
// text into a document create a range where start === end.
lsRange range;
// The string to be inserted. For delete operations use an
// empty string.
std::string newText;
bool operator==(const lsTextEdit& that);
};
MAKE_REFLECT_STRUCT(lsTextEdit, range, newText);
struct lsTextDocumentEdit {
// The text document to change.
lsVersionedTextDocumentIdentifier textDocument;

View File

@ -82,17 +82,44 @@ static const char *kBlacklist[] = {
// Arguments which are followed by a potentially relative path. We need to make
// all relative paths absolute, otherwise libclang will not resolve them.
const char* kPathArgs[] = {
"-isystem",
"-I",
"-iquote",
"-isystem",
"--sysroot="
};
Project::Entry GetCompilationEntryFromCompileCommandEntry(const std::vector<std::string>& extra_flags, const CompileCommandsEntry& entry) {
const char* kQuoteIncludeArgs[] = {
"-iquote"
};
const char* kAngleIncludeArgs[] = {
"-I",
"-isystem"
};
bool ShouldAddToQuoteIncludes(const std::string& arg) {
for (const char* flag_type : kQuoteIncludeArgs) {
if (arg == flag_type)
return true;
}
return false;
}
bool ShouldAddToAngleIncludes(const std::string& arg) {
for (const char* flag_type : kAngleIncludeArgs) {
if (StartsWith(arg, flag_type))
return true;
}
return false;
}
Project::Entry GetCompilationEntryFromCompileCommandEntry(
std::unordered_set<std::string>& quote_includes, std::unordered_set<std::string>& angle_includes,
const std::vector<std::string>& extra_flags, const CompileCommandsEntry& entry) {
Project::Entry result;
result.filename = NormalizePath(entry.file);
bool make_next_flag_absolute = false;
bool add_next_flag_quote = false;
bool add_next_flag_angle = false;
result.args.reserve(entry.args.size() + extra_flags.size());
for (size_t i = 0; i < entry.args.size(); ++i) {
@ -116,26 +143,40 @@ Project::Entry GetCompilationEntryFromCompileCommandEntry(const std::vector<std:
if (arg.size() > 0 && arg[0] != '/')
arg = NormalizePath(entry.directory + arg);
make_next_flag_absolute = false;
if (add_next_flag_quote)
quote_includes.insert(arg);
if (add_next_flag_angle)
angle_includes.insert(arg);
add_next_flag_quote = false;
add_next_flag_angle = false;
}
// Update arg if it is a path.
for (const char* flag_type : kPathArgs) {
if (arg == flag_type) {
make_next_flag_absolute = true;
add_next_flag_quote = ShouldAddToQuoteIncludes(arg);
add_next_flag_angle = ShouldAddToAngleIncludes(arg);
break;
}
if (StartsWith(arg, flag_type)) {
std::string path = arg.substr(strlen(flag_type));
if (path.size() > 0 && path[0] != '/') {
path = NormalizePath(entry.directory + "/" + path);
if (!entry.directory.empty())
path = entry.directory + "/" + path;
path = NormalizePath(path);
arg = flag_type + path;
}
if (ShouldAddToQuoteIncludes(arg))
quote_includes.insert(path);
if (ShouldAddToAngleIncludes(arg))
angle_includes.insert(path);
break;
}
}
if (make_next_flag_absolute)
continue;
result.args.push_back(arg);
}
@ -154,7 +195,9 @@ Project::Entry GetCompilationEntryFromCompileCommandEntry(const std::vector<std:
return result;
}
std::vector<Project::Entry> LoadFromCompileCommandsJson(const std::vector<std::string>& extra_flags, const std::string& project_directory) {
std::vector<Project::Entry> LoadFromCompileCommandsJson(
std::unordered_set<std::string>& quote_includes, std::unordered_set<std::string>& angle_includes,
const std::vector<std::string>& extra_flags, const std::string& project_directory) {
// TODO: Fix this function, it may be way faster than libclang's implementation.
optional<std::string> compile_commands_content = ReadContent(project_directory + "/compile_commands.json");
@ -175,12 +218,14 @@ std::vector<Project::Entry> LoadFromCompileCommandsJson(const std::vector<std::s
if (entry.args.empty() && !entry.command.empty())
entry.args = SplitString(entry.command, " ");
result.push_back(GetCompilationEntryFromCompileCommandEntry(extra_flags, entry));
result.push_back(GetCompilationEntryFromCompileCommandEntry(quote_includes, angle_includes, extra_flags, entry));
}
return result;
}
std::vector<Project::Entry> LoadFromDirectoryListing(const std::vector<std::string>& extra_flags, const std::string& project_directory) {
std::vector<Project::Entry> LoadFromDirectoryListing(
std::unordered_set<std::string>& quote_includes, std::unordered_set<std::string>& angle_includes,
const std::vector<std::string>& extra_flags, const std::string& project_directory) {
std::vector<Project::Entry> result;
std::vector<std::string> args;
@ -193,27 +238,25 @@ std::vector<Project::Entry> LoadFromDirectoryListing(const std::vector<std::stri
std::cerr << line;
args.push_back(line);
}
for (const std::string& flag : extra_flags) {
std::cerr << flag << std::endl;
args.push_back(flag);
}
std::cerr << std::endl;
std::vector<std::string> files = GetFilesInFolder(project_directory, true /*recursive*/, true /*add_folder_to_path*/);
for (const std::string& file : files) {
if (EndsWith(file, ".cc") || EndsWith(file, ".cpp") || EndsWith(file, ".c")) {
Project::Entry entry;
entry.filename = NormalizePath(file);
entry.args = args;
result.push_back(entry);
CompileCommandsEntry e;
e.file = NormalizePath(file);
e.args = args;
result.push_back(GetCompilationEntryFromCompileCommandEntry(quote_includes, angle_includes, extra_flags, e));
}
}
return result;
}
std::vector<Project::Entry> LoadCompilationEntriesFromDirectory(const std::vector<std::string>& extra_flags, const std::string& project_directory) {
std::vector<Project::Entry> LoadCompilationEntriesFromDirectory(
std::unordered_set<std::string>& quote_includes, std::unordered_set<std::string>& angle_includes,
const std::vector<std::string>& extra_flags, const std::string& project_directory) {
// TODO: Figure out if this function or the clang one is faster.
//return LoadFromCompileCommandsJson(extra_flags, project_directory);
@ -222,7 +265,7 @@ std::vector<Project::Entry> LoadCompilationEntriesFromDirectory(const std::vecto
CXCompilationDatabase cx_db = clang_CompilationDatabase_fromDirectory(project_directory.c_str(), &cx_db_load_error);
if (cx_db_load_error == CXCompilationDatabase_CanNotLoadDatabase) {
std::cerr << "Unable to load compile_commands.json located at \"" << project_directory << "\"; using directory listing instead." << std::endl;
return LoadFromDirectoryListing(extra_flags, project_directory);
return LoadFromDirectoryListing(quote_includes, angle_includes, extra_flags, project_directory);
}
CXCompileCommands cx_commands = clang_CompilationDatabase_getAllCompileCommands(cx_db);
@ -245,7 +288,7 @@ std::vector<Project::Entry> LoadCompilationEntriesFromDirectory(const std::vecto
for (unsigned i = 0; i < num_args; ++i)
entry.args.push_back(clang::ToString(clang_CompileCommand_getArg(cx_command, i)));
result.push_back(GetCompilationEntryFromCompileCommandEntry(extra_flags, entry));
result.push_back(GetCompilationEntryFromCompileCommandEntry(quote_includes, angle_includes, extra_flags, entry));
}
clang_CompileCommands_dispose(cx_commands);
@ -278,10 +321,30 @@ int ComputeGuessScore(const std::string& a, const std::string& b) {
return score;
}
void EnsureEndsInSlash(std::string& path) {
if (path.empty() || path[path.size() - 1] != '/')
path += '/';
}
} // namespace
void Project::Load(const std::vector<std::string>& extra_flags, const std::string& directory) {
entries = LoadCompilationEntriesFromDirectory(extra_flags, directory);
std::unordered_set<std::string> unique_quote_includes;
std::unordered_set<std::string> unique_angle_includes;
entries = LoadCompilationEntriesFromDirectory(unique_quote_includes, unique_angle_includes, extra_flags, directory);
quote_include_directories.assign(unique_quote_includes.begin(), unique_quote_includes.end());
angle_include_directories.assign(unique_angle_includes.begin(), unique_angle_includes.end());
for (std::string& path : quote_include_directories) {
EnsureEndsInSlash(path);
std::cerr << "quote_include_dir: " << path << std::endl;
}
for (std::string& path : angle_include_directories) {
EnsureEndsInSlash(path);
std::cerr << "angle_include_dir: " << path << std::endl;
}
absolute_path_to_entry_index_.resize(entries.size());
for (int i = 0; i < entries.size(); ++i)

View File

@ -21,6 +21,11 @@ struct Project {
bool is_inferred = false;
};
// Include directories for "" headers
std::vector<std::string> quote_include_directories;
// Include directories for <> headers
std::vector<std::string> angle_include_directories;
std::vector<Entry> entries;
spp::sparse_hash_map<std::string, int> absolute_path_to_entry_index_;

182
src/standard_includes.cc Normal file
View File

@ -0,0 +1,182 @@
#include "standard_includes.h"
// See http://stackoverflow.com/a/2029106.
const char* kStandardLibraryIncludes[177] = {
"aio.h",
"algorithm",
"any",
"arpa/inet.h",
"array",
"assert.h",
"atomic",
"bitset",
"cassert",
"ccomplex",
"cctype",
"cerrno",
"cfenv",
"cfloat",
"chrono",
"cinttypes",
"ciso646",
"climits",
"clocale",
"cmath",
"codecvt",
"complex",
"complex.h",
"condition_variable",
"cpio.h",
"csetjmp",
"csignal",
"cstdalign",
"cstdarg",
"cstdbool",
"cstddef",
"cstdint",
"cstdio",
"cstdlib",
"cstring",
"ctgmath",
"ctime",
"ctype.h",
"cuchar",
"curses.h",
"cwchar",
"cwctype",
"deque",
"dirent.h",
"dlfcn.h",
"errno.h",
"exception",
"execution",
"fcntl.h",
"fenv.h",
"filesystem",
"float.h",
"fmtmsg.h",
"fnmatch.h",
"forward_list",
"fstream",
"ftw.h",
"functional",
"future",
"glob.h",
"grp.h",
"iconv.h",
"initializer_list",
"inttypes.h",
"iomanip",
"ios",
"iosfwd",
"iostream",
"iso646.h",
"istream",
"iterator",
"langinfo.h",
"libgen.h",
"limits",
"limits.h",
"list",
"locale",
"locale.h",
"map",
"math.h",
"memory",
"memory_resource",
"monetary.h",
"mqueue.h",
"mutex",
"ndbm.h",
"net/if.h",
"netdb.h",
"netinet/in.h",
"netinet/tcp.h",
"new",
"nl_types.h",
"numeric",
"optional",
"ostream",
"poll.h",
"pthread.h",
"pwd.h",
"queue",
"random",
"ratio",
"regex",
"regex.h",
"sched.h",
"scoped_allocator",
"search.h",
"semaphore.h",
"set",
"setjmp.h",
"shared_mutex",
"signal.h",
"spawn.h",
"sstream",
"stack",
"stdalign.h",
"stdarg.h",
"stdatomic.h",
"stdbool.h",
"stddef.h",
"stdexcept",
"stdint.h",
"stdio.h",
"stdlib.h",
"stdnoreturn.h",
"streambuf",
"string",
"string.h",
"string_view",
"strings.h",
"stropts.h",
"strstream",
"sys/ipc.h",
"sys/mman.h",
"sys/msg.h",
"sys/resource.h",
"sys/select.h",
"sys/sem.h",
"sys/shm.h",
"sys/socket.h",
"sys/stat.h",
"sys/statvfs.h",
"sys/time.h",
"sys/times.h",
"sys/types.h",
"sys/uio.h",
"sys/un.h",
"sys/utsname.h",
"sys/wait.h",
"syslog.h",
"system_error",
"tar.h",
"term.h",
"termios.h",
"tgmath.h",
"thread",
"threads.h",
"time.h",
"trace.h",
"tuple",
"type_traits",
"typeindex",
"typeinfo",
"uchar.h",
"ulimit.h",
"uncntrl.h",
"unistd.h",
"unordered_map",
"unordered_set",
"utility",
"utime.h",
"utmpx.h",
"valarray",
"variant",
"vector",
"wchar.h",
"wctype.h",
"wordexp.h",
};

4
src/standard_includes.h Normal file
View File

@ -0,0 +1,4 @@
#pragma once
// A set of standard libary header names, ie, "vector".
extern const char* kStandardLibraryIncludes[177];