#include "qcodeedit.h"

#include "ecombobox.h"
#include "iconedlabel.h"
#include "qad_types.h"
#include "qcodeedit_completer_p.h"
#include "ui_qcodeedit.h"

#include <QAction>
#include <QApplication>
#include <QBoxLayout>
#include <QDesktopServices>
#include <QLayout>
#include <QLineEdit>
#include <QPainter>
#include <QPlainTextEdit>
#include <QScrollBar>
#include <QTextBlock>
#include <QTextCursor>
#include <QTextDocument>
#include <QTextDocumentFragment>
#include <QTextOption>
#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
#  include <QWindow>
#endif

Q_DECLARE_METATYPE(QTextCursor)
Q_DECLARE_METATYPE(QCodeEdit::ACEntry)


class _QCE_Viewport: public QWidget {
public:
	_QCE_Viewport(QWidget * p = nullptr): QWidget(p) {
		setObjectName("__qcodeedit_viewport__");
		setMouseTracking(true);
	}

	void paintEvent(QPaintEvent *) override {
		if (!isEnabled()) return;
		ce->drawCursor();
	}

	QCodeEdit * ce;
};


QCodeEdit::QCodeEdit(QWidget * parent): QWidget(parent), ui(new Ui::QCodeEdit) {
	overlay = nullptr;
	prev_lc = auto_comp_pl = cur_search_ind = pos_press = pos_el_press = -1;
	timer_parse                                                        = 0;
	_ignore_focus_out = _destructor = _replacing = cursor_state = block_sel_state = false;
	_first                                                                        = true;
	comment_text                                                                  = "//";
	qRegisterMetaType<QTextCursor>();
	qRegisterMetaType<QCodeEdit::ACEntry>();
	ui->setupUi(this);
	detectTheme();
	overlay     = new _QCE_Viewport(ui->textCode->viewport());
	overlay->ce = this;
	overlay->show();
	ui->widgetSearch->hide();
	ui->comboSearch->installEventFilter(this);
	ui->comboReplace->installEventFilter(this);
	es_line.format.setBackground(QColor(240, 245, 240));
	es_line.format.setProperty(QTextFormat::FullWidthSelection, true);
	es_cursor.format.setBackground(QColor(220, 255, 200));
	es_bracket.format.setBackground(QColor(180, 238, 180));
	es_bracket.format.setForeground(Qt::red);
	es_search.format.setBackground(QColor(255, 240, 10));
	es_range.format.setBackground(QColor(230, 246, 255));
	es_range.format.setProperty(QTextFormat::FullWidthSelection, true);
	es_link.format.setForeground(Qt::blue);
	es_link.format.setFontUnderline(true);
	widget_help = new QFrame();
	widget_help->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
	widget_help->setFocusPolicy(Qt::NoFocus);
	widget_help->setFrameShadow(QFrame::Sunken);
	widget_help->setFrameShape(QFrame::StyledPanel);
	widget_help->setLayout(new QBoxLayout(QBoxLayout::TopToBottom));
	widget_help->layout()->setContentsMargins(0, 0, 0, 0);
	for (int i = 0; i < lh_last; ++i) {
		lbl_help[i] = new IconedLabel();
		lbl_help[i]->setFrameShadow(QFrame::Plain);
		lbl_help[i]->setFrameShape(QFrame::NoFrame);
		lbl_help[i]->setDirection(IconedLabel::RightToLeft);
		widget_help->layout()->addWidget(lbl_help[i]);
	}
	lbl_help[lhF1]->setIcon(QIcon(":/icons/f1.png"));
	lbl_help[lhF1]->setText(tr("Press F1 for details"));
	help_visible = true;
	completer    = new QCodeEditCompleter();

	cursor_width = qMax<int>(qRound(fontHeight(this) / 10.), 1);
	ui->textCode->setCursorWidth(0);
	ui->textCode->viewport()->setMouseTracking(true);
	ui->textLines->viewport()->setAutoFillBackground(false);
	ui->textLines->viewport()->setCursor(Qt::ArrowCursor);
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
	ui->textLines->setFixedWidth(ui->textLines->fontMetrics().horizontalAdvance("  "));
#else
	ui->textLines->setFixedWidth(ui->textLines->fontMetrics().width("  "));
#endif

	QAction * a = new QAction(this);
	ui->textCode->addAction(a);
	a->setShortcut(QKeySequence("Shift+Tab"));
	a->setShortcutContext(Qt::WidgetShortcut);
	connect(a, SIGNAL(triggered()), this, SLOT(deindent()));
	a = new QAction(this);
	ui->textCode->addAction(a);
	a->setShortcut(QKeySequence("Ctrl+D"));
	a->setShortcutContext(Qt::WidgetShortcut);
	connect(a, SIGNAL(triggered()), this, SLOT(deleteLine()));
	a = new QAction(this);
	ui->textCode->addAction(a);
	a->setShortcut(QKeySequence("Ctrl+Return"));
	a->setShortcutContext(Qt::WidgetShortcut);
	connect(a, SIGNAL(triggered()), this, SLOT(newLine()));
	a = new QAction(this);
	ui->textCode->addAction(a);
	a->setShortcut(QKeySequence("Ctrl+Up"));
	a->setShortcutContext(Qt::WidgetShortcut);
	connect(a, SIGNAL(triggered()), this, SLOT(scrollUp()));
	a = new QAction(this);
	ui->textCode->addAction(a);
	a->setShortcut(QKeySequence("Ctrl+Down"));
	a->setShortcutContext(Qt::WidgetShortcut);
	connect(a, SIGNAL(triggered()), this, SLOT(scrollDown()));
	a = new QAction(this);
	ui->textCode->addAction(a);
	a->setShortcut(QKeySequence("Ctrl+Shift+Return"));
	a->setShortcutContext(Qt::WidgetShortcut);
	connect(a, SIGNAL(triggered()), this, SLOT(newLineBefore()));
	ui->frame->setFocusProxy(ui->textCode);
	QTextOption to = ui->textLines->document()->defaultTextOption();
	to.setAlignment(Qt::AlignTop | Qt::AlignRight);
	ui->textLines->document()->setDefaultTextOption(to);
	setShowSpaces(true);

	a = new QAction(this);
	a->setShortcut(QKeySequence("Ctrl+F"));
	a->setShortcutContext(Qt::WidgetWithChildrenShortcut);
	connect(a, SIGNAL(triggered(bool)), this, SLOT(search_triggered()));
	addAction(a);

	connect(completer, SIGNAL(itemDoubleClicked(QTreeWidgetItem *, int)), this, SLOT(commitCompletition()));
	connect(completer, SIGNAL(commit()), this, SLOT(commitCompletition()));
	connect(completer, SIGNAL(gotoHRef(QCodeEdit::ACEntry)), this, SLOT(gotoHelpHRef(QCodeEdit::ACEntry)));
	connect(ui->textCode->verticalScrollBar(), SIGNAL(valueChanged(int)), ui->textLines->verticalScrollBar(), SLOT(setValue(int)));
	connect(ui->textCode->verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(hideHelp()));
	connect(ui->textCode->verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(resizeOverlay()));
	connect(ui->textCode->horizontalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(resizeOverlay()));
	connect(ui->textCode, SIGNAL(textChanged()), this, SLOT(textEdit_textChanged()));
	connect(ui->textCode, SIGNAL(textChanged()), this, SIGNAL(textChanged()));
	connect(ui->textCode, SIGNAL(cursorPositionChanged()), this, SLOT(textEdit_cursorPositionChanged()));
	connect(ui->textCode, SIGNAL(selectionChanged()), this, SLOT(textEdit_selectionChanged()));
	connect(ui->textCode, SIGNAL(redoAvailable(bool)), this, SLOT(textEdit_redoAvailable(bool)));
	connect(ui->comboSearch->lineEdit(), SIGNAL(returnPressed()), this, SLOT(searchNext()));
	connect(ui->comboReplace->lineEdit(), SIGNAL(returnPressed()), this, SLOT(on_buttonReplaceSearch_clicked()));
#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
	connect(qApp, &QGuiApplication::focusWindowChanged, this, [this](QWindow * w) {
		if (w == nullptr) {
			hideHelp();
			completer->hide();
		}
	});
#endif
	updateLines();
	timer_blink = startTimer(QApplication::cursorFlashTime() / 2);

	registerAutoCompletitionClass(-1, QCodeEdit::Keyword, "Words", QIcon(":/icons/code-word.png"));
}


QCodeEdit::~QCodeEdit() {
	_destructor = true;
	delete completer;
	delete widget_help;
	delete ui;
}


QTextCursor QCodeEdit::textCursor() const {
	return ui->textCode->textCursor();
}


QTextDocument * QCodeEdit::document() const {
	return ui->textCode->document();
}


