Add and remove tags without clearing markup.

This commit is contained in:
John Preston 2021-06-22 16:35:01 +04:00
parent 96b4c6c57a
commit d3eff6f38a
2 changed files with 160 additions and 75 deletions

View file

@ -98,6 +98,60 @@ bool IsNewline(QChar ch) {
return (kNewlineChars.indexOf(ch) >= 0); return (kNewlineChars.indexOf(ch) >= 0);
} }
[[nodiscard]] bool IsSeparateTag(const QStringRef &tag) {
return (tag == kTagCode.midRef(0)) || (tag == kTagPre.midRef(0));
}
[[nodiscard]] bool IsSeparateTag(const QString &tag) {
return IsSeparateTag(tag.midRef(0));
}
[[nodiscard]] QString JoinTag(const QVector<QStringRef> &list) {
if (list.isEmpty()) {
return QString();
}
auto length = (list.size() - 1);
for (const auto &entry : list) {
length += entry.size();
}
auto result = QString();
result.reserve(length);
result.append(list.front());
for (auto i = 1, count = list.size(); i != count; ++i) {
if (!IsSeparateTag(list[i])) {
result.append('|').append(list[i]);
}
}
return result;
}
[[nodiscard]] QString TagWithRemoved(
const QString &tag,
const QString &removed) {
if (tag == removed) {
return QString();
}
auto list = tag.splitRef('|');
list.erase(ranges::remove(list, removed.midRef(0)), list.end());
return JoinTag(list);
}
[[nodiscard]] QString TagWithAdded(
const QString &tag,
const QString &added) {
if (tag == added) {
return tag;
}
auto list = tag.splitRef('|');
const auto ref = added.midRef(0);
if (list.contains(ref)) {
return tag;
}
list.push_back(ref);
ranges::sort(list);
return JoinTag(list);
}
[[nodiscard]] bool IsValidMarkdownLink(const QStringRef &link) { [[nodiscard]] bool IsValidMarkdownLink(const QStringRef &link) {
return (link.indexOf('.') >= 0) || (link.indexOf(':') >= 0); return (link.indexOf('.') >= 0) || (link.indexOf(':') >= 0);
} }
@ -133,9 +187,11 @@ bool IsNewline(QChar ch) {
resultLink = single.toString(); resultLink = single.toString();
found = true; found = true;
break; break;
} else if (resultLink.midRef(0) != single) { } else if (resultLink.midRef(0) == single) {
return QString(); found = true;
break;
} }
return QString();
} else if (!checkingLink && tag.midRef(0) == normalized) { } else if (!checkingLink && tag.midRef(0) == normalized) {
found = true; found = true;
break; break;
@ -2922,6 +2978,10 @@ auto InputField::selectionEditLinkData(EditLinkSelection selection) const
const auto format = state.i.fragment().charFormat(); const auto format = state.i.fragment().charFormat();
return format.property(kTagProperty).toString(); return format.property(kTagProperty).toString();
}; };
const auto stateTagHasLink = [&](const State &state) {
const auto tag = stateTag(state);
return (tag == link) || tag.splitRef('|').contains(link.midRef(0));
};
const auto stateStart = [&](const State &state) { const auto stateStart = [&](const State &state) {
return state.i.fragment().position(); return state.i.fragment().position();
}; };
@ -2941,14 +3001,14 @@ auto InputField::selectionEditLinkData(EditLinkSelection selection) const
} else if (fragmentStart >= selection.till) { } else if (fragmentStart >= selection.till) {
break; break;
} }
if (stateTag(state) == link) { if (stateTagHasLink(state)) {
auto start = fragmentStart; auto start = fragmentStart;
auto finish = fragmentEnd; auto finish = fragmentEnd;
auto copy = state; auto copy = state;
while (moveToPrevious(copy) && (stateTag(copy) == link)) { while (moveToPrevious(copy) && stateTagHasLink(copy)) {
start = stateStart(copy); start = stateStart(copy);
} }
while (skipInvalid(state) && (stateTag(state) == link)) { while (skipInvalid(state) && stateTagHasLink(state)) {
finish = stateEnd(state); finish = stateEnd(state);
moveToNext(state); moveToNext(state);
} }
@ -3270,73 +3330,87 @@ bool InputField::commitMarkdownReplacement(
return true; return true;
} }
void InputField::addMarkdownReplacement( void InputField::addMarkdownTag(
int from, int from,
int till, int till,
const QString &tag) { const QString &tag) {
const auto end = [&] {
auto cursor = QTextCursor(document()->docHandle(), 0);
cursor.movePosition(QTextCursor::End);
return cursor.position();
}();
const auto current = getTextWithTagsPart(from, till); const auto current = getTextWithTagsPart(from, till);
const auto insert = current.text;
const auto tagRef = tag.midRef(0); const auto tagRef = tag.midRef(0);
auto markdownTagApplies = std::vector<MarkdownTag>(); auto markdownTagApplies = std::vector<MarkdownTag>();
// #TODO Trim inserted tag, so that all newlines are left outside. // #TODO Trim inserted tag, so that all newlines are left outside.
_insertedTags.clear(); auto tags = TagList();
auto filled = 0; auto filled = 0;
for (const auto &existing : current.tags) { const auto add = [&](const TextWithTags::Tag &existing) {
if (existing.offset >= till) { const auto id = TagWithAdded(existing.id, tag);
break; tags.push_back({ existing.offset, existing.length, id });
} else if (existing.offset > filled) { filled = std::clamp(
_insertedTags.push_back({ filled, existing.offset - filled, tag }); existing.offset + existing.length,
auto &inserted = _insertedTags.back(); filled,
filled = existing.offset; till - from);
markdownTagApplies.push_back({ from + inserted.offset, from + filled, -1, -1, false, inserted.id }); markdownTagApplies.push_back({
} from + existing.offset,
_insertedTags.push_back(existing); from + filled,
auto &inserted = _insertedTags.back(); -1,
auto list = existing.id.splitRef('|'); -1,
if (list.contains(tagRef)) { false,
continue; id,
} });
list.push_back(tagRef); };
ranges::sort(list); if (!IsSeparateTag(tag)) {
inserted.id.clear(); for (const auto &existing : current.tags) {
for (const auto &single : list) { if (existing.offset >= till) {
if (!inserted.id.isEmpty()) { break;
inserted.id.append('|'); } else if (existing.offset > filled) {
add({ filled, existing.offset - filled, tag });
} }
inserted.id.append(single); add(existing);
} }
filled = std::clamp(existing.offset + existing.length, filled, till - from);
markdownTagApplies.push_back({ from + inserted.offset, from + filled, -1, -1, false, inserted.id });
} }
if (filled < till - from) { if (filled < till - from) {
_insertedTags.push_back({ filled, till - from - filled, tag }); add({ filled, till - from - filled, tag });
auto &inserted = _insertedTags.back();
filled = till - from;
markdownTagApplies.push_back({ from + inserted.offset, from + filled, -1, -1, false, inserted.id });
} }
// Replace. finishMarkdownTagChange(from, till, { current.text, tags });
// Fire the tags to the spellchecker.
for (auto &apply : markdownTagApplies) {
_markdownTagApplies.fire(std::move(apply));
}
}
void InputField::removeMarkdownTag(
int from,
int till,
const QString &tag) {
const auto current = getTextWithTagsPart(from, till);
const auto tagRef = tag.midRef(0);
auto tags = TagList();
for (const auto &existing : current.tags) {
const auto id = TagWithRemoved(existing.id, tag);
if (!id.isEmpty()) {
tags.push_back({ existing.offset, existing.length, id });
}
}
finishMarkdownTagChange(from, till, { current.text, tags });
}
void InputField::finishMarkdownTagChange(
int from,
int till,
const TextWithTags &textWithTags) {
auto cursor = _inner->textCursor(); auto cursor = _inner->textCursor();
cursor.setPosition(from); cursor.setPosition(from);
cursor.setPosition(till, QTextCursor::KeepAnchor); cursor.setPosition(till, QTextCursor::KeepAnchor);
_insertedTags = textWithTags.tags;
_insertedTagsAreFromMime = false; _insertedTagsAreFromMime = false;
cursor.insertText(insert, _defaultCharFormat); cursor.insertText(textWithTags.text, _defaultCharFormat);
_insertedTags.clear(); _insertedTags.clear();
cursor.setCharFormat(_defaultCharFormat); cursor.setCharFormat(_defaultCharFormat);
_inner->setTextCursor(cursor); _inner->setTextCursor(cursor);
// Fire the tag to the spellchecker.
for (auto &apply : markdownTagApplies) {
_markdownTagApplies.fire(std::move(apply));
}
} }
bool InputField::IsValidMarkdownLink(const QStringRef &link) { bool InputField::IsValidMarkdownLink(const QStringRef &link) {
@ -3380,34 +3454,40 @@ void InputField::toggleSelectionMarkdown(const QString &tag) {
if (from == till) { if (from == till) {
return; return;
} }
if (tag.isEmpty() if (tag.isEmpty()) {
|| HasFullTextTag(getTextWithTagsSelected(), tag)) {
//RemoveDocumentTag(_st, document(), from, till, tag);
RemoveDocumentTags(_st, document(), from, till); RemoveDocumentTags(_st, document(), from, till);
return; } else if (HasFullTextTag(getTextWithTagsSelected(), tag)) {
} removeMarkdownTag(from, till, tag);
const auto commitTag = [&] { } else {
if (tag != kTagCode) { const auto useTag = [&] {
return tag; if (tag != kTagCode) {
} return tag;
const auto leftForBlock = [&] {
if (!from) {
return true;
} }
const auto text = getTextWithTagsPart(from - 1, from + 1).text; const auto leftForBlock = [&] {
return text.isEmpty() if (!from) {
|| IsNewline(text[0]) return true;
|| IsNewline(text[text.size() - 1]); }
const auto text = getTextWithTagsPart(
from - 1,
from + 1
).text;
return text.isEmpty()
|| IsNewline(text[0])
|| IsNewline(text[text.size() - 1]);
}();
const auto rightForBlock = [&] {
const auto text = getTextWithTagsPart(
till - 1,
till + 1
).text;
return text.isEmpty()
|| IsNewline(text[0])
|| IsNewline(text[text.size() - 1]);
}();
return (leftForBlock && rightForBlock) ? kTagPre : kTagCode;
}(); }();
const auto rightForBlock = [&] { addMarkdownTag(from, till, useTag);
const auto text = getTextWithTagsPart(till - 1, till + 1).text; }
return text.isEmpty()
|| IsNewline(text[0])
|| IsNewline(text[text.size() - 1]);
}();
return (leftForBlock && rightForBlock) ? kTagPre : kTagCode;
}();
addMarkdownReplacement(from, till, commitTag);
auto restorePosition = textCursor(); auto restorePosition = textCursor();
restorePosition.setPosition((position == till) ? from : till); restorePosition.setPosition((position == till) ? from : till);
restorePosition.setPosition(position, QTextCursor::KeepAnchor); restorePosition.setPosition(position, QTextCursor::KeepAnchor);

View file

@ -453,7 +453,12 @@ private:
int till, int till,
const QString &tag, const QString &tag,
const QString &edge = QString()); const QString &edge = QString());
void addMarkdownReplacement(int from, int till, const QString &tag); void addMarkdownTag(int from, int till, const QString &tag);
void removeMarkdownTag(int from, int till, const QString &tag);
void finishMarkdownTagChange(
int from,
int till,
const TextWithTags &textWithTags);
void toggleSelectionMarkdown(const QString &tag); void toggleSelectionMarkdown(const QString &tag);
void clearSelectionMarkdown(); void clearSelectionMarkdown();