/* Part of SWI-Prolog interface to Qt Author: Carlo Capelli E-mail: cc.carlo.cap@gmail.com Copyright (c) 2013-2015, Carlo Capelli All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include "Swipl_IO.h" #include "do_events.h" #include "PREDICATE.h" #include "Completion.h" #include "Preferences.h" #include "pqMainWindow.h" #include "blockSig.h" #include "ParenMatching.h" #include #include #include //#include #include #include #include #include #include #include #include #include #include #include "ansi_esc_seq.h" /** peek color by index */ static QColor ANSI2col(int c, bool highlight = false) { return Preferences::ANSI2col(c, highlight); } /** can be disabled from ~/.plrc */ bool ConsoleEdit::color_term = true; /** build command line interface to SWI Prolog engine * this start the *primary* console */ ConsoleEdit::ConsoleEdit(int argc, char **argv, QWidget *parent) : ConsoleEditBase(parent), io(0) { // mandatory for QTextBrowser instances setReadOnly(false); qApp->setWindowIcon(QIcon(":/swipl.png")); qRegisterMetaType("pfunc"); setup(); eng = new SwiPrologEngine(this); // wire up console IO connect(eng, SIGNAL(user_output(QString)), this, SLOT(user_output(QString))); connect(eng, SIGNAL(user_prompt(int, bool)), this, SLOT(user_prompt(int, bool))); connect(this, SIGNAL(user_input(QString)), eng, SLOT(user_input(QString))); connect(eng, SIGNAL(finished()), this, SLOT(eng_completed())); // issue worker thread start eng->start(argc, argv); // reactive console connect(this, SIGNAL(anchorClicked(const QUrl &)), this, SLOT(anchorClicked(const QUrl &))); } void ConsoleEdit::add_thread(int id) { Q_ASSERT(id > 0); Q_ASSERT(thids.empty()); thids.append(id); } /** this start an *interactor* console hosted in a QMainWindow */ ConsoleEdit::ConsoleEdit(Swipl_IO* io, QString title) : ConsoleEditBase(), eng(0), io(io) { auto w = new QMainWindow(); w->setCentralWidget(this); w->setWindowTitle(title); w->show(); setup(io); } /** start an *interactor* console in tabbed interface * no MainWindow to attach this */ ConsoleEdit::ConsoleEdit(Swipl_IO* io) : ConsoleEditBase(), eng(0), io(io) { setup(io); } /** more factorization, after introducing the possibility * of instancing in a tabbed interface */ void ConsoleEdit::setup(Swipl_IO* io) { setup(); // wire up console IO connect(io, SIGNAL(user_output(QString)), this, SLOT(user_output(QString))); connect(io, SIGNAL(user_prompt(int, bool)), this, SLOT(user_prompt(int, bool))); connect(this, SIGNAL(user_input(QString)), io, SLOT(user_input(QString))); connect(io, SIGNAL(sig_eng_at_exit()), this, SLOT(eng_completed())); io->attached(this); } /** common setup between =main= and =thread= console * different setting required, due to difference in events handling */ void ConsoleEdit::setup() { status = idle; promptPosition = -1; qApp->installEventFilter(this); count_output = 0; update_refresh_rate = 100; preds = 0; Preferences p; /*/ preset presentation attributes output_text_fmt.setForeground(ANSI2col(p.console_out_fore)); output_text_fmt.setBackground(ANSI2col(p.console_out_back)); input_text_fmt.setForeground(ANSI2col(p.console_inp_fore)); input_text_fmt.setBackground(ANSI2col(p.console_inp_back)); */ set_colors(); setLineWrapMode(p.wrapMode); setFont(p.console_font); connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(onCursorPositionChanged())); connect(this, SIGNAL(sig_run_function(pfunc)), this, SLOT(run_function(pfunc))); // so far, connect(this, SIGNAL(selectionChanged()), this, SLOT(selectionChanged())); } /** set presentation attributes */ void ConsoleEdit::set_colors() { output_text_fmt.setForeground(ANSI2col(Preferences::console_out_fore)); output_text_fmt.setBackground(ANSI2col(Preferences::console_out_back)); input_text_fmt.setForeground(ANSI2col(Preferences::console_inp_fore)); input_text_fmt.setBackground(ANSI2col(Preferences::console_inp_back)); } /** strict control on keyboard events required */ void ConsoleEdit::keyPressEvent(QKeyEvent *event) { using namespace Qt; QTextCursor c = textCursor(); bool on_completion = preds && preds->popup()->isVisible(); if (on_completion) { // following keys are forwarded by the completer to the widget switch (event->key()) { case Key_Enter: case Key_Return: case Key_Escape: case Key_Tab: case Key_Backtab: event->ignore(); return; // let the completer do default behavior default: compinit(c); break; } } bool ctrl = event->modifiers() == CTRL; int cp = c.position(), k = event->key(); bool accept = true, ret = false, down = true, editable = cp >= fixedPosition; QString cmd; switch (k) { case Key_Space: if (!on_completion && ctrl && editable) { compinit2(c); return; } accept = editable; break; case Key_Tab: if (ctrl) { event->ignore(); // otherwise tab control get lost ! return; } if (!on_completion && !ctrl && editable) { compinit(c); return; } break; case Key_Backtab: // otherwise tab control get lost ! event->ignore(); return; case Key_Home: if (!ctrl && cp > fixedPosition) { c.setPosition(fixedPosition, (event->modifiers() & SHIFT) ? c.KeepAnchor : c.MoveAnchor); setTextCursor(c); return; } case Key_End: case Key_Left: case Key_Right: case Key_PageUp: case Key_PageDown: break; case Key_Return: ret = editable; if (ret) { c.movePosition(c.End); setTextCursor(c); } break; case Key_Backspace: accept = (cp > fixedPosition); break; case Key_Up: down = false; // fall through case Key_Down: if (!ctrl) { // naive history handler if (editable) { if (!history.empty()) { c.setPosition(fixedPosition); c.movePosition(c.End, c.KeepAnchor); auto repc = [&](QString t) { c.removeSelectedText(); if (color_term) c.insertText(t, input_text_fmt); else c.insertText(t); c.movePosition(c.End); ensureCursorVisible(); }; if (down) { if (history_next < history.count() - 1) repc(history[++history_next]); else if (history_next == history.count() - 1) { ++history_next; repc(history_spare); } } else { if (history_next == history.count()) { history_spare = c.selectedText(); repc(history[--history_next]); } else if (history_next > 0) repc(history[--history_next]); } return; } } event->ignore(); return; } c.movePosition(k == Key_Up ? c.Up : c.Down); setTextCursor(c); return; case Key_D: #ifdef Q_OS_DARWIN if ((accept = editable) && event->modifiers() == META) #else if ((accept = editable) && ctrl) #endif { qDebug() << "^D" << thids; c.movePosition(c.End); setTextCursor(c); ret = true; status = eof; } break; case Key_C: // case Key_Pause: I thought this one also work. It's not true. if (ctrl && status == running) { qDebug() << "^C" << thids << status; PL_thread_raise(thids[0], SIGINT); return; } // fall through default: accept = editable || event->matches(QKeySequence::Copy); } if (accept) { if (is_tty && c.atEnd()) { cmd = event->text(); if (!cmd.isEmpty()) goto _cmd_; } if (color_term) setCurrentCharFormat(input_text_fmt); ConsoleEditBase::keyPressEvent(event); if (on_completion) { c.select(QTextCursor::WordUnderCursor); preds->setCompletionPrefix(c.selectedText()); preds->popup()->setCurrentIndex(preds->completionModel()->index(0, 0)); } else { // handle ^A+Del (clear buffer) c.movePosition(c.End); if (fixedPosition > c.position()) fixedPosition = c.position(); } } if (ret) { c.setPosition(fixedPosition); c.movePosition(c.End, c.KeepAnchor); cmd = c.selectedText(); if (!cmd.isEmpty()) { cmd.replace(cmd.length() - 1, 1, '\n'); add_history_line(cmd.left(cmd.length() - 1)); } _cmd_: if (io) io->take_input(cmd); else emit user_input(cmd); if ( status != eof || !cmd.isEmpty() ) status = running; } } /** jump to source location on warning/error messages */ void ConsoleEdit::mousePressEvent(QMouseEvent *e) { QTextCursor c = cursorForPosition(e->pos()); clickable_message_line(c, false); ConsoleEditBase::mousePressEvent(e); } /** place accepted Completer selection in editor * very clean, after removing (useless?) customcompleter sample code */ void ConsoleEdit::insertCompletion(QString completion) { int sep = completion.indexOf(" | "); if (sep > 0) // remove description completion = completion.left(sep); int extra = completion.length() - preds->completionPrefix().length(); textCursor().insertText(completion.right(extra)); } /** completion initialize * this is the simpler setup I found so far */ void ConsoleEdit::compinit(QTextCursor c) { /*/ issue setof(M,current_module(M),L) QStringList lmods; { SwiPrologEngine::in_thread _it; PlTerm M, Ms; if (PlCall("setof", PlTermv(M, PlCompound("current_module", M), Ms))) for (PlTail x(Ms); x.next(M); ) lmods.append(CCP(M)); } */ QStringList lpreds; QString prefix = Completion::initialize(fixedPosition, c, lpreds); if (!preds) { preds = new t_Completion(new QStringListModel(lpreds)); preds->setWidget(this); connect(preds, SIGNAL(activated(QString)), this, SLOT(insertCompletion(QString))); } else { auto model = qobject_cast(preds->model()); model->setStringList(lpreds); } preds->setCompletionPrefix(prefix); preds->popup()->setCurrentIndex(preds->completionModel()->index(0, 0)); QRect cr = cursorRect(); cr.setWidth(300); preds->complete(cr); } void ConsoleEdit::compinit2(QTextCursor c) { QStringList atoms; QString prefix = Completion::initialize(fixedPosition, c, atoms); if (!preds) { preds = new t_Completion(new QStringListModel()); preds->setWidget(this); connect(preds, SIGNAL(activated(QString)), this, SLOT(insertCompletion(QString))); } QStringList lpreds; foreach (auto a, atoms) { auto p = Completion::pred_docs.constFind(a); if (p != Completion::pred_docs.constEnd()) // was pred_docs.end(), seems a Qt bug it's allowed foreach (auto d, p.value()) { QStringList la; for (int n = 0; n < d.first; ++n) la.append(QString(QChar('A' + n))); if (!la.isEmpty()) lpreds.append(QString("%1(%2) | %3").arg(a).arg(la.join(", ")).arg(d.second)); else lpreds.append(QString("%1 | %2").arg(a).arg(d.second)); } else lpreds.append(a); } auto model = qobject_cast(preds->model()); model->setStringList(lpreds); preds->setCompletionPrefix(prefix); preds->popup()->setCurrentIndex(preds->completionModel()->index(0, 0)); QRect cr = cursorRect(); cr.setWidth(400); preds->complete(cr); } /** handle focus event to keep QCompleter happy */ void ConsoleEdit::focusInEvent(QFocusEvent *e) { if (preds) preds->setWidget(this); ConsoleEditBase::focusInEvent(e); } /** filter out insertion when cursor is not in editable position */ void ConsoleEdit::insertFromMimeData(const QMimeData *source) { qDebug() << "insertFromMimeData" << source; auto c = textCursor(); if (c.position() >= fixedPosition) ConsoleEditBase::insertFromMimeData(source); else { c.movePosition(c.End); c.insertText(source->text()); } } /** \brief send text to output * * Decode ANSI terminal sequences, to output coloured text. * Colours encoding are (approx) derived from swipl console. */ void ConsoleEdit::user_output(QString text) { #if defined(Q_OS_WIN) text.replace("\r\n", "\n"); #endif QTextCursor c = textCursor(); if (status == wait_input) c.setPosition(promptPosition); else { promptPosition = c.position(); // save for later c.movePosition(QTextCursor::End); } auto instext = [&](QString text) { if (color_term) c.insertText(text, output_text_fmt); else c.insertText(text); if (status == wait_input) { int ltext = text.length(); promptPosition += ltext; fixedPosition += ltext; ensureCursorVisible(); } }; ANSI_ESC_SEQ filter(text, output_text_fmt); if (filter) while (filter) instext(filter.next()); else instext(text); #if 0 // filter and apply (some) ANSI sequence int pos = text.indexOf(0x1b); if (pos >= 0) { //QTextStream out(stdout); //out << text << endl; int left = 0; static QRegExp eseq("\x001b\\[(?:(3([0-7]);([01])m)|(0m)|(1m;)|1;3([0-7])m|(1m)|(?:3([0-7])m))"); forever { int pos1 = eseq.indexIn(text, pos); if (pos1 == -1) break; QStringList lcap = eseq.capturedTexts(); Q_ASSERT(lcap.length() == 9); // match captures in eseq, 0 seems unrelated to paren // put 'out-of-band' text with current attribute, before changing it instext(text.mid(left, pos1 - left)); // map sequence to text attributes QFont::Weight w; QBrush c; int skip = lcap[1].length(); if (skip) { QString A = lcap[2], B = lcap[3]; w = QFont::Normal; c = ANSI2col(B.toInt(), A == "1"); } else if (!lcap[6].isNull()) { skip = 5; w = QFont::Bold; c = ANSI2col(lcap[6].toInt()); } else if ((skip = lcap[7].length()) > 0) { w = QFont::Bold; c = ANSI2col(0); } else if (!lcap[8].isNull()) { skip = 3; w = QFont::Normal; c = ANSI2col(lcap[8].toInt()); } else { skip = lcap[4].length() + lcap[5].length(); w = QFont::Normal; c = ANSI2col(0); } output_text_fmt.setFontWeight(w); output_text_fmt.setForeground(c); left = pos = pos1 + skip + 2; // add the SCI } instext(text.mid(pos)); } else instext(text); #endif } bool ConsoleEdit::match_thread(int thread_id) const { return thread_id == -1 || thids.contains(thread_id); } /** issue an input request */ void ConsoleEdit::user_prompt(int threadId, bool tty) { Q_ASSERT(thids.contains(threadId)); is_tty = tty; Completion::setup(); QTextCursor c = textCursor(); c.movePosition(QTextCursor::End); fixedPosition = c.position(); setTextCursor(c); ensureCursorVisible(); status = wait_input; if (commands.count() > 0) QTimer::singleShot(1, this, SLOT(command_do())); } /** push command on queue */ bool ConsoleEdit::command(QString cmd) { commands.append(cmd); if (commands.count() == 1) QTimer::singleShot(1, this, SLOT(command_do())); return true; } /** push command from queue to Prolog processor */ void ConsoleEdit::command_do() { QString cmd = commands.takeFirst(); QTextCursor c = textCursor(); c.movePosition(QTextCursor::End); c.insertText(cmd); emit user_input(cmd); } /** handle tooltip from helpidx to display current cursor word synopsis */ bool ConsoleEdit::event(QEvent *event) { if (event->type() == QEvent::ToolTip) { QHelpEvent *helpEvent = static_cast(event); if (!last_tip.isEmpty()) QToolTip::showText(helpEvent->globalPos(), last_tip); else { QToolTip::hideText(); event->ignore(); } return true; } return ConsoleEditBase::event(event); } /** sense word under cursor for tooltip display */ bool ConsoleEdit::eventFilter(QObject *, QEvent *event) { if (event->type() == QEvent::MouseMove) { QTextCursor c = cursorForPosition(static_cast(event)->pos()); set_cursor_tip(c); clickable_message_line(c, true); } return false; } /** the user identifying label is attached somewhere to parents chain */ QString ConsoleEdit::titleLabel() { QString title; for (QWidget *p = parentWidget(); title.isEmpty() && p; p = p->parentWidget()) if (auto t = qobject_cast(p)) title = t->tabText(t->indexOf(this)); else title = p->windowTitle(); return title; } /** attempt to gracefully stop XPCE thread */ bool ConsoleEdit::can_close() { return true; } /** display different cursor where editing available */ void ConsoleEdit::onCursorPositionChanged() { QTextCursor c = textCursor(); set_cursor_tip(c); if (fixedPosition > c.position()) { viewport()->setCursor(Qt::OpenHandCursor); //setReadOnly(true); clickable_message_line(c, true); } else { setReadOnly(false); viewport()->setCursor(Qt::IBeamCursor); } if (pmatched.size()) { pmatched.format_both(c); pmatched = ParenMatching::range(); } ParenMatching pm(c); if (pm) (pmatched = pm.positions).format_both(c, pmatched.bold()); } /** check if line content is appropriate, then highlight or open editor on it */ #ifndef PQCONSOLE_HANDLE_HOOVERING void ConsoleEdit::clickable_message_line(QTextCursor c, bool highlight) { Q_UNUSED(c) Q_UNUSED(highlight) } #else void ConsoleEdit::clickable_message_line(QTextCursor c, bool highlight) { c.movePosition(c.StartOfLine); int cposition_ = c.position(); QTextCharFormat fposition_ = c.charFormat(); c.movePosition(c.EndOfLine, c.KeepAnchor); QString line = c.selectedText(); static QRegularExpression msg("(ERROR|Warning):[ \t]*(([a-zA-Z]:)?[^:]+):([0-9]+)(:([0-9]+))?.*", QRegularExpression::CaseInsensitiveOption); auto parts = msg.match(line); if (parts.hasMatch()) { if ( highlight ) { if (cposition != cposition_) { cposition = cposition_; fposition = fposition_; QTextCharFormat f = fposition_; f.setFontUnderline(true); c.setCharFormat(f); } return; } else { auto cmd = QString("edit('"+parts.captured(2)+"':"+parts.captured(4)); if ( !parts.captured(6).isEmpty() ) { cmd += ":"+parts.captured(6); } cmd += ")"; qDebug() << cmd; query_run(cmd); } } if (fposition != QTextCharFormat()) { c.setPosition(cposition); c.movePosition(c.EndOfLine, c.KeepAnchor); c.setCharFormat(fposition); fposition = QTextCharFormat(); cposition = -1; } } #endif /** setup tooltip info */ void ConsoleEdit::set_cursor_tip(QTextCursor c) { last_tip = Completion::pred_tip(c); if (!last_tip.isEmpty()) setToolTip(last_tip); } /** pass interrupt request to PlEngine */ void ConsoleEdit::int_request() { qDebug() << "int_request" << thids; if (!thids.empty()) PL_thread_raise(thids[0], SIGINT); } /** serve the user menu issuing the command */ void ConsoleEdit::onConsoleMenuAction() { auto a = qobject_cast(sender()); if (a) { QString action = a->toolTip(); onConsoleMenuActionMap(action); } } void ConsoleEdit::onConsoleMenuActionMap(QString action) { if (auto w = find_parent(this)) { if (ConsoleEdit *target = w->consoleActive()) { qDebug() << action << target->status << QTime::currentTime(); if (target->status == running) { { SwiPrologEngine::in_thread e; int t = PL_thread_self(); Q_ASSERT(!target->thids.contains(t)); target->thids.append(t); try { PL_set_prolog_flag("console_thread", PL_INTEGER, t); PlCall(action.toStdWString().data()); for (int c = 0; c < 100; c++) do_events(10); } catch(const PlException& e) { qDebug() << CCP(e); } target->thids.removeLast(); } return; } target->query_run("notrace("+action+")"); } } } /** remove all text */ void ConsoleEdit::tty_clear() { clear(); fixedPosition = promptPosition = 0; } /** issue instancing in GUI thread (cant moveToThread a Widget) */ void ConsoleEdit::new_console(Swipl_IO *io, QString title) { Q_ASSERT(io->target == 0); auto r = new req_new_console(io, title); QApplication::instance()->postEvent(this, r); } /** added to serve creation in thread * signal from foreign thread to Qt was not fired */ void ConsoleEdit::customEvent(QEvent *event) { Q_ASSERT(event->type() == QEvent::User); auto e = static_cast(event); // multi tabbed interface: pqMainWindow *mw = 0; for (QWidget *w = parentWidget(); w && !mw; w = w->parentWidget()) mw = qobject_cast(w); ConsoleEdit *nc; if (mw) mw->addConsole(nc = new ConsoleEdit(e->iop), e->title); else /* fire and forget :) auto ce = */ nc = new ConsoleEdit(e->iop, e->title); } /** store lines from swipl-win console protocol */ void ConsoleEdit::add_history_line(QString line) { if (history.isEmpty() || history.back() != line) history.append(line); history_next = history.count(); history_spare.clear(); } /** when engine gracefully complete-... */ void ConsoleEdit::eng_completed() { if (eng) { eng = 0; QApplication::postEvent(qApp, new QCloseEvent); } else if (io) { if (auto mw = find_parent(this)) mw->remConsole(this); } } /** dispatch execution to appropriate object */ void ConsoleEdit::query_run(QString call) { if (eng) eng->query_run(call); else if (io) io->query_run(call); } /** dispatch qualified execution to appropriate object */ void ConsoleEdit::query_run(QString module, QString call) { if (eng) eng->query_run(module, call); else if (io) query_run(module + ":" + call); } ConsoleEdit::exec_sync::exec_sync(int timeout_ms) { Q_UNUSED(timeout_ms); stop_ = QThread::currentThread(); go_ = 0; } void ConsoleEdit::exec_sync::stop() { Q_ASSERT(QThread::currentThread() == stop_); for ( ; ; ) { { QMutexLocker lk(&sync); if (go_) break; } SwiPrologEngine::msleep(10); } //Q_ASSERT(go_ && go_ != stop_); } void ConsoleEdit::exec_sync::go() { Q_ASSERT(go_ == 0); Q_ASSERT(stop_ != 0); auto t = QThread::currentThread(); if (stop_ != t) { QMutexLocker lk(&sync); go_ = t; } else go_ = t; } void ConsoleEdit::setSource(const QUrl &name) { qDebug() << "setSource" << name; } void ConsoleEdit::anchorClicked(const QUrl &url) { query_run(url.toString()); } void ConsoleEdit::html_write(QString html) { auto c = textCursor(); c.movePosition(c.End); c.insertHtml(html); } void ConsoleEdit::selectionChanged() { if (sel_check_timing.isValid()) { if (sel_check_timing.elapsed() < 100) return; sel_check_timing.restart(); } else sel_check_timing.start(); blockSig bs(this); foreach (ExtraSelection s, extraSelections()) s.cursor.setCharFormat(s.format); extraSelections().clear(); QTextCursor c = textCursor(); if (c.hasSelection()) { QString csel = c.selectedText(); QList lsel; QTextCharFormat bold = ParenMatching::range::bold(); QTextCursor cfirst = cursorForPosition(QPoint(0, 0)); if (!cfirst.isNull()) { while (c.block().position() > cfirst.block().position()) c.movePosition(c.Up); c.movePosition(c.Up); for ( ; ; ) { c = document()->find(csel, c, QTextDocument::FindCaseSensitively); if (c.isNull() || !c.block().isVisible()) break; lsel.append(ExtraSelection {c, c.blockCharFormat()}); c.setCharFormat(bold); } } setExtraSelections(lsel); } }