void QCodeEdit::setDocument(QTextDocument * doc) {
	cancelBlockSelection();
	if (document()) {
		document()->setProperty("_cursor", QVariant::fromValue(textCursor()));
		document()->setProperty("_vpos", textEdit()->verticalScrollBar()->value());
	}
	ui->textCode->setEnabled(doc);
	ui->textLines->setEnabled(doc);
	documentUnset();
	if (!doc) {
		ui->textCode->setDocument(nullptr);
		documentChanged(nullptr);
		return;
	}
	if (!qobject_cast<QPlainTextDocumentLayout *>(doc->documentLayout())) doc->setDocumentLayout(new QPlainTextDocumentLayout(doc));
	ui->textCode->setDocument(doc);
	cursor_width = qMax<int>(qRound(fontHeight(this) / 10.), 1);
	ui->textCode->setCursorWidth(0);
	setShowSpaces(spaces_);
	if (doc->property("_cursor").isValid()) {
		setTextCursor(doc->property("_cursor").value<QTextCursor>());
		textEdit()->verticalScrollBar()->setValue(doc->property("_vpos").toInt());
	}
	documentChanged(doc);
	doc->setDefaultFont(editorFont());
	updateLines();
}


void QCodeEdit::setTextCursor(const QTextCursor & c) {
	ui->textCode->setTextCursor(c);
}


void QCodeEdit::centerCursor() {
	ui->textCode->centerCursor();
	updateLines();
}


void QCodeEdit::insertText(const QString & text) {
	ui->textCode->insertPlainText(text);
	updateLines();
}


void QCodeEdit::appendText(const QString & text) {
	ui->textCode->appendPlainText(text);
	updateLines();
}


void QCodeEdit::setCustomExtraSelection(const QList<QTextEdit::ExtraSelection> & es) {
	es_custom = es;
	applyExtraSelection();
}


QRect QCodeEdit::cursorRect() const {
	return ui->textCode->cursorRect();
}


QRect QCodeEdit::cursorRect(const QTextCursor & cursor) const {
	return ui->textCode->cursorRect(cursor);
}


QString QCodeEdit::text() const {
	return ui->textCode->toPlainText();
}


QStringList QCodeEdit::cursorScope() const {
	return cursor_scope;
}


bool QCodeEdit::showLineNumbers() const {
	return ui->textLines->isVisible();
}


void QCodeEdit::setHelpHintVisible(bool on) {
	help_visible = on;
}


bool QCodeEdit::isHelpHintVisible() const {
	return help_visible;
}


QString QCodeEdit::commentText() const {
	return comment_text;
}


void QCodeEdit::setCommentText(QString t) {
	comment_text = t;
}


void QCodeEdit::setEditorFont(QFont f) {
	ui->textCode->setFont(f);
	ui->textLines->setFont(f);
}


QFont QCodeEdit::editorFont() const {
	return ui->textCode->font();
}


QPlainTextEdit * QCodeEdit::textEdit() const {
	return ui->textCode;
}


void QCodeEdit::registerAutoCompletitionClass(int id, QCodeEdit::ACClassType ac_class, const QString & name, const QIcon & icon) {
	ac_classes[id] = ACClass(id, ac_class, name, icon);
}


int QCodeEdit::skipRange(const QString & s, int pos, QChar oc, QChar cc, QChar sc) {
	int cnt   = 0;
	bool skip = false;
	for (int i = pos - 1; i >= 0; --i) {
		QChar c = s[i];
		if (skip) {
			skip = false;
			continue;
		}
		if (c == sc) {
			skip = true;
			continue;
		}
		if (c == cc) {
			cnt++;
			continue;
		}
		if (c == oc) {
			cnt--;
			if (cnt == 0) return i;
		}
	}
	return -1;
}


int QCodeEdit::skipCWord(const QString & s, int pos) {
	QChar pc(0), c(0);
	for (int i = pos - 1; i >= 0; --i) {
		pc = c;
		c  = s[i];
		if (c.isLetterOrNumber() || (c.toLatin1() == '_')) continue;
		if (pc.isLetter() || (pc.toLatin1() == '_')) return i + 1;
		return -1;
	}
	return 0;
}


bool QCodeEdit::matchWritten(QString s, QString w) {
	if (s.isEmpty() || w.isEmpty()) return true;
	if (s.startsWith(w, Qt::CaseInsensitive)) return true;
	int sp(0);
	for (int i = 0; i < w.size(); ++i, ++sp) {
		if (sp >= s.size()) return false;
		QChar wc(w[i].toLower());
		bool ns = false, bl = true;
		while (sp < s.size()) {
			if (ns || s[sp].toLatin1() == '_') {
				if (s[sp].toLatin1() == '_') {
					sp++;
					bl = false;
					continue;
				}
				if (s[sp].isLower() && bl) {
					sp++;
					continue;
				}
				if (s[sp].toLower() != wc) return false;
			}
			if (s[sp].toLower() == wc) break;
			ns = true;
			sp++;
		}
		if (sp >= s.size()) return false;
	}
	return true;
}


QChar QCodeEdit::pairChar(QChar c) {
	switch (c.toLatin1()) {
	case '\"': return '\"';
	case '(': return ')';
	case ')': return '(';
	case '[': return ']';
	case ']': return '[';
	default: break;
	}
	return QChar();
}


bool QCodeEdit::eventFilter(QObject * o, QEvent * e) {
	if (_destructor) return QWidget::eventFilter(o, e);
	if (e->type() == QEvent::Destroy) {
		completer->removeEventFilter(this);
		ui->textCode->removeEventFilter(this);
		ui->textCode->viewport()->removeEventFilter(this);
		ui->textLines->viewport()->removeEventFilter(this);
		return QWidget::eventFilter(o, e);
	}
	if (ui->textLines) {
		if (o == ui->textLines->viewport()) {
			QTextCursor tc;
			int tcpos = 0;
			switch (e->type()) {
			case QEvent::MouseButtonPress:
				if (!isEnabled()) {
					break;
				}
				tc = ui->textCode->cursorForPosition(static_cast<QMouseEvent *>(e)->pos());
				tc.movePosition(QTextCursor::EndOfLine);
				pos_el_press = tc.anchor();
				tc.movePosition(QTextCursor::StartOfLine);
				pos_press = tc.anchor();
				if (!tc.movePosition(QTextCursor::Down, QTextCursor::KeepAnchor)) {
					tc.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
				}
				ui->textCode->setFocus();
				ui->textCode->setTextCursor(tc);
				return true;
			case QEvent::MouseMove:
				if (!isEnabled()) break;
				tc = ui->textCode->cursorForPosition(static_cast<QMouseEvent *>(e)->pos());
				tc.movePosition(QTextCursor::StartOfLine);
				if (pos_press == tc.anchor()) {
					if (!tc.movePosition(QTextCursor::Down, QTextCursor::KeepAnchor)) {
						tc.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
					}
					ui->textCode->setTextCursor(tc);
					return true;
				}
				if (pos_press < tc.anchor()) {
					if (!tc.movePosition(QTextCursor::Down, QTextCursor::KeepAnchor)) {
						tc.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
					}
				}
				tcpos = tc.position();
				tc.setPosition(pos_press < tc.anchor() ? pos_press : pos_el_press);
				tc.setPosition(tcpos, QTextCursor::KeepAnchor);
				ui->textCode->setTextCursor(tc);
				return true;
			case QEvent::Wheel:
				if (!isEnabled()) break;
				QApplication::sendEvent(ui->textCode->viewport(), e);
				return true;
			default: break;
			}
		}
	}
	if (o == completer) {
		// qDebug() << o << e;
		if (e->type() == QEvent::WindowActivate) _ignore_focus_out = true;
		// qDebug() << e;
		return QWidget::eventFilter(o, e);
	}
	if (o == ui->comboSearch || o == ui->comboReplace) {
		// qDebug() << o << e;
		if (e->type() == QEvent::KeyPress) {
			if (static_cast<QKeyEvent *>(e)->key() == Qt::Key_Escape) {
				hideHelp();
				if (completer->isVisible())
					completer->hide();
				else
					hideSearch();
			}
		}
		// qDebug() << e;
		return QWidget::eventFilter(o, e);
	}
	if (ui->textCode) {
		if (o == ui->textCode->viewport()) {
			switch (e->type()) {
			case QEvent::MouseButtonPress: {
				cancelBlockSelection();
				completer->hide();
				hideHelp();
				auto me = static_cast<QMouseEvent *>(e);
				if (me->modifiers().testFlag(Qt::ControlModifier) && (me->button() == Qt::LeftButton))
					if (!hasBlockSelection() && !ui->textCode->textCursor().hasSelection()) gotoLink();
			} break;
			case QEvent::MouseMove: {
				if (!completer->isHidden()) break;
				auto me = static_cast<QMouseEvent *>(e);
				if (me->buttons() != 0) switchBlockSelection();
				if (me->modifiers().testFlag(Qt::ControlModifier))
					if (!hasBlockSelection() && !ui->textCode->textCursor().hasSelection()) showLink();
			} break;
			case QEvent::Paint: resizeOverlay(); break;
			case QEvent::DragMove:
				if (!isEnabled()) break;
				drag_cursor = ui->textCode->cursorForPosition(
#if QT_VERSION_MAJOR <= 5
					static_cast<QDragMoveEvent *>(e)->pos()
#else
					static_cast<QDragMoveEvent *>(e)->position().toPoint()
#endif
				);
				repaintCursor();
				break;
			case QEvent::MouseButtonRelease:
			case QEvent::DragLeave:
			case QEvent::Drop: cancelDragCursor(); break;
			default: break;
			}
			return QWidget::eventFilter(o, e);
		}
		if (o == ui->textCode) {
			// qDebug() << e;
			QMetaObject::invokeMethod(this, "syncScrolls", Qt::QueuedConnection);
			switch (e->type()) {
			case QEvent::ToolTip:
				if (completer->isHidden()) {
					QTextCursor tc = ui->textCode->cursorForPosition(static_cast<QHelpEvent *>(e)->pos());
					tc.select(QTextCursor::WordUnderCursor);
					raiseHelp(tc);
				}
				break;
			case QEvent::KeyPress:
				// qDebug() << "key" << ke;
				if (codeKeyEvent(static_cast<QKeyEvent *>(e))) return true;
				break;
			case QEvent::KeyRelease: hideLink(); break;
			case QEvent::FocusOut:
				if (_ignore_focus_out) {
					_ignore_focus_out = false;
					break;
				}
			case QEvent::FocusIn: createBlockSelection(); break;
			case QEvent::Hide:
			case QEvent::HideToParent: hideLink();
			case QEvent::MouseButtonPress:
				// qDebug() << e;
				completer->hide();
				hideHelp();
			default: break;
			}
		}
	}
	return QWidget::eventFilter(o, e);
}


