Инкрементальный индекс, пресеты автосейва, диалог Настройки
- VaultIndex: updateFile/removeFile/syncIncremental (mtime+size), полный рескан только на старте/смене/внешних изменениях; relink висячих [[ссылок]] при создании файла; FileStore.externalChanged для внешних правок - QML: сейвы и правки идут точечно, дерево — через syncIncremental - Настройки: пресеты safe/balanced/eco/manual (диск 2/5/10с/выкл, коммит после паузы 5/15/60с/вручную), диалог с +/-, шкалами и подсказкой; коммит только в простое, в manual — по Ctrl+S - Удалены highlightedCode, словный merge, fileDiffText/Html, 2-arg fileHistory; renderInner без побочек - Тесты: инкремент индекса, пресеты, autosaveEnabled, ~~~-блоки
This commit is contained in:
+259
-45
@@ -140,53 +140,11 @@ QString VaultIndex::resolveLink(const QString &fileDir, const QString &target) c
|
||||
return QString();
|
||||
}
|
||||
|
||||
void VaultIndex::rebuild(const QString &vault)
|
||||
void VaultIndex::deduplicateEdges()
|
||||
{
|
||||
if (vault.isEmpty() || !QDir(vault).exists()) {
|
||||
m_nodes.clear();
|
||||
m_edges.clear();
|
||||
m_nodeById.clear();
|
||||
m_tagsOfFile.clear();
|
||||
m_linksOfFile.clear();
|
||||
m_filesWithTag.clear();
|
||||
m_fileByBase.clear();
|
||||
m_fileCount = 0;
|
||||
emit indexChanged();
|
||||
return;
|
||||
}
|
||||
if (m_busy)
|
||||
return;
|
||||
m_busy = true;
|
||||
emit busyChanged();
|
||||
|
||||
m_nodes.clear();
|
||||
m_edges.clear();
|
||||
m_nodeById.clear();
|
||||
m_tagsOfFile.clear();
|
||||
m_linksOfFile.clear();
|
||||
m_filesWithTag.clear();
|
||||
m_fileByBase.clear();
|
||||
m_fileCount = 0;
|
||||
|
||||
// Проход 1: собрать все md-файлы и карту имён
|
||||
QStringList files;
|
||||
collectFiles(vault, 0, files);
|
||||
m_fileCount = files.size();
|
||||
for (const QString &f : files) {
|
||||
const QString base = QFileInfo(f).completeBaseName();
|
||||
const QString key = base.toLower();
|
||||
if (!base.isEmpty() && !m_fileByBase.contains(key))
|
||||
m_fileByBase.insert(key, f);
|
||||
// также храним оригинал для точного поиска
|
||||
if (!base.isEmpty() && !m_fileByBase.contains(base))
|
||||
m_fileByBase.insert(base, f);
|
||||
}
|
||||
// Проход 2: узлы/теги/ссылки
|
||||
for (const QString &f : files)
|
||||
parseFile(f);
|
||||
// Дедупликация рёбер
|
||||
QSet<QString> seen;
|
||||
QVariantList uniq;
|
||||
uniq.reserve(m_edges.size());
|
||||
for (const auto &e : std::as_const(m_edges)) {
|
||||
const QString k = e.toMap().value("from").toString() + "->" + e.toMap().value("to").toString();
|
||||
if (!seen.contains(k)) {
|
||||
@@ -195,12 +153,258 @@ void VaultIndex::rebuild(const QString &vault)
|
||||
}
|
||||
}
|
||||
m_edges = uniq;
|
||||
}
|
||||
|
||||
bool VaultIndex::shouldIndex(const QString &cleanPath) const
|
||||
{
|
||||
if (m_vault.isEmpty())
|
||||
return false;
|
||||
if (cleanPath != m_vault && !cleanPath.startsWith(m_vault + '/'))
|
||||
return false;
|
||||
const QFileInfo fi(cleanPath);
|
||||
if (!fi.exists() || !fi.isFile())
|
||||
return false;
|
||||
if (kIgnoredNames.contains(fi.fileName()))
|
||||
return false;
|
||||
return fi.suffix().compare(QLatin1String("md"), Qt::CaseInsensitive) == 0;
|
||||
}
|
||||
|
||||
VaultIndex::FileMeta VaultIndex::metaOf(const QString &cleanPath)
|
||||
{
|
||||
const QFileInfo fi(cleanPath);
|
||||
return {fi.lastModified(), fi.size()};
|
||||
}
|
||||
|
||||
void VaultIndex::purgeOutgoing(const QString &cleanPath)
|
||||
{
|
||||
// Теги файла
|
||||
const QStringList oldTags = m_tagsOfFile.take(cleanPath);
|
||||
for (const QString &tag : oldTags) {
|
||||
auto it = m_filesWithTag.find(tag);
|
||||
if (it != m_filesWithTag.end()) {
|
||||
it.value().removeAll(cleanPath);
|
||||
if (it.value().isEmpty())
|
||||
m_filesWithTag.erase(it);
|
||||
}
|
||||
}
|
||||
// Исходящие рёбра (входящие от других файлов не трогаем — они всё ещё валидны)
|
||||
const QString key = nodeKeyFor(cleanPath, true);
|
||||
QVariantList kept;
|
||||
kept.reserve(m_edges.size());
|
||||
for (const auto &e : std::as_const(m_edges)) {
|
||||
if (e.toMap().value("from").toString() != key)
|
||||
kept.append(e);
|
||||
}
|
||||
m_edges = kept;
|
||||
m_linksOfFile.remove(cleanPath);
|
||||
m_unresolvedByFile.remove(cleanPath);
|
||||
}
|
||||
|
||||
void VaultIndex::dropNode(const QString &key)
|
||||
{
|
||||
auto it = m_nodeById.find(key);
|
||||
if (it == m_nodeById.end())
|
||||
return;
|
||||
m_nodes.removeAt(it.value());
|
||||
m_nodeById.clear();
|
||||
for (int i = 0; i < m_nodes.size(); ++i)
|
||||
m_nodeById.insert(m_nodes[i].toMap().value("key").toString(), i);
|
||||
}
|
||||
|
||||
void VaultIndex::dropFile(const QString &cleanPath)
|
||||
{
|
||||
const QString key = nodeKeyFor(cleanPath, true);
|
||||
purgeOutgoing(cleanPath);
|
||||
// Входящие рёбра и ссылки других файлов на удалённый — тоже убрать
|
||||
QVariantList kept;
|
||||
kept.reserve(m_edges.size());
|
||||
for (const auto &e : std::as_const(m_edges)) {
|
||||
if (e.toMap().value("to").toString() != key)
|
||||
kept.append(e);
|
||||
}
|
||||
m_edges = kept;
|
||||
for (auto it = m_linksOfFile.begin(); it != m_linksOfFile.end(); ++it) {
|
||||
QVariantList &links = it.value();
|
||||
for (int i = links.size() - 1; i >= 0; --i) {
|
||||
if (links[i].toMap().value("path").toString() == cleanPath)
|
||||
links.removeAt(i);
|
||||
}
|
||||
}
|
||||
dropNode(key);
|
||||
// bybase-записи, указывавшие на удалённый файл
|
||||
for (auto it = m_fileByBase.begin(); it != m_fileByBase.end();) {
|
||||
if (it.value() == cleanPath)
|
||||
it = m_fileByBase.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
m_fileMeta.remove(cleanPath);
|
||||
}
|
||||
|
||||
bool VaultIndex::relinkForNewFile(const QString &newPath)
|
||||
{
|
||||
Q_UNUSED(newPath)
|
||||
// Новый файл мог починить чьи-то висячие ссылки — перепроверяем только их
|
||||
bool any = false;
|
||||
const QStringList files = m_unresolvedByFile.keys();
|
||||
for (const QString &f : files) {
|
||||
if (!m_fileMeta.contains(f)) {
|
||||
m_unresolvedByFile.remove(f);
|
||||
continue;
|
||||
}
|
||||
const QString dir = QFileInfo(f).absolutePath();
|
||||
bool nowOk = false;
|
||||
for (const QString &t : std::as_const(m_unresolvedByFile)[f]) {
|
||||
if (!resolveLink(dir, t).isEmpty()) {
|
||||
nowOk = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (nowOk) {
|
||||
purgeOutgoing(f);
|
||||
parseFile(f);
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
if (any)
|
||||
deduplicateEdges();
|
||||
return any;
|
||||
}
|
||||
|
||||
void VaultIndex::rebuild(const QString &vault)
|
||||
{
|
||||
if (vault.isEmpty() || !QDir(vault).exists()) {
|
||||
m_vault.clear();
|
||||
m_nodes.clear();
|
||||
m_edges.clear();
|
||||
m_nodeById.clear();
|
||||
m_tagsOfFile.clear();
|
||||
m_linksOfFile.clear();
|
||||
m_filesWithTag.clear();
|
||||
m_fileByBase.clear();
|
||||
m_unresolvedByFile.clear();
|
||||
m_fileMeta.clear();
|
||||
emit indexChanged();
|
||||
return;
|
||||
}
|
||||
if (m_busy)
|
||||
return;
|
||||
m_busy = true;
|
||||
emit busyChanged();
|
||||
|
||||
m_vault = QDir::cleanPath(vault);
|
||||
m_nodes.clear();
|
||||
m_edges.clear();
|
||||
m_nodeById.clear();
|
||||
m_tagsOfFile.clear();
|
||||
m_linksOfFile.clear();
|
||||
m_filesWithTag.clear();
|
||||
m_fileByBase.clear();
|
||||
m_unresolvedByFile.clear();
|
||||
m_fileMeta.clear();
|
||||
|
||||
// Проход 1: собрать все md-файлы и карту имён
|
||||
QStringList files;
|
||||
collectFiles(m_vault, 0, files);
|
||||
for (const QString &f : files) {
|
||||
const QString clean = QDir::cleanPath(f);
|
||||
const QString base = QFileInfo(clean).completeBaseName();
|
||||
const QString key = base.toLower();
|
||||
if (!base.isEmpty() && !m_fileByBase.contains(key))
|
||||
m_fileByBase.insert(key, clean);
|
||||
// также храним оригинал для точного поиска
|
||||
if (!base.isEmpty() && !m_fileByBase.contains(base))
|
||||
m_fileByBase.insert(base, clean);
|
||||
m_fileMeta.insert(clean, metaOf(clean));
|
||||
}
|
||||
// Проход 2: узлы/теги/ссылки
|
||||
for (const QString &f : files)
|
||||
parseFile(QDir::cleanPath(f));
|
||||
deduplicateEdges();
|
||||
|
||||
m_busy = false;
|
||||
emit busyChanged();
|
||||
emit indexChanged();
|
||||
}
|
||||
|
||||
void VaultIndex::updateFile(const QString &path)
|
||||
{
|
||||
if (m_vault.isEmpty())
|
||||
return;
|
||||
const QString clean = QDir::cleanPath(path);
|
||||
if (!shouldIndex(clean)) {
|
||||
// Удаление / переименование-из / не-md: просто выкинуть из индекса
|
||||
if (m_fileMeta.contains(clean)) {
|
||||
dropFile(clean);
|
||||
emit indexChanged();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const bool isNew = !m_fileMeta.contains(clean);
|
||||
const QStringList oldTags = m_tagsOfFile.value(clean);
|
||||
const QVariantList oldLinks = m_linksOfFile.value(clean);
|
||||
purgeOutgoing(clean);
|
||||
parseFile(clean);
|
||||
deduplicateEdges();
|
||||
m_fileMeta.insert(clean, metaOf(clean));
|
||||
bool changed = isNew || m_tagsOfFile.value(clean) != oldTags
|
||||
|| m_linksOfFile.value(clean) != oldLinks;
|
||||
if (isNew)
|
||||
changed = relinkForNewFile(clean) || changed;
|
||||
if (changed)
|
||||
emit indexChanged();
|
||||
}
|
||||
|
||||
void VaultIndex::removeFile(const QString &path)
|
||||
{
|
||||
if (m_vault.isEmpty())
|
||||
return;
|
||||
const QString clean = QDir::cleanPath(path);
|
||||
if (!m_fileMeta.contains(clean) && !m_nodeById.contains(nodeKeyFor(clean, true)))
|
||||
return;
|
||||
dropFile(clean);
|
||||
emit indexChanged();
|
||||
}
|
||||
|
||||
void VaultIndex::syncIncremental()
|
||||
{
|
||||
if (m_vault.isEmpty() || !QDir(m_vault).exists() || m_busy)
|
||||
return;
|
||||
// Только метаданные (без чтения содержимого) — дёшево даже на тысячах файлов
|
||||
QStringList files;
|
||||
collectFiles(m_vault, 0, files);
|
||||
QSet<QString> seen;
|
||||
seen.reserve(files.size() * 2);
|
||||
bool changed = false;
|
||||
for (const QString &f : files) {
|
||||
const QString clean = QDir::cleanPath(f);
|
||||
seen.insert(clean);
|
||||
auto it = m_fileMeta.constFind(clean);
|
||||
if (it == m_fileMeta.constEnd() || it.value() != metaOf(clean)) {
|
||||
const QStringList oldTags = m_tagsOfFile.value(clean);
|
||||
const QVariantList oldLinks = m_linksOfFile.value(clean);
|
||||
const bool isNew = (it == m_fileMeta.constEnd());
|
||||
purgeOutgoing(clean);
|
||||
parseFile(clean);
|
||||
m_fileMeta.insert(clean, metaOf(clean));
|
||||
changed = changed || isNew || m_tagsOfFile.value(clean) != oldTags
|
||||
|| m_linksOfFile.value(clean) != oldLinks;
|
||||
if (isNew)
|
||||
changed = relinkForNewFile(clean) || changed;
|
||||
}
|
||||
}
|
||||
for (auto it = m_fileMeta.constBegin(); it != m_fileMeta.constEnd(); ++it) {
|
||||
if (!seen.contains(it.key())) {
|
||||
dropFile(it.key());
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
deduplicateEdges();
|
||||
emit indexChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void VaultIndex::collectFiles(const QString &dir, int depth, QStringList &out) const
|
||||
{
|
||||
if (depth > 24)
|
||||
@@ -251,8 +455,10 @@ void VaultIndex::parseFile(const QString &path)
|
||||
m_filesWithTag[tag].append(path);
|
||||
}
|
||||
|
||||
// Ссылки [[X]] и [text](rel) (поддерживаем алиас | в wiki)
|
||||
// Ссылки [[X]] и [text](rel) (поддерживаем алиас | в wiki).
|
||||
// Неразрешённые цели запоминаем — вдруг нужный файл создадут позже.
|
||||
QVariantList links;
|
||||
QStringList unresolved;
|
||||
static const QRegularExpression wikiRe(QStringLiteral("\\[\\[([^\\]]+)\\]\\]"));
|
||||
auto wi = wikiRe.globalMatch(text);
|
||||
while (wi.hasNext()) {
|
||||
@@ -270,6 +476,8 @@ void VaultIndex::parseFile(const QString &path)
|
||||
addEdge(key, nodeKeyFor(resolved, true));
|
||||
ensureNode(nodeKeyFor(resolved, true),
|
||||
QFileInfo(resolved).fileName(), QStringLiteral("file"));
|
||||
} else if (!unresolved.contains(target)) {
|
||||
unresolved.append(target);
|
||||
}
|
||||
}
|
||||
static const QRegularExpression mdLinkRe(
|
||||
@@ -286,9 +494,15 @@ void VaultIndex::parseFile(const QString &path)
|
||||
addEdge(key, nodeKeyFor(resolved, true));
|
||||
ensureNode(nodeKeyFor(resolved, true),
|
||||
QFileInfo(resolved).fileName(), QStringLiteral("file"));
|
||||
} else if (!unresolved.contains(target)) {
|
||||
unresolved.append(target);
|
||||
}
|
||||
}
|
||||
m_linksOfFile.insert(path, links);
|
||||
if (unresolved.isEmpty())
|
||||
m_unresolvedByFile.remove(path);
|
||||
else
|
||||
m_unresolvedByFile.insert(path, unresolved);
|
||||
}
|
||||
|
||||
QStringList VaultIndex::tagsOfFile(const QString &path) const
|
||||
|
||||
Reference in New Issue
Block a user