#include "working_files.hh"
#include "log.hh"
#include "position.hh"
#include <clang/Basic/CharInfo.h>
#include <algorithm>
#include <chrono>
#include <climits>
#include <numeric>
namespace chrono = std::chrono;
using namespace clang;
using namespace llvm;
namespace ccls {
namespace {
constexpr int kMaxDiff = 20;
constexpr int kMaxColumnAlignSize = 200;
Position getPositionForOffset(const std::string &content, int offset) {
if (offset >= content.size())
offset = (int)content.size() - 1;
int line = 0, col = 0;
int i = 0;
for (; i < offset; i++) {
if (content[i] == '\n')
line++, col = 0;
else
col++;
}
return {line, col};
}
std::vector<std::string> toLines(const std::string &c) {
std::vector<std::string> ret;
int last = 0, e = c.size();
for (int i = 0; i < e; i++)
if (c[i] == '\n') {
ret.emplace_back(&c[last], i - last - (i && c[i - 1] == '\r'));
last = i + 1;
}
if (last < e)
ret.emplace_back(&c[last], e - last);
return ret;
}
int myersDiff(const char *a, int la, const char *b, int lb, int threshold) {
assert(threshold <= kMaxDiff);
static int v_static[2 * kMaxColumnAlignSize + 2];
const char *ea = a + la, *eb = b + lb;
for (; a < ea && b < eb && *a == *b; a++, b++) {
}
for (; a < ea && b < eb && ea[-1] == eb[-1]; ea--, eb--) {
}
la = int(ea - a);
lb = int(eb - b);
if (la + lb > 2 * kMaxColumnAlignSize)
return std::min(abs(la - lb), threshold + 1);
int *v = v_static + lb;
v[1] = 0;
for (int di = 0; di <= threshold; di++) {
int low = -di + 2 * std::max(0, di - lb),
high = di - 2 * std::max(0, di - la);
for (int i = low; i <= high; i += 2) {
int x = i == -di || (i != di && v[i - 1] < v[i + 1]) ? v[i + 1]
: v[i - 1] + 1,
y = x - i;
while (x < la && y < lb && a[x] == b[y])
x++, y++;
v[i] = x;
if (x == la && y == lb)
return di;
}
}
return threshold + 1;
}
int myersDiff(const std::string &a, const std::string &b, int threshold) {
return myersDiff(a.data(), a.size(), b.data(), b.size(), threshold);
}
std::vector<int> editDistanceVector(std::string a, std::string b) {
std::vector<int> d(b.size() + 1);
std::iota(d.begin(), d.end(), 0);
for (int i = 0; i < (int)a.size(); i++) {
int ul = d[0];
d[0] = i + 1;
for (int j = 0; j < (int)b.size(); j++) {
int t = d[j + 1];
d[j + 1] = a[i] == b[j] ? ul : std::min(d[j], d[j + 1]) + 1;
ul = t;
}
}
return d;
}
int alignColumn(const std::string &a, int column, std::string b, bool is_end) {
int head = 0, tail = 0;
while (head < (int)a.size() && head < (int)b.size() && a[head] == b[head])
head++;
while (tail < (int)a.size() && tail < (int)b.size() &&
a[a.size() - 1 - tail] == b[b.size() - 1 - tail])
tail++;
if (column < head)
return column;
if ((int)a.size() - tail < column)
return column + (int)b.size() - (int)a.size();
if (std::max(a.size(), b.size()) - head - tail >= kMaxColumnAlignSize)
return std::min(column, (int)b.size());
b = b.substr(head, b.size() - tail - head);
std::vector<int> left = editDistanceVector(a.substr(head, column - head), b);
std::string a_rev = a.substr(column, a.size() - tail - column);
std::reverse(a_rev.begin(), a_rev.end());
std::reverse(b.begin(), b.end());
std::vector<int> right = editDistanceVector(a_rev, b);
std::reverse(right.begin(), right.end());
int best = 0, best_cost = INT_MAX;
for (size_t i = 0; i < left.size(); i++) {
int cost = left[i] + right[i];
if (is_end ? cost < best_cost : cost <= best_cost) {
best_cost = cost;
best = i;
}
}
return head + best;
}
std::optional<int>
findMatchingLine(const std::vector<std::string> &index_lines,
const std::vector<int> &index_to_buffer, int line, int *column,
const std::vector<std::string> &buffer_lines, bool is_end) {
if (index_to_buffer[line] >= 0) {
int ret = index_to_buffer[line];
if (column)
*column =
alignColumn(index_lines[line], *column, buffer_lines[ret], is_end);
return ret;
}
int up = line, down = line;
while (--up >= 0 && index_to_buffer[up] < 0) {
}
while (++down < int(index_to_buffer.size()) && index_to_buffer[down] < 0) {
}
up = up < 0 ? 0 : index_to_buffer[up];
down = down >= int(index_to_buffer.size()) ? int(buffer_lines.size()) - 1
: index_to_buffer[down];
if (up > down)
return std::nullopt;
int best = up, best_dist = kMaxDiff + 1;
const std::string &needle = index_lines[line];
for (int i = up; i <= down; i++) {
int dist = myersDiff(needle, buffer_lines[i], kMaxDiff);
if (dist < best_dist) {
best_dist = dist;
best = i;
}
}
if (column)
*column =
alignColumn(index_lines[line], *column, buffer_lines[best], is_end);
return best;
}
}
WorkingFile::WorkingFile(const std::string &filename,
const std::string &buffer_content)
: filename(filename), buffer_content(buffer_content) {
onBufferContentUpdated();
}
void WorkingFile::setIndexContent(const std::string &index_content) {
index_lines = toLines(index_content);
index_to_buffer.clear();
buffer_to_index.clear();
}
void WorkingFile::onBufferContentUpdated() {
buffer_lines = toLines(buffer_content);
index_to_buffer.clear();
buffer_to_index.clear();
}
void WorkingFile::computeLineMapping() {
std::unordered_map<uint64_t, int> hash_to_unique;
std::vector<uint64_t> index_hashes(index_lines.size());
std::vector<uint64_t> buffer_hashes(buffer_lines.size());
index_to_buffer.resize(index_lines.size());
buffer_to_index.resize(buffer_lines.size());
hash_to_unique.reserve(
std::max(index_to_buffer.size(), buffer_to_index.size()));
int i = 0;
for (StringRef line : index_lines) {
uint64_t h = hashUsr(line);
auto it = hash_to_unique.find(h);
if (it == hash_to_unique.end()) {
hash_to_unique[h] = i;
index_to_buffer[i] = i;
} else {
if (it->second >= 0)
index_to_buffer[it->second] = -1;
index_to_buffer[i] = it->second = -1;
}
index_hashes[i++] = h;
}
i = 0;
hash_to_unique.clear();
for (StringRef line : buffer_lines) {
uint64_t h = hashUsr(line);
auto it = hash_to_unique.find(h);
if (it == hash_to_unique.end()) {
hash_to_unique[h] = i;
buffer_to_index[i] = i;
} else {
if (it->second >= 0)
buffer_to_index[it->second] = -1;
buffer_to_index[i] = it->second = -1;
}
buffer_hashes[i++] = h;
}
i = 0;
for (auto h : index_hashes) {
if (index_to_buffer[i] >= 0) {
auto it = hash_to_unique.find(h);
if (it != hash_to_unique.end() && it->second >= 0 &&
buffer_to_index[it->second] >= 0)
index_to_buffer[i] = it->second;
else
index_to_buffer[i] = -1;
}
i++;
}
for (i = 0; i < (int)index_hashes.size() - 1; i++) {
int j = index_to_buffer[i];
if (0 <= j && j + 1 < buffer_hashes.size() &&
index_hashes[i + 1] == buffer_hashes[j + 1])
index_to_buffer[i + 1] = j + 1;
}
for (i = (int)index_hashes.size(); --i > 0;) {
int j = index_to_buffer[i];
if (0 < j && index_hashes[i - 1] == buffer_hashes[j - 1])
index_to_buffer[i - 1] = j - 1;
}
std::fill(buffer_to_index.begin(), buffer_to_index.end(), -1);
for (i = 0; i < (int)index_hashes.size(); i++)
if (index_to_buffer[i] >= 0)
buffer_to_index[index_to_buffer[i]] = i;
}
std::optional<int> WorkingFile::getBufferPosFromIndexPos(int line, int *column,
bool is_end) {
if (line == (int)index_lines.size() && !*column)
return buffer_content.size();
if (line < 0 || line >= (int)index_lines.size()) {
LOG_S(WARNING) << "bad index_line (got " << line << ", expected [0, "
<< index_lines.size() << ")) in " << filename;
return std::nullopt;
}
if (index_to_buffer.empty())
computeLineMapping();
return findMatchingLine(index_lines, index_to_buffer, line, column,
buffer_lines, is_end);
}
std::optional<int> WorkingFile::getIndexPosFromBufferPos(int line, int *column,
bool is_end) {
if (line < 0 || line >= (int)buffer_lines.size())
return std::nullopt;
if (buffer_to_index.empty())
computeLineMapping();
return findMatchingLine(buffer_lines, buffer_to_index, line, column,
index_lines, is_end);
}
Position WorkingFile::getCompletionPosition(Position pos, std::string *filter) const {
int start = getOffsetForPosition(pos, buffer_content);
int i = start;
#if LLVM_VERSION_MAJOR < 14
#define isAsciiIdentifierContinue isIdentifierBody
#endif
while (i > 0 && isAsciiIdentifierContinue(buffer_content[i - 1]))
--i;
*filter = buffer_content.substr(i, start - i);
return getPositionForOffset(buffer_content, i);
}
WorkingFile *WorkingFiles::getFile(const std::string &path) {
std::lock_guard lock(mutex);
return getFileUnlocked(path);
}
WorkingFile *WorkingFiles::getFileUnlocked(const std::string &path) {
auto it = files.find(path);
return it != files.end() ? it->second.get() : nullptr;
}
std::string WorkingFiles::getContent(const std::string &path) {
std::lock_guard lock(mutex);
auto it = files.find(path);
return it != files.end() ? it->second->buffer_content : "";
}
WorkingFile *WorkingFiles::onOpen(const TextDocumentItem &open) {
std::lock_guard lock(mutex);
std::string path = open.uri.getPath();
std::string content = open.text;
auto &wf = files[path];
if (wf) {
wf->version = open.version;
wf->buffer_content = content;
wf->onBufferContentUpdated();
} else {
wf = std::make_unique<WorkingFile>(path, content);
}
return wf.get();
}
void WorkingFiles::onChange(const TextDocumentDidChangeParam &change) {
std::lock_guard lock(mutex);
std::string path = change.textDocument.uri.getPath();
WorkingFile *file = getFileUnlocked(path);
if (!file) {
LOG_S(WARNING) << "Could not change " << path << " because it was not open";
return;
}
file->timestamp = chrono::duration_cast<chrono::seconds>(
chrono::high_resolution_clock::now().time_since_epoch())
.count();
if (change.textDocument.version)
file->version = *change.textDocument.version;
for (const TextDocumentContentChangeEvent &diff : change.contentChanges) {
if (!diff.range) {
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);
file->buffer_content.replace(file->buffer_content.begin() + start_offset,
file->buffer_content.begin() + end_offset,
diff.text);
file->onBufferContentUpdated();
}
}
}
void WorkingFiles::onClose(const std::string &path) {
std::lock_guard lock(mutex);
files.erase(path);
}
int getOffsetForPosition(Position pos, std::string_view content) {
size_t i = 0;
for (; pos.line > 0 && i < content.size(); i++)
if (content[i] == '\n')
pos.line--;
for (; pos.character > 0 && i < content.size() && content[i] != '\n';
pos.character--)
if (uint8_t(content[i++]) >= 128) {
while (i < content.size() && uint8_t(content[i]) >= 128 &&
uint8_t(content[i]) < 192)
i++;
}
return int(i);
}
std::string_view lexIdentifierAroundPos(Position position,
std::string_view content) {
int start = getOffsetForPosition(position, content), end = start + 1;
char c;
for (; start > 0; start--) {
c = content[start - 1];
if (c == ':' && start > 1 && content[start - 2] == ':')
start--;
else if (!isAsciiIdentifierContinue(c))
break;
}
for (; end < content.size() && isAsciiIdentifierContinue(content[end]); end++)
;
return content.substr(start, end - start);
}
}