void QCodeEdit::showEvent(QShowEvent *) {
	detectTheme();
	if (!_first) return;
	_first = false;
	completer->installEventFilter(this);
	ui->textCode->installEventFilter(this);
	ui->textCode->viewport()->installEventFilter(this);
	ui->textLines->viewport()->installEventFilter(this);
}


void QCodeEdit::timerEvent(QTimerEvent * e) {
	if (e->timerId() == timer_parse) {
		parse();
		emit parseRequest();
		killTimer(timer_parse);
		timer_parse = 0;
	}
	if (e->timerId() == timer_blink) {
		cursor_state = !cursor_state;
		repaintCursor();
	}
}


void QCodeEdit::leaveEvent(QEvent * e) {
	hideHelp();
	QWidget::leaveEvent(e);
}


void QCodeEdit::changeEvent(QEvent * e) {
	QWidget::changeEvent(e);
	switch (e->type()) {
	case QEvent::LanguageChange:
		ui->retranslateUi(this);
		lbl_help[lhF1]->setText(tr("Press F1 for details"));
		break;
	case QEvent::PaletteChange: detectTheme(); break;
	default: break;
	}
}


bool QCodeEdit::codeKeyEvent(QKeyEvent * ke) {
	QChar kc;
	switch (ke->key()) {
	case Qt::Key_Space:
		if (ke->modifiers().testFlag(Qt::ControlModifier)) {
			invokeAutoCompletition(true);
			return true;
		}
		break;
	case Qt::Key_Slash:
		if (ke->modifiers().testFlag(Qt::ControlModifier)) {
			toggleComment();
			return true;
		}
		break;
	case Qt::Key_Escape:
		hideHelp();
		if (completer->isVisible()) {
			completer->hide();
			return true;
		}
		if (hasBlockSelection()) {
			cancelBlockSelection();
			return true;
		}
		if (ui->widgetSearch->isVisible()) {
			hideSearch();
			return true;
		}
		break;
	case Qt::Key_Up:
		switchBlockSelection(ke);
		if (completer->isVisible()) {
			completer->previousCompletition();
			return true;
		}
		completer->hide();
		hideHelp();
		if (ke->modifiers().testFlag(Qt::AltModifier)) {
			if (ke->modifiers().testFlag(Qt::ShiftModifier)) {
				return false;
			} else {
				copyLineUp();
			}
			return true;
		}
		if (ke->modifiers().testFlag(Qt::ControlModifier) && ke->modifiers().testFlag(Qt::ShiftModifier)) {
			moveLineUp();
			return true;
		}
		break;
	case Qt::Key_Down:
		switchBlockSelection(ke);
		if (completer->isVisible()) {
			completer->nextCompletition();
			return true;
		}
		completer->hide();
		hideHelp();
		if (ke->modifiers().testFlag(Qt::AltModifier)) {
			if (ke->modifiers().testFlag(Qt::ShiftModifier)) {
				return false;
			} else {
				copyLineDown();
			}
			return true;
		}
		if (ke->modifiers().testFlag(Qt::ControlModifier) && ke->modifiers().testFlag(Qt::ShiftModifier)) {
			moveLineDown();
			return true;
		}
		break;
	case Qt::Key_Home:
	case Qt::Key_End:
	case Qt::Key_PageUp:
	case Qt::Key_PageDown:
		switchBlockSelection(ke);
		if (completer->isVisible()) {
			qApp->sendEvent(completer, new QKeyEvent(ke->type(), ke->key(), ke->modifiers()));
			return true;
		}
		break;
	case Qt::Key_Left:
	case Qt::Key_Right:
		switchBlockSelection(ke);
		if (hasBlockSelection()) {
			break;
		}
	case Qt::Key_Backspace:
	case Qt::Key_Delete:
		if (completer->isVisible()) {
			QMetaObject::invokeMethod(this, "invokeAutoCompletition", Qt::QueuedConnection, Q_ARG(bool, false));
		}
		if (removeBlockSelection(ke->key() == Qt::Key_Delete)) {
			return true;
		}
		break;
	case Qt::Key_Return:
		if (hasBlockSelection()) {
			cancelBlockSelection();
			return true;
		}
		if (completer->isVisible()) {
			commitCompletition();
			completer->hide();
			return true;
		}
		if (ui->textCode->textCursor().selectedText().isEmpty()) {
			QMetaObject::invokeMethod(this, "autoIndent", Qt::QueuedConnection);
		}
		break;
	case Qt::Key_Tab:
		if (!ui->textCode->textCursor().selectedText().isEmpty()) {
			if (ke->modifiers().testFlag(Qt::ShiftModifier)) {
				deindent();
			} else {
				indent();
			}
			return true;
		}
		break;
	case Qt::Key_D:
		if (ke->modifiers().testFlag(Qt::ControlModifier)) {
			completer->hide();
			return true;
		}
		break;
	case Qt::Key_Control:
		if (!hasBlockSelection() && !ui->textCode->textCursor().hasSelection()) {
			showLink();
		}
		break;
	case Qt::Key_F1:
		if (widget_help->isVisible()) {
			gotoHelpHRef(help_entry);
		}
		break;
	default: break;
	}
	if (!ke->text().isEmpty()) {
		if (hasBlockSelection() &&
		    (ke->modifiers() == 0 || ke->modifiers() == Qt::ShiftModifier || ke->modifiers() == Qt::KeypadModifier)) {
			insertBlockSelection(ke->text());
			return true;
		}
		kc = ke->text()[0];
	}
	if (kc == '.') {
		completer->hide();
		QMetaObject::invokeMethod(this, "invokeAutoCompletition", Qt::QueuedConnection, Q_ARG(bool, false));
	} else {
		if ((kc.isLetterOrNumber() || kc.toLatin1() == '_') && completer->isVisible()) {
			QMetaObject::invokeMethod(this, "invokeAutoCompletition", Qt::QueuedConnection, Q_ARG(bool, false));
		}
	}
	return false;
}


void QCodeEdit::toggleComment() {
	QTextCursor tc = ui->textCode->textCursor();
	tc.beginEditBlock();
	int ss      = tc.selectionStart();
	int ss_     = ss;
	int se      = tc.selectionEnd();
	QString st_ = tc.selection().toPlainText();
	if (st_.endsWith("\n")) {
		st_.chop(1);
		se--;
	}
	int se_ = se;
	tc.setPosition(ss);
	tc.movePosition(QTextCursor::StartOfLine);
	ss = tc.position();
	tc.setPosition(se);
	tc.movePosition(QTextCursor::EndOfLine);
	se = tc.position();
	tc.setPosition(ss);
	bool need_comment = false;
	QMap<int, bool> comms;
	while (tc.position() <= se_) {
		int line = tc.blockNumber();
		tc.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, comment_text.size());
		if (line == tc.blockNumber()) {
			if (tc.selectedText() == comment_text) {
				comms[line] = true;
			} else {
				need_comment = true;
				comms[line]  = false;
			}
		} else {
			comms[line] = false;
		}
		tc.movePosition(QTextCursor::StartOfLine);
		if (!tc.movePosition(QTextCursor::Down)) {
			break;
		}
	}
	tc.setPosition(ss);
	bool first = true;
	while (tc.position() <= se_) {
		int line = tc.blockNumber();
		if (need_comment) {
			if (!comms[line]) {
				tc.insertText(comment_text);
				se_ += comment_text.size();
				if (first) ss_ += comment_text.size();
			}
		} else {
			if (comms[line]) {
				tc.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, comment_text.size());
				tc.removeSelectedText();
				se_ -= comment_text.size();
				if (first) ss_ -= comment_text.size();
			}
		}
		first = false;
		tc.movePosition(QTextCursor::StartOfLine);
		if (!tc.movePosition(QTextCursor::Down)) {
			break;
		}
	}
	tc.setPosition(ss_);
	tc.setPosition(se_, QTextCursor::KeepAnchor);
	ui->textCode->setTextCursor(tc);
	tc.endEditBlock();
}


char antiBracket(char c) {
	switch (c) {
	case '(': return ')';
	case '[': return ']';
	case '{': return '}';
	case '<': return '>';
	case ')': return '(';
	case ']': return '[';
	case '}': return '{';
	case '>': return '<';
	}
	return 0;
}


void QCodeEdit::highlightBrackets() {
	es_brackets.clear();
	QTextCursor stc = ui->textCode->textCursor(), tc;
	QTextEdit::ExtraSelection es;
	stc.setPosition(stc.position());
	QTextCursor::MoveOperation mop[2] = {QTextCursor::Left, QTextCursor::Right};
	QString mbr[2]                    = {")]}>", "([{<"};
	for (int d = 0; d < 2; ++d) {
		tc = stc;
		tc.movePosition(mop[d], QTextCursor::KeepAnchor);
		if (!tc.selectedText().isEmpty()) {
			char ch = tc.selectedText()[0].toLatin1();
			if (mbr[d].contains(ch)) {
				es        = es_bracket;
				es.cursor = tc;
				es_brackets << es;
				QTextCursor ftc = tc;
				int bcnt        = 1;
				char fch        = antiBracket(ch);
				while (bcnt > 0) {
					ftc.setPosition(ftc.position());
					if (!ftc.movePosition(mop[d], QTextCursor::KeepAnchor)) break;
					// qDebug() << tc.selectedText();
					if (ftc.selectedText().isEmpty()) break;
					if (ftc.selectedText()[0].toLatin1() == ch) ++bcnt;
					if (ftc.selectedText()[0].toLatin1() == fch) --bcnt;
				}
				if (bcnt == 0) {
					es.cursor = ftc;
					es_brackets << es;
					es.format = es_range.format;
					es.cursor.setPosition(tc.position(), QTextCursor::KeepAnchor);
					if (!es.cursor.selection().isEmpty()) {
						es_brackets << es;
					}
				}
			}
		}
	}
}


QColor inverseColorValue(const QColor & c) {
	int hsv[4];
	c.getHsl(&hsv[0], &hsv[1], &hsv[2], &hsv[3]);
	return QColor::fromHsl(hsv[0], hsv[1], 255 - hsv[2], hsv[3]);
}
QBrush QCodeEdit::invertedBrush(const QBrush & b) {
	auto ret = b;
	ret.setColor(inverseColorValue(b.color()));
	return ret;
}


QTextCharFormat QCodeEdit::invertedFormat(const QTextCharFormat & f) {
	auto ret = f;
	ret.setForeground(invertedBrush(ret.foreground()));
	ret.setBackground(invertedBrush(ret.background()));
	return ret;
}


void QCodeEdit::applyExtraSelection() {
	QList<QTextEdit::ExtraSelection> esl;
	esl << es_line << es_selected << es_custom << es_brackets << es_search_list << es_cursor << es_link;
	if (is_dark_theme) {
		for (auto & e: esl) {
			e.format = invertedFormat(e.format);
		}
	}
	esl << es_blockselection;
	ui->textCode->setExtraSelections(esl);
}


void QCodeEdit::clearSearch() {
	es_search_list.clear();
	applyExtraSelection();
}


void QCodeEdit::moveToSearch() {
	if (es_search_list.isEmpty()) {
		return;
	}
	if (cur_search_ind < 0) {
		cur_search_ind += es_search_list.size();
	}
	if (cur_search_ind >= es_search_list.size()) {
		cur_search_ind = 0;
	}
	if (cur_search_ind < 0 || cur_search_ind >= es_search_list.size()) {
		return;
	}
	ui->textCode->setTextCursor(es_search_list[cur_search_ind].cursor);
}


int QCodeEdit::searchIndFromCursor() {
	if (es_search_list.isEmpty()) return -1;
	int ci = ui->textCode->textCursor().anchor();
	for (int i = 0; i < es_search_list.size(); ++i) {
		if (es_search_list[i].cursor.anchor() > ci) {
			return i - 1;
		}
	}
	return -1;
}


QRect QCodeEdit::cursorRect() {
	QRect r = ui->textCode->cursorRect(textCursor()), lr = r;
	if (hasBlockSelection()) {
		r |= ui->textCode->cursorRect(block_start_cursor);
		lr.setY(r.y());
		lr.setHeight(r.height());
	}
	lr.setWidth(cursor_width);
	return lr;
}


QRect QCodeEdit::blockSelectionRect() {
	QTextCursor tc = ui->textCode->textCursor();
	QPoint ps(block_start_cursor.positionInBlock(), block_start_cursor.blockNumber());
	QPoint pe(tc.positionInBlock(), tc.blockNumber());
	return (ui->textCode->cursorRect(tc) | ui->textCode->cursorRect(block_start_cursor))
	    .translated(ui->textCode->horizontalScrollBar()->value(), 0);
}


QVector<QTextCursor> QCodeEdit::blockSelectionCursors(QRect bsr) {
	QVector<QTextCursor> ret;
	// qDebug() << bsr;
	int sline = ui->textCode->cursorForPosition(bsr.topLeft()).blockNumber();
	int eline = ui->textCode->cursorForPosition(bsr.bottomRight()).blockNumber();
	for (int l = sline; l <= eline; ++l) {
		QTextCursor stc(ui->textCode->document()->findBlockByNumber(l)), etc;
		QRect stc_rect = ui->textCode->cursorRect(stc);
		stc            = ui->textCode->cursorForPosition(stc_rect.center() + QPoint(bsr.left(), 0));
		etc            = ui->textCode->cursorForPosition(stc_rect.center() + QPoint(bsr.right(), 0));
		stc.setPosition(etc.position(), QTextCursor::KeepAnchor);
		ret << stc;
	}
	return ret;
}


void QCodeEdit::repaintCursor() {
	overlay->update(cursorRect());
}


void QCodeEdit::drawCursor() {
	if (!isEnabled() || !ui->textCode->hasFocus()) return;
	QPainter p(overlay);
	QTextCursor tc = textCursor();
	// qDebug() << block_start_cursor.position() << tc.position();
	QRect line     = cursorRect();
	if (cursor_state && ui->textCode->hasFocus()) {
		line.adjust(0, 1, 0, -1);
		p.setCompositionMode(QPainter::CompositionMode_Difference);
		p.fillRect(line, Qt::white);
	}
	if (!drag_cursor.isNull()) {
		line = ui->textCode->cursorRect(drag_cursor);
		line.setWidth(cursor_width);
		p.setCompositionMode(QPainter::CompositionMode_Difference);
		p.fillRect(line, Qt::white);
	}
}


bool QCodeEdit::hasBlockSelection() const {
	return !block_start_cursor.isNull();
}


void QCodeEdit::startBlockSelection() {
	if (!hasBlockSelection()) {
		QTextCursor tc     = textCursor();
		block_start_cursor = tc;
		block_start_cursor.setPosition(tc.selectionStart());
	}
}


void QCodeEdit::cancelBlockSelection() {
	block_start_cursor = QTextCursor();
	es_blockselection.clear();
	applyExtraSelection();
}


void QCodeEdit::switchBlockSelection(QKeyEvent * ke) {
	bool alt = QApplication::keyboardModifiers().testFlag(Qt::AltModifier);
	if (ke) {
		alt = ke->modifiers().testFlag(Qt::AltModifier) && ke->modifiers().testFlag(Qt::ShiftModifier);
	}
	if (alt) {
		startBlockSelection();
		QTextCursor tc                = ui->textCode->textCursor();
		QTextCursor::MoveOperation op = QTextCursor::NoMove;
		if (!ke) {
			return;
		}
		switch (ke->key()) {
		case Qt::Key_Left: op = QTextCursor::Left; break;
		case Qt::Key_Right: op = QTextCursor::Right; break;
		case Qt::Key_Up: op = QTextCursor::Up; break;
		case Qt::Key_Down: op = QTextCursor::Down; break;
		default: break;
		}
		if (op != QTextCursor::NoMove) {
			tc.movePosition(op);
			ui->textCode->setTextCursor(tc);
		}
		repaintCursor();
	} else {
		cancelBlockSelection();
	}
}


bool QCodeEdit::removeBlockSelection(bool is_del) {
	if (!hasBlockSelection()) {
		return false;
	}
	QRect bsr = blockSelectionRect();
	bool del  = false;
	bool back = false;
	if (bsr.width() <= 1) {
		if (is_del) {
			del = true;
		} else {
			back = true;
		}
	}
	QVector<QTextCursor> clist = blockSelectionCursors(bsr);
	QTextCursor tc             = ui->textCode->textCursor();
	tc.beginEditBlock();
	int bspx  = ui->textCode->cursorRect(block_start_cursor).center().x();
	int nullw = ui->textCode->fontMetrics().
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
	            horizontalAdvance
#else
	            width
#endif
	            (" ");
	int min_dist = nullw / 2;
	int new_pos  = -1;
	for (QTextCursor & c: clist) {
		if (del || back) {
			if (qAbs(bspx - ui->textCode->cursorRect(c).center().x()) > min_dist) {
				continue;
			}
			int line = c.blockNumber();
			c.movePosition(del ? QTextCursor::Right : QTextCursor::Left, QTextCursor::KeepAnchor);
			if (line != c.blockNumber()) {
				continue;
			}
			// qDebug() << qAbs(bspx - ui->textCode->cursorRect(c).center().x()) << min_dist;
		}
		c.removeSelectedText();
		if (c.blockNumber() == tc.blockNumber()) {
			new_pos = c.position();
		}
	}
	tc.setPosition(new_pos);
	ui->textCode->setTextCursor(tc);
	tc.endEditBlock();
	return true;
}


void QCodeEdit::insertBlockSelection(QString text) {
	if (!hasBlockSelection()) {
		return;
	}
	QTextCursor tc = ui->textCode->textCursor();
	QRect bsr      = blockSelectionRect();
	// qDebug() << "___" << bsr;
	int scrl       = ui->textCode->horizontalScrollBar()->value();
	if (bsr.width() > 1) {
		removeBlockSelection(false);
		bsr = blockSelectionRect();
		// qDebug() << "del" << bsr;
	}
	tc.beginEditBlock();
	int nullw = ui->textCode->fontMetrics().
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
	            horizontalAdvance
#else
	            width
#endif
	            (" ");
	QVector<QTextCursor> clist = blockSelectionCursors(bsr);
	for (QTextCursor & c: clist) {
		c.removeSelectedText();
		int spcnt = (bsr.left() - ui->textCode->cursorRect(c).center().x() - scrl) / nullw;
		if (spcnt > 0) {
			c.insertText(QString(spcnt, QChar(' ')));
		}
		c.insertText(text);
	}
	tc.endEditBlock();
}


void QCodeEdit::createBlockSelection() {
	if (!hasBlockSelection()) {
		return;
	}
	es_blockselection.clear();
	QTextEdit::ExtraSelection es;
	es.format.setForeground(palette().brush(QPalette::HighlightedText));
	es.format.setBackground(palette().brush(QPalette::Highlight));
	QVector<QTextCursor> clist = blockSelectionCursors(blockSelectionRect());
	for (QTextCursor & c: clist) {
		es.cursor = c;
		es_blockselection << es;
	}
	applyExtraSelection();
}


void QCodeEdit::cancelDragCursor() {
	drag_cursor = QTextCursor();
	overlay->update();
}


void QCodeEdit::searchAll() {
	QString st = ui->comboSearch->currentText();
	es_search_list.clear();
	if (!st.isEmpty() && !ui->widgetSearch->isHidden()) {
		QTextDocument::FindFlags ff = QTextDocument::FindFlags();
		if (ui->buttonSearchCase->isChecked()) ff |= QTextDocument::FindCaseSensitively;
		if (ui->buttonSearchWord->isChecked()) ff |= QTextDocument::FindWholeWords;
		QTextCursor tc(ui->textCode->document()->begin());
		QTextEdit::ExtraSelection es = es_search;
		while (true) {
			tc = ui->textCode->document()->find(st, tc, ff);
			if (tc.isNull()) {
				break;
			}
			es.cursor = tc;
			es_search_list << es;
		}
	}
	applyExtraSelection();
	QString ss;
	if (es_search_list.isEmpty()) {
		ss = "color: rgb(180, 0, 0);";
	}
	ui->comboSearch->lineEdit()->setStyleSheet(ss);
}


void QCodeEdit::search_triggered() {
	QTextCursor tc = ui->textCode->textCursor();
	QString st     = tc.selectedText();
	if (st.isEmpty()) {
		tc.select(QTextCursor::WordUnderCursor);
		st = tc.selectedText();
	}
	search(st);
	if (ui->comboSearch->findText(st) < 0) {
		ui->comboSearch->insertItem(0, st);
	}
}


void QCodeEdit::syncScrolls() {
	ui->textLines->verticalScrollBar()->setValue(ui->textCode->verticalScrollBar()->value());
	ui->textLines->setHorizontalScrollBarPolicy(ui->textCode->horizontalScrollBar()->isVisible() ? Qt::ScrollBarAlwaysOn
	                                                                                             : Qt::ScrollBarAlwaysOff);
}


void QCodeEdit::scrollUp() {
	ui->textCode->verticalScrollBar()->setValue(ui->textCode->verticalScrollBar()->value() - 1);
}


void QCodeEdit::scrollDown() {
	ui->textCode->verticalScrollBar()->setValue(ui->textCode->verticalScrollBar()->value() + 1);
}


void QCodeEdit::deleteLine() {
	QTextCursor tc = ui->textCode->textCursor();
	tc.beginEditBlock();
	tc.movePosition(QTextCursor::EndOfLine);
	tc.movePosition(QTextCursor::StartOfLine, QTextCursor::KeepAnchor);
	bool md = true;
	if (!tc.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor)) {
		tc.movePosition(QTextCursor::StartOfLine);
		tc.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
		tc.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
		md = false;
	}
	tc.removeSelectedText();
	tc.movePosition(QTextCursor::StartOfLine);
	if (md) tc.movePosition(QTextCursor::Down);
	ui->textCode->setTextCursor(tc);
	tc.endEditBlock();
}


void QCodeEdit::copyLineUp() {
	QTextCursor tc = ui->textCode->textCursor();
	tc.beginEditBlock();
	int ss      = tc.selectionStart();
	int ss_     = ss;
	int se      = tc.selectionEnd();
	QString st_ = tc.selection().toPlainText();
	if (st_.endsWith("\n")) {
		st_.chop(1);
		se--;
	}
	int se_ = se;
	tc.setPosition(ss);
	tc.movePosition(QTextCursor::StartOfLine);
	ss = tc.position();
	tc.setPosition(se);
	tc.movePosition(QTextCursor::EndOfLine);
	se = tc.position();
	tc.setPosition(ss);

	tc.setPosition(se, QTextCursor::KeepAnchor);
	bool ins_nl = false;
	if (!tc.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor)) {
		ins_nl = true;
	}
	QString l = tc.selectedText();
	tc.beginEditBlock();
	tc.setPosition(ss);
	if (ins_nl) {
		l.append("\n");
	}

	tc.insertText(l);
	tc.setPosition(ss_);
	tc.setPosition(se_, QTextCursor::KeepAnchor);
	tc.endEditBlock();
	ui->textCode->setTextCursor(tc);
	tc.endEditBlock();
}


void QCodeEdit::copyLineDown() {
	QTextCursor tc = ui->textCode->textCursor();
	tc.beginEditBlock();
	int ss      = tc.selectionStart();
	int ss_     = ss;
	int se      = tc.selectionEnd();
	QString st_ = tc.selection().toPlainText();
	if (st_.endsWith("\n")) {
		st_.chop(1);
		se--;
	}
	int se_ = se;
	tc.setPosition(ss);
	tc.movePosition(QTextCursor::StartOfLine);
	ss = tc.position();
	tc.setPosition(se);
	tc.movePosition(QTextCursor::EndOfLine);
	se = tc.position();
	tc.setPosition(ss);

	tc.setPosition(se, QTextCursor::KeepAnchor);
	bool ins_nl = false;
	if (!tc.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor)) {
		ins_nl = true;
	}
	QString l = tc.selectedText();
	tc.beginEditBlock();
	tc.setPosition(ss);
	ss_ += l.size();
	se_ += l.size();
	if (ins_nl) {
		l.append("\n");
		ss_++;
		se_++;
	}

	tc.insertText(l);
	tc.setPosition(ss_);
	tc.setPosition(se_, QTextCursor::KeepAnchor);
	tc.endEditBlock();
	ui->textCode->setTextCursor(tc);
	tc.endEditBlock();
}


void QCodeEdit::moveLineUp() {
	QTextCursor tc = ui->textCode->textCursor();
	int ss         = tc.selectionStart();
	int ss_        = ss;
	int se         = tc.selectionEnd();
	QString st_    = tc.selection().toPlainText();
	if (st_.endsWith("\n")) {
		st_.chop(1);
		se--;
	}
	int se_ = se;
	tc.setPosition(ss);
	tc.movePosition(QTextCursor::StartOfLine);
	ss = tc.position();
	tc.setPosition(se);
	tc.movePosition(QTextCursor::EndOfLine);
	se = tc.position();
	tc.setPosition(ss);

	if (!tc.movePosition(QTextCursor::Up)) {
		return;
	}
	tc.beginEditBlock();
	tc.movePosition(QTextCursor::Down, QTextCursor::KeepAnchor);
	QString l = tc.selectedText();
	ss -= l.size();
	se -= l.size();
	ss_ -= l.size();
	se_ -= l.size();
	tc.beginEditBlock();
	tc.removeSelectedText();
	tc.setPosition(se);
	bool de = false;
	if (!tc.movePosition(QTextCursor::Right)) {
		l.prepend("\n");
		de = true;
	}

	tc.insertText(l);
	if (de) {
		tc.movePosition(QTextCursor::End);
		tc.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor);
		tc.removeSelectedText();
	}
	tc.setPosition(ss_);
	tc.setPosition(se_, QTextCursor::KeepAnchor);
	tc.endEditBlock();
	ui->textCode->setTextCursor(tc);
	tc.endEditBlock();
}


void QCodeEdit::moveLineDown() {
	QTextCursor tc = ui->textCode->textCursor();
	int ss         = tc.selectionStart();
	int ss_        = ss;
	int se         = tc.selectionEnd();
	QString st_    = tc.selection().toPlainText();
	if (st_.endsWith("\n")) {
		st_.chop(1);
		se--;
	}
	int se_ = se;
	tc.setPosition(ss);
	tc.movePosition(QTextCursor::StartOfLine);
	ss = tc.position();
	tc.setPosition(se);
	tc.movePosition(QTextCursor::EndOfLine);
	se = tc.position();
	tc.setPosition(se);

	if (!tc.movePosition(QTextCursor::Right)) {
		return;
	}
	tc.beginEditBlock();
	bool de = false;
	if (!tc.movePosition(QTextCursor::Down, QTextCursor::KeepAnchor)) {
		tc.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
		de = true;
	}
	QString l = tc.selectedText();
	tc.beginEditBlock();
	tc.removeSelectedText();
	tc.setPosition(ss);
	if (de) {
		l += "\n";
	}
	tc.insertText(l);
	if (de) {
		tc.movePosition(QTextCursor::End);
		tc.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor);
		tc.removeSelectedText();
	}
	ss += l.size();
	se += l.size();
	ss_ += l.size();
	se_ += l.size();
	tc.setPosition(ss_);
	tc.setPosition(se_, QTextCursor::KeepAnchor);
	tc.endEditBlock();
	ui->textCode->setTextCursor(tc);
	tc.endEditBlock();
}


void QCodeEdit::indent() {
	QTextCursor tc = ui->textCode->textCursor();
	tc.beginEditBlock();
	int ss      = tc.selectionStart();
	int ss_     = ss;
	int se      = tc.selectionEnd();
	QString st_ = tc.selection().toPlainText();
	if (st_.endsWith("\n")) {
		st_.chop(1);
		se--;
	}
	int se_ = se;
	tc.setPosition(ss);
	tc.movePosition(QTextCursor::StartOfLine);
	ss = tc.position();
	tc.setPosition(se);
	tc.movePosition(QTextCursor::EndOfLine);
	se = tc.position();
	tc.setPosition(ss);
	while (tc.position() < se_) {
		tc.insertText("\t");
		se_++;
		tc.movePosition(QTextCursor::StartOfLine);
		if (!tc.movePosition(QTextCursor::Down)) {
			break;
		}
	}
	tc.setPosition(ss_ + 1);
	tc.setPosition(se_, QTextCursor::KeepAnchor);
	ui->textCode->setTextCursor(tc);
	tc.endEditBlock();
}


void QCodeEdit::deindent() {
	QTextCursor tc = ui->textCode->textCursor();
	tc.beginEditBlock();
	int ss      = tc.selectionStart();
	int ss_     = ss;
	int se      = tc.selectionEnd();
	QString st_ = tc.selection().toPlainText();
	if (st_.endsWith("\n")) {
		st_.chop(1);
		se--;
	}
	int se_ = se;
	tc.setPosition(ss);
	tc.movePosition(QTextCursor::StartOfLine);
	ss = tc.position();
	tc.setPosition(se);
	tc.movePosition(QTextCursor::EndOfLine);
	se = tc.position();
	tc.setPosition(ss);
	bool first = true;
	while (tc.position() < se_) {
		tc.movePosition(QTextCursor::StartOfLine);
		tc.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
		int rs = 0;
		if (tc.selectedText() == "\t") {
			tc.removeSelectedText();
			rs = 1;
		} else {
			for (int i = 0; i < 4; ++i) {
				tc.movePosition(QTextCursor::StartOfLine);
				tc.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
				if (tc.selectedText() == " ") {
					tc.removeSelectedText();
					rs++;
				}
			}
		}
		if (first) {
			first = false;
			ss_ -= rs;
		}
		se_ -= rs;
		tc.movePosition(QTextCursor::StartOfLine);
		if (!tc.movePosition(QTextCursor::Down)) break;
	}
	tc.setPosition(ss_);
	tc.setPosition(se_, QTextCursor::KeepAnchor);
	ui->textCode->setTextCursor(tc);
	tc.endEditBlock();
}


void QCodeEdit::autoIndent() {
	QTextCursor tc  = ui->textCode->textCursor();
	QTextCursor stc = tc;
	tc.movePosition(QTextCursor::StartOfLine);
	if (!tc.movePosition(QTextCursor::Up)) {
		return;
	}
	tc.movePosition(QTextCursor::Down, QTextCursor::KeepAnchor);
	QString line = tc.selectedText();
	QString tabs;
	int i = 0;
	for (; i < line.size(); ++i)
		if (!line[i].isSpace()) {
			tabs = line.left(i);
			break;
		}
	if (i >= line.size()) {
		tabs = line.left(line.size() - 1);
	}
	int nt = qMax<int>(0, line.count(QChar('{')) - line.count(QChar('}')));
	tabs.append(QString("\t").repeated(nt));
	if (tabs.isEmpty()) {
		return;
	}
	stc.beginEditBlock();
	stc.insertText(tabs);
	ui->textCode->setTextCursor(stc);
	stc.endEditBlock();
}


void QCodeEdit::showLink() {
	QTextCursor tc = ui->textCode->cursorForPosition(ui->textCode->mapFromGlobal(QCursor::pos()));
	tc.select(QTextCursor::WordUnderCursor);
	link_entry = findEntryOnCursor(tc);
	if (link_entry.isNull()) {
		hideLink();
		return;
	}
	if (!linkValidate(link_entry)) {
		hideLink();
		return;
	}
	es_link.cursor = tc;
	overlay->setCursor(tc.isNull() ? Qt::IBeamCursor : Qt::PointingHandCursor);
	applyExtraSelection();
	// qDebug() << "showLink" << tc.selectedText() << link_entry.type << link_entry.name;
}


void QCodeEdit::hideLink() {
	es_link.cursor = QTextCursor();
	overlay->setCursor(Qt::IBeamCursor);
	link_entry = ACEntry();
	applyExtraSelection();
	// qDebug() << "hideLink";
}


void QCodeEdit::gotoLink() {
	if (link_entry.isNull()) return;
	QMetaObject::invokeMethod(this, "_activateLink", Qt::QueuedConnection, Q_ARG(QCodeEdit::ACEntry, link_entry));
	hideLink();
}


void QCodeEdit::scrollToTop() {
	prev_lc = -1;
	updateLines();
	ui->textCode->verticalScrollBar()->setValue(0);
	ui->textLines->verticalScrollBar()->setValue(0);
}


void QCodeEdit::newLine() {
	QTextCursor tc = ui->textCode->textCursor();
	tc.movePosition(QTextCursor::EndOfLine);
	tc.insertText("\n");
	ui->textCode->setTextCursor(tc);
	QMetaObject::invokeMethod(this, "autoIndent", Qt::QueuedConnection);
}


void QCodeEdit::newLineBefore() {
	QTextCursor tc = ui->textCode->textCursor();
	tc.movePosition(QTextCursor::StartOfLine);
	tc.insertText("\n");
	tc.movePosition(QTextCursor::Up);
	ui->textCode->setTextCursor(tc);
	QMetaObject::invokeMethod(this, "autoIndent", Qt::QueuedConnection);
}


void QCodeEdit::setFocus() {
	ui->textCode->setFocus();
}


void QCodeEdit::setText(const QString & t) {
	ui->textCode->setPlainText(t);
}


void QCodeEdit::updateLines() {
	if (timer_parse > 0) {
		killTimer(timer_parse);
	}
	timer_parse = startTimer(500);
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
#  if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
	ui->textCode->setTabStopDistance(ui->textCode->fontMetrics().horizontalAdvance("    "));
#  else
	ui->textCode->setTabStopDistance(ui->textCode->fontMetrics().width("    "));
#  endif
#else
	ui->textCode->setTabStopWidth(ui->textCode->fontMetrics().width("    "));
#endif
	int lc = ui->textCode->document()->lineCount();
	if (prev_lc == lc) {
		return;
	}
	prev_lc = lc;
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
	ui->textLines->setFixedWidth(ui->textLines->fontMetrics().horizontalAdvance(QString(" %1").arg(lc)));
#else
	ui->textLines->setFixedWidth(ui->textLines->fontMetrics().width(QString(" %1").arg(lc)));
#endif
	ui->textLines->clear();
	for (int i = 1; i <= lc; ++i) {
		ui->textLines->appendPlainText(QString("%1").arg(i));
	}
	ui->textLines->verticalScrollBar()->setValue(ui->textCode->verticalScrollBar()->value());
}


QString QCodeEdit::selectArg(QString s, int arg) {
	if (!s.contains('(') || arg < 0) {
		return s;
	}
	QString ss = s.left(s.indexOf('('));
	s.remove(0, ss.size());
	if (s.startsWith('(')) {
		s.remove(0, 1);
	}
	if (s.endsWith(')')) {
		s.chop(1);
	}
	QStringList al = s.split(",");
	QString ret    = ss + "(";
	for (int i = 0; i < al.size(); ++i) {
		if (i > 0) {
			ret += ", ";
		}
		if (i == arg) {
			ret += "<span style=\"font-weight:600;\">";
		}
		ret += al[i].trimmed();
		if (i == arg) {
			ret += "</span>";
		}
	}
	ret += ")";
	return ret;
}


QCodeEdit::ACEntry QCodeEdit::findEntryOnCursor(QTextCursor tc, int arg, ACClass * acc, QPair<QStringList, QString> * scope) {
	bool ok                        = false;
	QPair<QStringList, QString> sc = getScope(tc, &ok);
	if (scope) {
		*scope = sc;
	}
	QString st = tc.selectedText();
	if (arg >= 0) {
		st = sc.second;
	}
	if (!ok || st.isEmpty()) {
		return ACEntry();
	}
	ok = false;
	ACList acl(autoCompletitionList(sc.first, sc.second));
	for (const ACSection & i: acl) {
		for (const ACEntry & s: i.second) {
			QString ts = s.name;
			// qDebug() << ts << st;
			if (ts != st) {
				if (ts.startsWith(st)) {
					ts.remove(0, st.size());
					ts = ts.trimmed();
					if (!ts.isEmpty()) {
						if (ts[0] != '(') {
							continue;
						}
					}
				} else {
					continue;
				}
			}
			// qDebug() << s.second << st;
			if (acc) {
				*acc = ac_classes.value(i.first);
			}
			return s;
		}
	}
	return ACEntry();
}


void QCodeEdit::detectTheme() {
	is_dark_theme = palette().color(QPalette::Window).valueF() < 0.5;
}


void QCodeEdit::raiseHelp(QTextCursor tc, int arg) {
	ACClass acc;
	QPair<QStringList, QString> scope;
	ACEntry e = findEntryOnCursor(tc, arg, &acc, &scope);
	if (e.isNull()) {
		hideHelp();
		return;
	}
	lbl_help[lhMain]->setText(QString("<html><body>[%1]  %2  %3</html></body>").arg(acc.name, e.type, selectArg(e.name, arg)));
	lbl_help[lhMain]->setIcon(acc.icon);
	QString hint;
	if (!e.hint.isEmpty()) {
		hint = QString("<span style=\"font-style:italic;\">%1</span>").arg(e.hint);
	}
	lbl_help[lhHint]->setText(hint);
	lbl_help[lhHint]->setHidden(hint.isEmpty());
	// qDebug() << "help found" << tc.selectionStart() << tc.selectionEnd();
	es_cursor.cursor = tc;
	applyExtraSelection();
	// tc.movePosition(QTextCursor::StartOfWord, QTextCursor::MoveAnchor);
	lbl_help[lhMain]->setFont(font());
	lbl_help[lhF1]->setVisible(!e.help_href.isEmpty() && help_visible);
	qApp->processEvents();
	widget_help->adjustSize();
	widget_help->resize(widget_help->sizeHint());
	qApp->processEvents();
	QRect whr = ui->textCode->cursorRect(tc);
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
	whr.setWidth(ui->textCode->fontMetrics().horizontalAdvance(tc.selectedText()));
#else
	whr.setWidth(ui->textCode->fontMetrics().width(tc.selectedText()));
#endif
	QPoint whp;
	whp.setX(whr.left() - whr.width() - (widget_help->width() - whr.width()) / 2);
	whp.setY(whr.top() - widget_help->height() - (fontHeight(this) / 3));
	// qDebug() << whr << whp << widget_help->width() << ", " << st;
	widget_help->move(ui->textCode->viewport()->mapToGlobal(whp));
	widget_help->show();
	widget_help->raise();
	help_entry   = e;
	cursor_scope = scope.first;
	cursor_scope << scope.second;
	// qDebug() << "tooltip" << st;
}


void QCodeEdit::gotoHelpHRef(QCodeEdit::ACEntry e) {
	if (e.help_href.isEmpty()) {
		return;
	}
	QDesktopServices::openUrl(e.help_href);
}


void QCodeEdit::resizeOverlay() {
	if (overlay) {
		overlay->setGeometry(ui->textCode->viewport()->geometry());
	}
}


void QCodeEdit::hideHelp() {
	help_entry = ACEntry();
	widget_help->hide();
	es_cursor.cursor = QTextCursor();
	cursor_scope.clear();
	applyExtraSelection();
}


QTextCursor QCodeEdit::functionStart(QTextCursor tc, int * arg) {
	QString doc = ui->textCode->toPlainText();
	int bcnt = 0, a = 0, i = -1;
	for (i = tc.position() - 1; i >= 0; --i) {
		if (doc[i] == ')') bcnt++;
		if (doc[i] == '(') {
			if (bcnt == 0) {
				break;
			} else {
				bcnt--;
			}
		}
		if (doc[i] == ',' && bcnt == 0) {
			a++;
		}
	}
	if (i < 0) {
		return QTextCursor();
	}
	if (arg) {
		*arg = a;
	}
	QTextCursor ret(ui->textCode->document());
	ret.setPosition(i);
	// qDebug() << "found" << i << a;
	return ret;
}


QCodeEdit::ACList QCodeEdit::wordsCompletitionList(const QString & written) const {
	QCodeEdit::ACList ret;
	if (!written.isEmpty()) {
		QTextCursor tc = QTextCursor(ui->textCode->document()->begin());
		QTextCursor stc;
		QStringList acwl;
		tc = QTextCursor(ui->textCode->document()->begin());
		while (true) {
			tc = ui->textCode->document()->find(written, tc);
			if (tc.isNull()) {
				break;
			}
			stc = tc;
			stc.movePosition(QTextCursor::Left);
			stc.select(QTextCursor::WordUnderCursor);
			if (!stc.selectedText().isEmpty() && stc.selectedText().trimmed() != written) {
				acwl << stc.selectedText();
			}
		}
		acwl.removeDuplicates();
		ACSection acl;
		acl.first = -1;
		for (const QString & s: acwl) {
			acl.second << ACEntry("", s);
		}
		ret << acl;
	}
	return ret;
}


QPair<QStringList, QString> QCodeEdit::getScope(QTextCursor tc, bool * ok) {
	QPair<QStringList, QString> ret;
	QTextCursor stc = tc;
	if (tc.isNull()) {
		completer->hide();
		if (ok) {
			*ok = false;
		}
		return ret;
	}
	int line = tc.block().firstLineNumber();
	if (completer->isVisible()) {
		if (auto_comp_pl != line) {
			completer->hide();
			auto_comp_pl = line;
			if (ok) {
				*ok = false;
			}
			return ret;
		}
	}
	QString doc  = ui->textCode->toPlainText();
	auto_comp_pl = line;
	completer->clear();
	int spos = tc.position(), cpos = spos;
	tc.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor);
	QStringList scope;
	QString written = tc.selectedText().trimmed();
	// qDebug() << "\n*** invokeAutoCompletition ***";
	if (written != "_" && !written.leftJustified(1)[0].isLetterOrNumber()) {
		written.clear();
	} else {
		cpos = skipCWord(doc, spos);
		if (cpos >= 0) {
			written = doc.mid(cpos, spos - cpos).trimmed();
		}
	}
	while (cpos >= 0) {
		cpos--;
		// qDebug() << "char =" << doc.mid(cpos, 1);
		if (doc.mid(cpos, 1) != ".") break;
		QChar c  = doc.mid(cpos - 1, 1).leftJustified(1)[0];
		int ppos = cpos;
		if (c == '\"' || c == ')' || c == ']') {
			cpos = skipRange(doc, cpos, pairChar(c), c, '\\');
			// qDebug() << "range" << cpos;
			if (cpos < 0) break;
		}
		int npos = skipCWord(doc, cpos);
		if (npos < 0) break;
		scope.push_front(doc.mid(npos, ppos - npos));
		cpos = npos;
	}
	ret.first  = scope;
	ret.second = written;
	if (ok) *ok = true;
	return ret;
}


void QCodeEdit::invokeAutoCompletition(bool force) {
	int arg         = -1;
	QTextCursor htc = functionStart(ui->textCode->textCursor(), &arg);
	if (!htc.isNull()) {
		// qDebug() << "raise";
		raiseHelp(htc, arg);
	}
	bool ok;
	QPair<QStringList, QString> scope = getScope(ui->textCode->textCursor(), &ok);
	if (!ok) return;
	ACList acl(autoCompletitionList(scope.first, scope.second));
	// qDebug() << written << scope << acl.size();
	if (scope.first.isEmpty() && scope.second.isEmpty() && !force) {
		completer->hide();
		hideHelp();
		return;
	}
	if (word_complete) {
		acl << wordsCompletitionList(scope.second);
	}
	QFont bf(font());
	bf.setBold(true);
	foreach(const ACSection & ac, acl) {
		if (ac.second.isEmpty()) {
			continue;
		}
		ACClass acc = ac_classes.value(ac.first);
		completer->addItems(bf, acc, ac);
	}
	completer->invoke(ui->textCode->mapToGlobal(ui->textCode->cursorRect().bottomRight()));
}


void QCodeEdit::commitCompletition() {
	if (completer->currentItem() == nullptr) return;
	if (!completer->currentItem()->flags().testFlag(Qt::ItemIsSelectable)) return;
	QString ins = completer->currentValue(), ret = completer->currentReturn();
	QTextCursor tc = ui->textCode->textCursor(), stc = tc;
	tc.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
	bool ins_br = true, shifted = false;
	if (!tc.selectedText().isEmpty()) {
		if (!tc.selectedText()[0].isLetterOrNumber() && !tc.selectedText()[0].isSpace() && !(tc.selectedText()[0] == '_')) {
			stc.movePosition(QTextCursor::Left);
			shifted = true;
		} else {
			tc.movePosition(QTextCursor::Left);
			tc.movePosition(QTextCursor::EndOfWord);
			tc.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
		}
		if (!tc.selectedText().isEmpty()) {
			if (tc.selectedText()[0].toLatin1() == '(') {
				ins_br = false;
			}
		}
	}
	if (ins.contains("(")) {
		ins = ins.left(ins.indexOf("(")) + "()";
	}
	if (!ins_br && ins.endsWith("()")) {
		ins.chop(2);
	}
	tc = stc;
	tc.select(QTextCursor::WordUnderCursor);
	if (!tc.selectedText().leftJustified(1)[0].isLetterOrNumber() && !(tc.selectedText().leftJustified(1)[0] == '_')) {
		tc = stc;
		if (shifted) {
			tc.movePosition(QTextCursor::Right);
		}
	}
	ui->textCode->setTextCursor(tc);
	ui->textCode->textCursor().insertText(ins);
	tc = ui->textCode->textCursor();
	if (ins_br) {
		if (ret == "void") {
			tc.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
			if (tc.selectedText() != ";") {
				ui->textCode->textCursor().insertText(";");
				tc.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, 2);
			}
		}
		if (ins.endsWith(")") && !completer->currentValue().endsWith("()")) {
			tc.movePosition(QTextCursor::Left);
			ui->textCode->setTextCursor(tc);
		}
	} else {
		if (completer->currentValue().endsWith(")")) {
			tc.movePosition(QTextCursor::Right);
			ui->textCode->setTextCursor(tc);
		}
		if (completer->currentValue().endsWith("()")) {
			tc.movePosition(QTextCursor::Right);
			ui->textCode->setTextCursor(tc);
		}
	}
	completer->hide();
}


void QCodeEdit::textEdit_cursorPositionChanged() {
	es_line.cursor = ui->textCode->textCursor();
	// qDebug() << "cursorPositionChanged" << es_line.cursor.position();
	es_line.cursor.select(QTextCursor::LineUnderCursor);
	es_line.cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
	highlightBrackets();
	applyExtraSelection();
	if (timer_blink) {
		killTimer(timer_blink);
	}
	timer_blink  = startTimer(QApplication::cursorFlashTime() / 2);
	cursor_state = true;
	createBlockSelection();
}


void QCodeEdit::textEdit_textChanged() {
	if (_replacing) {
		return;
	}
	searchAll();
	updateLines();
}


void QCodeEdit::textEdit_selectionChanged() {
	if (hasBlockSelection()) {
		QTextCursor tc = ui->textCode->textCursor();
		// qDebug() << block_start_cursor.selectionStart() << tc.selectionEnd();
		bool bs        = ui->textCode->blockSignals(true);
		tc.clearSelection();
		ui->textCode->setTextCursor(tc);
		ui->textCode->blockSignals(bs);
		applyExtraSelection();
		return;
	}
	block_start_cursor = QTextCursor();
	es_selected.clear();
	QString sf = ui->textCode->textCursor().selectedText();
	if (sf.trimmed().isEmpty() || sf.contains("\n")) {
		applyExtraSelection();
		return;
	}
	QTextCursor tc(ui->textCode->document()->begin());
	QTextEdit::ExtraSelection es;
	es.format.setBackground(QColor(251, 250, 150));
	while (true) {
		tc = ui->textCode->document()->find(sf, tc, QTextDocument::FindCaseSensitively | QTextDocument::FindWholeWords);
		if (tc.isNull()) break;
		es.cursor = tc;
		es_selected << es;
	}
	applyExtraSelection();
}


void QCodeEdit::textEdit_redoAvailable(bool available) {
	if (available) {
		cancelBlockSelection();
	}
}


void QCodeEdit::setShowSpaces(bool yes) {
	spaces_                = yes;
	QTextOption to         = ui->textCode->document()->defaultTextOption();
	QTextOption::Flags tof = to.flags();
	if (yes) {
		tof |= QTextOption::ShowTabsAndSpaces;
	} else {
		tof &= ~QTextOption::ShowTabsAndSpaces;
	}
	to.setFlags(tof);
	ui->textCode->document()->setDefaultTextOption(to);
}


void QCodeEdit::setShowLineNumbers(bool yes) {
	ui->textLines->setVisible(yes);
}


void QCodeEdit::search(const QString & t) {
	ui->widgetSearch->show();
	ui->comboSearch->setEditText(QString());
	ui->comboSearch->setEditText(t);
	ui->comboSearch->setFocus();
	searchNext(false);
}


void QCodeEdit::searchNext(bool next) {
	if (es_search_list.isEmpty()) {
		return;
	}
	cur_search_ind = searchIndFromCursor() + (next ? 1 : 0);
	moveToSearch();
}


void QCodeEdit::searchPrevious() {
	if (es_search_list.isEmpty()) {
		return;
	}
	cur_search_ind = searchIndFromCursor() - 1;
	moveToSearch();
}


void QCodeEdit::hideSearch() {
	ui->widgetSearch->hide();
	searchAll();
}


void QCodeEdit::on_comboSearch_currentTextChanged(const QString & t) {
	searchAll();
	searchNext(false);
}


void QCodeEdit::on_buttonReplace_clicked() {
	if (es_search_list.isEmpty() || cur_search_ind < 0 || cur_search_ind >= es_search_list.size()) {
		return;
	}
	if (ui->textCode->textCursor() != es_search_list[cur_search_ind].cursor) {
		return;
	}
	if (ui->textCode->textCursor().selectedText().size() != es_search_list[cur_search_ind].cursor.selectedText().size()) {
		return;
	}
	ui->textCode->textCursor().insertText(ui->comboReplace->currentText());
}


void QCodeEdit::on_buttonReplaceSearch_clicked() {
	on_buttonReplace_clicked();
	searchNext();
}


void QCodeEdit::on_buttonReplaceAll_clicked() {
	_replacing = true;
	QString rt = ui->comboReplace->currentText();
	textCursor().beginEditBlock();
	for (int i = es_search_list.size() - 1; i >= 0; --i) {
		es_search_list[i].cursor.insertText(rt);
	}
	_replacing = false;
	textCursor().endEditBlock();
	textEdit_textChanged();
}


QString QCodeEdit::ACEntry::declaration() const {
	if (declaration_pos < 0 || declaration_file.isEmpty()) {
		return QString();
	}
	return (declaration_file + ": %1").arg(declaration_pos);
}
