Mercurial > octave
view libgui/src/m-editor/octave-qscintilla.cc @ 32633:4b601ca024d5
maint: merge stable to default
author | John W. Eaton <jwe@octave.org> |
---|---|
date | Fri, 22 Dec 2023 12:14:08 -0500 |
parents | 094a296b3ba1 2e484f9f1f18 |
children | 489026b7257b |
line wrap: on
line source
//////////////////////////////////////////////////////////////////////// // // Copyright (C) 2013-2024 The Octave Project Developers // // See the file COPYRIGHT.md in the top-level directory of this // distribution or <https://octave.org/copyright/>. // // This file is part of Octave. // // Octave is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Octave is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Octave; see the file COPYING. If not, see // <https://www.gnu.org/licenses/>. // //////////////////////////////////////////////////////////////////////// #if defined (HAVE_CONFIG_H) # include "config.h" #endif #if defined (HAVE_QSCINTILLA) #include <Qsci/qscilexer.h> #include <QDir> #include <QKeySequence> #include <QMessageBox> #include <QMimeData> #include <QPointer> #include <QRegularExpression> #include <QTemporaryFile> #include <QToolTip> #include <QVBoxLayout> #if defined (HAVE_QSCI_QSCILEXEROCTAVE_H) # define HAVE_LEXER_OCTAVE 1 # include <Qsci/qscilexeroctave.h> #elif defined (HAVE_QSCI_QSCILEXERMATLAB_H) # define HAVE_LEXER_MATLAB 1 # include <Qsci/qscilexermatlab.h> #endif #include <Qsci/qscicommandset.h> #include <Qsci/qscilexerbash.h> #include <Qsci/qscilexerbatch.h> #include <Qsci/qscilexercpp.h> #include <Qsci/qscilexerdiff.h> #include <Qsci/qscilexerperl.h> #include "file-editor-tab.h" #include "gui-preferences-ed.h" #include "gui-settings.h" // FIXME: hardwired marker numbers? #include "marker.h" #include "octave-qscintilla.h" #include "workspace-model.h" #include "builtin-defun-decls.h" #include "cmd-edit.h" #include "interpreter-private.h" #include "interpreter.h" #include "oct-env.h" // Return true if CANDIDATE is a "closing" that matches OPENING, // such as "end" or "endif" for "if", or "catch" for "try". // Used for testing the last word of an "if" etc. line, // or the first word of the following line. OCTAVE_BEGIN_NAMESPACE(octave) static bool is_end (const QString& candidate, const QString& opening) { bool retval = false; if (opening == "do") // The only one that can't be ended by "end" { if (candidate == "until") retval = true; } else { if (candidate == "end") retval = true; else { if (opening == "try") { if (candidate == "catch" || candidate == "end_try_catch") retval = true; } else if (opening == "unwind_protect") { if (candidate == "unwind_protect_cleanup" || candidate == "end_unwind_protect") retval = true; } else if (candidate == "end" + opening) retval = true; else if (opening == "if" && candidate == "else") retval = true; } } return retval; } octave_qscintilla::octave_qscintilla (QWidget *p) : QsciScintilla (p), m_debug_mode (false), m_word_at_cursor (), m_selection (), m_selection_replacement (), m_selection_line (-1), m_selection_col (-1), m_indicator_id (1) { connect (this, SIGNAL (textChanged ()), this, SLOT (text_changed ())); connect (this, SIGNAL (cursorPositionChanged (int, int)), this, SLOT (cursor_position_changed (int, int))); connect (this, &octave_qscintilla::ctx_menu_run_finished_signal, this, &octave_qscintilla::ctx_menu_run_finished, Qt::QueuedConnection); // clear scintilla edit shortcuts that are handled by the editor QsciCommandSet *cmd_set = standardCommands (); // Disable buffered drawing on all systems SendScintilla (SCI_SETBUFFEREDDRAW, false); #if defined (HAVE_QSCI_VERSION_2_6_0) // find () was added in QScintilla 2.6 cmd_set->find (QsciCommand::SelectionCopy)->setKey (0); cmd_set->find (QsciCommand::SelectionCut)->setKey (0); cmd_set->find (QsciCommand::Paste)->setKey (0); cmd_set->find (QsciCommand::SelectAll)->setKey (0); cmd_set->find (QsciCommand::SelectionDuplicate)->setKey (0); cmd_set->find (QsciCommand::LineTranspose)->setKey (0); cmd_set->find (QsciCommand::Undo)->setKey (0); cmd_set->find (QsciCommand::Redo)->setKey (0); cmd_set->find (QsciCommand::SelectionUpperCase)->setKey (0); cmd_set->find (QsciCommand::SelectionLowerCase)->setKey (0); cmd_set->find (QsciCommand::ZoomIn)->setKey (0); cmd_set->find (QsciCommand::ZoomOut)->setKey (0); cmd_set->find (QsciCommand::DeleteWordLeft)->setKey (0); cmd_set->find (QsciCommand::DeleteWordRight)->setKey (0); cmd_set->find (QsciCommand::DeleteLineLeft)->setKey (0); cmd_set->find (QsciCommand::DeleteLineRight)->setKey (0); cmd_set->find (QsciCommand::LineDelete)->setKey (0); cmd_set->find (QsciCommand::LineCut)->setKey (0); cmd_set->find (QsciCommand::LineCopy)->setKey (0); #else // find commands via its default key (tricky way without find ()) QList< QsciCommand * > cmd_list = cmd_set->commands (); for (int i = 0; i < cmd_list.length (); i++) { int cmd_key = cmd_list.at (i)->key (); switch (cmd_key) { case Qt::Key_C | Qt::CTRL : // SelectionCopy case Qt::Key_X | Qt::CTRL : // SelectionCut case Qt::Key_V | Qt::CTRL : // Paste case Qt::Key_A | Qt::CTRL : // SelectAll case Qt::Key_D | Qt::CTRL : // SelectionDuplicate case Qt::Key_T | Qt::CTRL : // LineTranspose case Qt::Key_Z | Qt::CTRL : // Undo case Qt::Key_Y | Qt::CTRL : // Redo case Qt::Key_Z | Qt::CTRL | Qt::SHIFT : // Redo case Qt::Key_U | Qt::CTRL : // SelectionLowerCase case Qt::Key_U | Qt::CTRL | Qt::SHIFT : // SelectionUpperCase case Qt::Key_Plus | Qt::CTRL : // ZoomIn case Qt::Key_Minus | Qt::CTRL : // ZoomOut case Qt::Key_Backspace | Qt::CTRL | Qt::SHIFT : // DeleteLineLeft case Qt::Key_Delete | Qt::CTRL | Qt::SHIFT : // DeleteLineRight case Qt::Key_K | Qt::META : // DeleteLineRight case Qt::Key_Backspace | Qt::CTRL : // DeleteWordLeft case Qt::Key_Delete | Qt::CTRL : // DeleteWordRight case Qt::Key_L | Qt::CTRL | Qt::SHIFT : // LineDelete case Qt::Key_L | Qt::CTRL : // LineCut case Qt::Key_T | Qt::CTRL | Qt::SHIFT : // LineCopy cmd_list.at (i)->setKey (0); } } #endif #if defined (Q_OS_MAC) // Octave interprets Cmd key as Meta whereas Qscintilla interprets it // as Ctrl. We thus invert Meta/Ctrl in Qscintilla's shortcuts list. QList< QsciCommand * > cmd_list_mac = cmd_set->commands (); for (int i = 0; i < cmd_list_mac.length (); i++) { // Primary key int key = cmd_list_mac.at (i)->key (); if (static_cast<int> (key | Qt::META) == key && static_cast<int> (key | Qt::CTRL) != key) key = (key ^ Qt::META) | Qt::CTRL; else if (static_cast<int> (key | Qt::CTRL) == key) key = (key ^ Qt::CTRL) | Qt::META; cmd_list_mac.at (i)->setKey (key); // Alternate key key = cmd_list_mac.at (i)->alternateKey (); if (static_cast<int> (key | Qt::META) == key && static_cast<int> (key | Qt::CTRL) != key) key = (key ^ Qt::META) | Qt::CTRL; else if (static_cast<int> (key | Qt::CTRL) == key) key = (key ^ Qt::CTRL) | Qt::META; cmd_list_mac.at (i)->setAlternateKey (key); } #endif // selection markers m_indicator_id = indicatorDefine (QsciScintilla::StraightBoxIndicator); if (m_indicator_id == -1) m_indicator_id = 1; setIndicatorDrawUnder (true, m_indicator_id); markerDefine (QsciScintilla::Minus, marker::selection); // init state of undo/redo action for this tab emit status_update (isUndoAvailable (), isRedoAvailable ()); } void octave_qscintilla::setCursorPosition (int line, int col) { QsciScintilla::setCursorPosition (line, col); emit update_rowcol_indicator_signal (line, col); } void octave_qscintilla::set_selection_marker_color (const QColor& c) { QColor ic = c; ic.setAlphaF (0.45); setIndicatorForegroundColor (ic, m_indicator_id); setIndicatorOutlineColor (ic, m_indicator_id); setMarkerForegroundColor (c, marker::selection); setMarkerBackgroundColor (c, marker::selection); } // context menu requested void octave_qscintilla::contextMenuEvent (QContextMenuEvent *e) { #if defined (HAVE_QSCI_VERSION_2_6_0) QPoint global_pos, local_pos; // the menu's position QMenu *context_menu = createStandardContextMenu (); // standard menu bool in_left_margin = false; // determine position depending on mouse or keyboard event if (e->reason () == QContextMenuEvent::Mouse) { // context menu by mouse global_pos = e->globalPos (); // global mouse position local_pos = e->pos (); // local mouse position if (e->x () < marginWidth (1) + marginWidth (2)) in_left_margin = true; } else { // context menu by keyboard or other: get point of text cursor get_global_textcursor_pos (&global_pos, &local_pos); QRect editor_rect = geometry (); // editor rect mapped to global editor_rect.moveTopLeft (parentWidget ()->mapToGlobal (editor_rect.topLeft ())); if (! editor_rect.contains (global_pos)) // is cursor outside editor? global_pos = editor_rect.topLeft (); // yes, take top left corner } # if defined (HAVE_QSCI_VERSION_2_6_0) if (! in_left_margin) # endif { // fill context menu with editor's standard actions emit create_context_menu_signal (context_menu); // additional custom entries of the context menu context_menu->addSeparator (); // separator before custom entries // help menu: get the position of the mouse or the text cursor // (only for octave files) QString lexer_name = lexer ()->lexer (); if (lexer_name == "octave" || lexer_name == "matlab") { m_word_at_cursor = wordAtPoint (local_pos); if (! m_word_at_cursor.isEmpty ()) { context_menu->addAction (tr ("Help on") + ' ' + m_word_at_cursor, this, &octave_qscintilla::contextmenu_help); context_menu->addAction (tr ("Documentation on") + ' ' + m_word_at_cursor, this, &octave_qscintilla::contextmenu_doc); context_menu->addAction (tr ("Edit") + ' ' + m_word_at_cursor, this, &octave_qscintilla::contextmenu_edit); } } } # if defined (HAVE_QSCI_VERSION_2_6_0) else { // remove all standard actions from scintilla QList<QAction *> all_actions = context_menu->actions (); for (auto *a : all_actions) context_menu->removeAction (a); QAction *act = context_menu->addAction (tr ("dbstop if ..."), this, &octave_qscintilla::contextmenu_break_condition); act->setData (local_pos); } # endif // finally show the menu context_menu->exec (global_pos); #else octave_unused_parameter (e); #endif } // common function with flag for documentation void octave_qscintilla::contextmenu_help_doc (bool documentation) { if (documentation) { std::string name = m_word_at_cursor.toStdString (); emit interpreter_event ([=] (interpreter& interp) { // INTERPRETER THREAD F__event_manager_show_documentation__ (interp, ovl (name)); }); } else emit execute_command_in_terminal_signal ("help " + m_word_at_cursor); } // call edit the function related to the current word void octave_qscintilla::context_edit () { if (get_actual_word ()) contextmenu_edit (true); } // call edit the function related to the current word void octave_qscintilla::context_run () { if (hasSelectedText ()) { contextmenu_run (true); emit interpreter_event ([] (interpreter&) { command_editor::erase_empty_line (false); }); } } void octave_qscintilla::get_global_textcursor_pos (QPoint *global_pos, QPoint *local_pos) { long position = SendScintilla (SCI_GETCURRENTPOS); long point_x = SendScintilla (SCI_POINTXFROMPOSITION, 0, position); long point_y = SendScintilla (SCI_POINTYFROMPOSITION, 0, position); *local_pos = QPoint (point_x, point_y); // local cursor position *global_pos = mapToGlobal (*local_pos); // global position of cursor } // determine the actual word and whether we are in an octave or matlab script bool octave_qscintilla::get_actual_word () { QPoint global_pos, local_pos; get_global_textcursor_pos (&global_pos, &local_pos); m_word_at_cursor = wordAtPoint (local_pos); QString lexer_name = lexer ()->lexer (); return ((lexer_name == "octave" || lexer_name == "matlab") && ! m_word_at_cursor.isEmpty ()); } // helper function for clearing all indicators of a specific style void octave_qscintilla::clear_selection_markers () { int end_pos = text ().length (); int end_line, end_col; lineIndexFromPosition (end_pos, &end_line, &end_col); clearIndicatorRange (0, 0, end_line, end_col, m_indicator_id); markerDeleteAll (marker::selection); } QString octave_qscintilla::eol_string () { switch (eolMode ()) { case QsciScintilla::EolWindows: return ("\r\n"); case QsciScintilla::EolMac: return ("\r"); case QsciScintilla::EolUnix: return ("\n"); } // Last resort, if the above goes wrong (should never happen) return ("\r\n"); } // Function returning the true cursor position where the tab length // is taken into account. void octave_qscintilla::get_current_position (int *pos, int *line, int *col) { *pos = SendScintilla (QsciScintillaBase::SCI_GETCURRENTPOS); *line = SendScintilla (QsciScintillaBase::SCI_LINEFROMPOSITION, *pos); *col = SendScintilla (QsciScintillaBase::SCI_GETCOLUMN, *pos); } // Function returning the comment string of the current lexer QStringList octave_qscintilla::comment_string (bool comment) { int lexer = SendScintilla (SCI_GETLEXER); switch (lexer) { #if defined (HAVE_LEXER_OCTAVE) || defined (HAVE_LEXER_MATLAB) #if defined (HAVE_LEXER_OCTAVE) case SCLEX_OCTAVE: #else case SCLEX_MATLAB: #endif { gui_settings settings; int comment_string; if (comment) { // The commenting string is requested if (settings.contains (ed_comment_str.settings_key ())) // new version (radio buttons) comment_string = settings.int_value (ed_comment_str); else // old version (combo box) comment_string = settings.value (ed_comment_str_old.settings_key (), ed_comment_str.def ()).toInt (); return (QStringList (ed_comment_strings.at (comment_string))); } else { QStringList c_str; // The possible uncommenting string(s) are requested comment_string = settings.int_value (ed_uncomment_str); for (int i = 0; i < ed_comment_strings_count; i++) { if (1 << i & comment_string) c_str.append (ed_comment_strings.at (i)); } return c_str; } } #endif case SCLEX_PERL: case SCLEX_BASH: case SCLEX_DIFF: return QStringList ("#"); case SCLEX_CPP: return QStringList ("//"); case SCLEX_BATCH: return QStringList ("REM "); } return QStringList ("%"); // should never happen } // provide the style at a specific position int octave_qscintilla::get_style (int pos) { int position; if (pos < 0) // The positition has to be reduced by 2 for getting the real style (?) position = SendScintilla (QsciScintillaBase::SCI_GETCURRENTPOS) - 2; else position = pos; return SendScintilla (QsciScintillaBase::SCI_GETSTYLEAT, position); } // Is a specific cursor position in a line or block comment? int octave_qscintilla::is_style_comment (int pos) { int lexer = SendScintilla (QsciScintillaBase::SCI_GETLEXER); int style = get_style (pos); switch (lexer) { case SCLEX_CPP: return (ST_LINE_COMMENT * (style == QsciLexerCPP::CommentLine || style == QsciLexerCPP::CommentLineDoc) + ST_BLOCK_COMMENT * (style == QsciLexerCPP::Comment || style == QsciLexerCPP::CommentDoc || style == QsciLexerCPP::CommentDocKeyword || style == QsciLexerCPP::CommentDocKeywordError)); #if defined (HAVE_LEXER_MATLAB) case SCLEX_MATLAB: return (ST_LINE_COMMENT * (style == QsciLexerMatlab::Comment)); #endif #if defined (HAVE_LEXER_OCTAVE) case SCLEX_OCTAVE: return (ST_LINE_COMMENT * (style == QsciLexerOctave::Comment)); #endif case SCLEX_PERL: return (ST_LINE_COMMENT * (style == QsciLexerPerl::Comment)); case SCLEX_BATCH: return (ST_LINE_COMMENT * (style == QsciLexerBatch::Comment)); case SCLEX_DIFF: return (ST_LINE_COMMENT * (style == QsciLexerDiff::Comment)); case SCLEX_BASH: return (ST_LINE_COMMENT * (style == QsciLexerBash::Comment)); } return ST_NONE; } // Do smart indentation after if, for, ... void octave_qscintilla::smart_indent (bool do_smart_indent, int do_auto_close, int line, int ind_char_width) { QString prevline = text (line); QRegularExpression bkey {"^[\t ]*(if|for|while|switch" "|do|function|properties|events|classdef" "|unwind_protect|try" "|parfor|methods)" "[\r]?[\n\t #%]"}; // last word except for comments, assuming no ' or " in comment. // rx_end = QRegExp ("(\\w+)[ \t;\r\n]*([%#][^\"']*)?$"); // last word except for comments, // allowing % and # in single or double quoted strings // FIXME: This will get confused by transpose. QRegularExpression ekey {"(?:(?:['\"][^'\"]*['\"])?[^%#]*)*" "(\\w+)[ \t;\r\n]*(?:[%#].*)?$"}; QRegularExpressionMatch bmatch = bkey.match (prevline); if (bmatch.hasMatch ()) { // Found keyword after that indentation should be added // Check for existing end statement in the same line QRegularExpressionMatch ematch = ekey.match (prevline, bmatch.capturedStart ()); QString first_word = bmatch.captured (1); bool inline_end = ematch.hasMatch () && is_end (ematch.captured (1), first_word); if (do_smart_indent && ! inline_end) { // Do smart indent in the current line (line+1) indent (line+1); setCursorPosition (line+1, indentation (line+1) / ind_char_width); } if (do_auto_close && ! inline_end && ! first_word.contains (QRegularExpression {"(?:case|otherwise|unwind_protect_cleanup)"})) { // Do auto close auto_close (do_auto_close, line, prevline, first_word); } return; } QRegularExpression mkey {"^[\t ]*(?:else|elseif|catch|unwind_protect_cleanup)" "[\r]?[\t #%\n]"}; if (prevline.contains (mkey)) { int prev_ind = indentation (line-1); int act_ind = indentation (line); if (prev_ind == act_ind) unindent (line); else if (prev_ind > act_ind) { setIndentation (line+1, prev_ind); setCursorPosition (line+1, prev_ind); } return; } QRegularExpression case_key {"^[\t ]*(?:case|otherwise)[\r]?[\t #%\n]"}; if (prevline.contains (case_key) && do_smart_indent) { QString last_line = text (line-1); int prev_ind = indentation (line-1); int act_ind = indentation (line); if (last_line.contains (QRegularExpression {"^[\t ]*switch"})) { indent (line+1); act_ind = indentation (line+1); } else { if (prev_ind == act_ind) unindent (line); else if (prev_ind > act_ind) act_ind = prev_ind; } setIndentation (line+1, act_ind); setCursorPosition (line+1, act_ind); } ekey = QRegularExpression {"^[\t ]*(?:end|endif|endfor|endwhile|until|endfunction" "|endswitch|end_try_catch|end_unwind_protect)[\r]?[\t #%\n(;]"}; if (prevline.contains (ekey)) { if (indentation (line-1) <= indentation (line)) { unindent (line+1); unindent (line); if (prevline.contains ("endswitch")) { // endswitch has to me unndented twice unindent (line+1); unindent (line); } setCursorPosition (line+1, indentation (line)); } return; } } // Do smart indentation of current selection or line. void octave_qscintilla::smart_indent_line_or_selected_text (int lineFrom, int lineTo) { QRegularExpression blank_line_regexp {"^[\t ]*$"}; // end[xxxxx] [# comment] at end of a line QRegularExpression end_word_regexp {"(?:(?:['\"][^'\"]*['\"])?[^%#]*)*" "(?:end\\w*)[\r\n\t ;]*(?:[%#].*)?$"}; QRegularExpression begin_block_regexp {"^[\t ]*(?:if|elseif|else" "|for|while|do|parfor" "|switch|case|otherwise" "|function" "|classdef|properties|events|enumeration|methods" "|unwind_protect|unwind_protect_cleanup|try|catch)" "[\r\n\t #%]"}; QRegularExpression mid_block_regexp {"^[\t ]*(?:elseif|else" "|unwind_protect_cleanup|catch)" "[\r\n\t #%]"}; QRegularExpression end_block_regexp {"^[\t ]*(?:end" "|end(for|function|if|parfor|switch|while" "|classdef|enumeration|events|methods|properties)" "|end_(try_catch|unwind_protect)" "|until)" "[\r\n\t #%]"}; QRegularExpression case_block_regexp {"^[\t ]*(?:case|otherwise)" "[\r\n\t #%]"}; QRegularExpressionMatch match; int indent_column = -1; int indent_increment = indentationWidth (); bool in_switch = false; for (int line = lineFrom-1; line >= 0; line--) { QString line_text = text (line); match = blank_line_regexp.match (line_text); if (! match.hasMatch ()) { // Found first non-blank line above beginning of region or // current line. Base indentation from this line, increasing // indentation by indentationWidth if it looks like the // beginning of a code block. indent_column = indentation (line); match = begin_block_regexp.match (line_text); if (match.hasMatch ()) { indent_column += indent_increment; if (line_text.contains ("switch")) in_switch = true; } break; } } if (indent_column < 0) indent_column = indentation (lineFrom); QString prev_line; for (int line = lineFrom; line <= lineTo; line++) { QString line_text = text (line); match = end_block_regexp.match (line_text); if (match.hasMatch ()) { indent_column -= indent_increment; if (line_text.contains ("endswitch")) { // need a double de-indent for endswitch if (in_switch) indent_column -= indent_increment; in_switch = false; } } match = mid_block_regexp.match (line_text); if (match.hasMatch ()) indent_column -= indent_increment; match = case_block_regexp.match (line_text); if (match.hasMatch ()) { match = case_block_regexp.match (prev_line); if (! match.hasMatch () && ! prev_line.contains ("switch")) indent_column -= indent_increment; in_switch = true; } setIndentation (line, indent_column); match = begin_block_regexp.match (line_text); if (match.hasMatch ()) { // Check for existing end statement in the same line match = end_word_regexp.match (line_text, match.capturedStart ()); if (! match.hasMatch ()) indent_column += indent_increment; if (line_text.contains ("switch")) in_switch = true; } match = blank_line_regexp.match (line_text); if (! match.hasMatch ()) prev_line = line_text; } } void octave_qscintilla::set_word_selection (const QString& word) { m_selection = word; if (word.isEmpty ()) { m_selection_line = -1; m_selection_col = -1; m_selection_replacement = ""; clear_selection_markers (); QToolTip::hideText (); } else { int pos; get_current_position (&pos, &m_selection_line, &m_selection_col); } } void octave_qscintilla::show_selection_markers (int l1, int c1, int l2, int c2) { fillIndicatorRange (l1, c1, l2, c2, m_indicator_id); if (l1 == l2) markerAdd (l1, marker::selection); } void octave_qscintilla::contextmenu_help (bool) { contextmenu_help_doc (false); } void octave_qscintilla::contextmenu_doc (bool) { contextmenu_help_doc (true); } void octave_qscintilla::context_help_doc (bool documentation) { if (get_actual_word ()) contextmenu_help_doc (documentation); } void octave_qscintilla::contextmenu_edit (bool) { emit context_menu_edit_signal (m_word_at_cursor); } void octave_qscintilla::contextmenu_run_temp_error () { QMessageBox::critical (this, tr ("Octave Editor"), tr ("Creating temporary files failed.\n" "Make sure you have write access to temp. directory\n" "%1\n\n" "\"Run Selection\" requires temporary files.").arg (QDir::tempPath ())); } void octave_qscintilla::contextmenu_run (bool) { // Take selected code and extend it by commands for echoing each // evaluated line and for adding the line to the history (use script) QString code = QString (); QString hist = QString (); // Split contents into single lines and complete commands QStringList lines = selectedText ().split (QRegularExpression {"[\r\n]"}, #if defined (HAVE_QT_SPLITBEHAVIOR_ENUM) Qt::SkipEmptyParts); #else QString::SkipEmptyParts); #endif for (int i = 0; i < lines.count (); i++) { QString line = lines.at (i); if (line.trimmed ().isEmpty ()) continue; QString line_escaped = line; line_escaped.replace (QString ("'"), QString ("''")); QString line_history = line; // Prevent output of breakpoint in temp. file for keyboard QString next_bp_quiet; QString next_bp_quiet_reset; if (line.contains ("keyboard")) { // Define commands for not showing bp location and for resetting // this in case "keyboard" was within a comment next_bp_quiet = "__db_next_breakpoint_quiet__;\n"; next_bp_quiet_reset = "\n__db_next_breakpoint_quiet__(false);"; } // Add codeline code += next_bp_quiet + line + next_bp_quiet_reset + "\n"; hist += line_history + "\n"; } octave_stdout << hist.toStdString (); // Create tmp file with the code to be executed by the interpreter QPointer<QTemporaryFile> tmp_file = create_tmp_file ("m", code); if (tmp_file && tmp_file->open ()) tmp_file->close (); else { // tmp files not working: use old way to run selection contextmenu_run_temp_error (); return; } // Create tmp file required for adding command to history QPointer<QTemporaryFile> tmp_hist = create_tmp_file ("", hist); if (tmp_hist && tmp_hist->open ()) tmp_hist->close (); else { // tmp files not working: use old way to run selection contextmenu_run_temp_error (); return; } // Add commands to the history emit interpreter_event ([=] (interpreter& interp) { // INTERPRETER THREAD if (tmp_hist.isNull ()) return; std::string opt = "-r"; std::string path = tmp_hist->fileName ().toStdString (); Fhistory (interp, ovl (opt, path)); }); // Disable opening a file at a breakpoint in case keyboard () is used gui_settings settings; bool show_dbg_file = settings.bool_value (ed_show_dbg_file); settings.setValue (ed_show_dbg_file.settings_key (), false); // The interpreter_event callback function below emits a signal. // Because we don't control when that happens, use a guarded pointer // so that the callback can abort if this object is no longer valid. QPointer<octave_qscintilla> this_oq (this); // Let the interpreter execute the tmp file emit interpreter_event ([=] (interpreter& interp) { // INTERPRETER THREAD // FIXME: For now, just skip the entire callback if THIS_OQ is // no longer valid. Maybe there is a better way to do this // job? if (this_oq.isNull ()) return; std::string file = tmp_file->fileName ().toStdString (); std::string pending_input = command_editor::get_current_line (); int err_line = -1; // For storing the line of a poss. error // Get current state of auto command repeat in debug mode octave_value_list ovl_dbg = Fisdebugmode (interp); bool dbg = ovl_dbg(0).bool_value (); octave_value_list ovl_auto_repeat = ovl (true); if (dbg) ovl_auto_repeat = Fauto_repeat_debug_command (interp, ovl (false), 1); bool auto_repeat = ovl_auto_repeat(0).bool_value (); try { // Do the job interp.source_file (file); } catch (const execution_exception& ee) { // Catch errors otherwise the rest of the interpreter // will not be executed (cleaning up). // New error message and error stack QString new_msg = QString::fromStdString (ee.message ()); std::list<frame_info> stack = ee.stack_info (); // Remove line and column from first line of error message only // if it is related to the tmp itself, i.e. only if the // the error stack size is 0, 1, or, if in debug mode, 2 size_t max_stack_size = 1; if (dbg) max_stack_size = 2; if (stack.size () <= max_stack_size) { QRegularExpression rx {"source: error sourcing file [^\n]*$"}; if (new_msg.contains (rx)) { // Selected code has syntax errors new_msg.replace (rx, "error sourcing selected code"); err_line = 0; // Nothing into history? } else { // Normal error, detect line and remove file // name from message QStringList rx_list; rx_list << "near line (\\d+),[^\n]*\n"; rx_list << "near line (\\d+),[^\n]*$"; QStringList replace_list; replace_list << "\n"; replace_list << ""; for (int i = 0; i < rx_list.length (); i++) { rx = QRegularExpression {rx_list.at (i)}; QRegularExpressionMatch match = rx.match(new_msg); if (match.hasMatch ()) { err_line = match.captured (1).toInt (); new_msg = new_msg.replace (rx, replace_list.at (i)); } } } } // Drop first stack level, which is the temporary function file, // or, if in debug mode, drop first two stack levels if (stack.size () > 0) stack.pop_back (); if (dbg && (stack.size () > 0)) stack.pop_back (); // Clean up before throwing the modified error. emit ctx_menu_run_finished_signal (show_dbg_file, err_line, tmp_file, tmp_hist, dbg, auto_repeat); // New exception with updated message and stack execution_exception nee (ee.err_type (), ee.identifier (), new_msg.toStdString (), stack); // Throw throw (nee); } // Clean up emit ctx_menu_run_finished_signal (show_dbg_file, err_line, tmp_file, tmp_hist, dbg, auto_repeat); command_editor::erase_empty_line (true); command_editor::replace_line (""); command_editor::set_initial_input (pending_input); command_editor::redisplay (); command_editor::interrupt_event_loop (); command_editor::accept_line (); command_editor::erase_empty_line (true); }); } void octave_qscintilla::ctx_menu_run_finished (bool show_dbg_file, int, QPointer<QTemporaryFile> tmp_file, QPointer<QTemporaryFile> tmp_hist, bool dbg, bool auto_repeat) { emit focus_console_after_command_signal (); // TODO: Use line nr. (int argument) of possible error for removing // lines from history that were never executed. For this, // possible lines from commands at a debug prompt must be // taken into consideration. gui_settings settings; settings.setValue (ed_show_dbg_file.settings_key (), show_dbg_file); if (tmp_file && tmp_file->exists ()) tmp_file->remove (); if (tmp_hist && tmp_hist->exists ()) tmp_hist->remove (); emit interpreter_event ([=] (interpreter& interp) { // INTERPRETER THREAD if (dbg) Fauto_repeat_debug_command (interp, ovl (auto_repeat)); }); } // wrappers for dbstop related context menu items // FIXME: Why can't the data be sent as the argument to the function??? void octave_qscintilla::contextmenu_break_condition (bool) { #if defined (HAVE_QSCI_VERSION_2_6_0) QAction *action = qobject_cast<QAction *>(sender ()); QPoint local_pos = action->data ().value<QPoint> (); // pick point just right of margins, so lineAt doesn't give -1 int margins = marginWidth (1) + marginWidth (2) + marginWidth (3); local_pos = QPoint (margins + 1, local_pos.y ()); emit context_menu_break_condition_signal (lineAt (local_pos)); #endif } void octave_qscintilla::contextmenu_break_once (const QPoint& local_pos) { #if defined (HAVE_QSCI_VERSION_2_6_0) emit context_menu_break_once (lineAt (local_pos)); #else octave_unused_parameter (local_pos); #endif } void octave_qscintilla::text_changed () { emit status_update (isUndoAvailable (), isRedoAvailable ()); } void octave_qscintilla::cursor_position_changed (int line, int col) { // Clear the selection if we move away from it. We have to check the // position, because we allow entering text at the point of the // selection to trigger a search and replace that does not clear the // selection until it is complete. if (! m_selection.isEmpty () && (line != m_selection_line || col != m_selection_col)) set_word_selection (); } // when edit area gets focus update information on undo/redo actions void octave_qscintilla::focusInEvent (QFocusEvent *focusEvent) { emit status_update (isUndoAvailable (), isRedoAvailable ()); QsciScintilla::focusInEvent (focusEvent); } void octave_qscintilla::show_replace_action_tooltip () { int pos; get_current_position (&pos, &m_selection_line, &m_selection_col); // Offer to replace other instances. QKeySequence keyseq = Qt::SHIFT | Qt::Key_Return; QString msg = (tr ("Press '%1' to replace all occurrences of '%2' with '%3'.") . arg (keyseq.toString ()) . arg (m_selection) . arg (m_selection_replacement)); QPoint global_pos; QPoint local_pos; get_global_textcursor_pos (&global_pos, &local_pos); QFontMetrics ttfm (QToolTip::font ()); // Try to avoid overlapping with the text completion dialog // and the text that is currently being edited. global_pos += QPoint (2*ttfm.maxWidth (), -3*ttfm.height ()); QToolTip::showText (global_pos, msg); } void octave_qscintilla::replace_all (const QString& o_str, const QString& n_str, bool re, bool cs, bool wo) { // get the resulting cursor position int pos, line, col, nline, ncol; get_current_position (&pos, &line, &col); // remember first visible line for restoring the view afterwards int first_line = firstVisibleLine (); // search for first occurrence of the detected word bool find_result_available = findFirst (o_str, re, cs, wo, false, true, 0, 0); // replace and find more occurrences in a loop beginUndoAction (); while (find_result_available) { // findNext doesn't work properly if the length of the replacement // text is different from the original replace (n_str); get_current_position (&pos, &nline, &ncol); find_result_available = findFirst (o_str, re, cs, wo, false, true, nline, ncol); } endUndoAction (); // restore the visible area setFirstVisibleLine (first_line); // fix cursor column if outside of new line length int eol_len = eol_string ().length (); if (line == lines () - 1) eol_len = 0; const int col_max = text (line).length () - eol_len; if (col_max < col) col = col_max; setCursorPosition (line, col); } bool octave_qscintilla::event (QEvent *e) { if (m_debug_mode && e->type() == QEvent::ToolTip) { // FIXME: can we handle display of a tooltip using an // interpreter event or a custom signal/slot connection? QHelpEvent *help_e = static_cast<QHelpEvent *> (e); QString symbol = wordAtPoint (help_e->pos()); emit show_symbol_tooltip_signal (help_e->globalPos (), symbol); return true; } return QsciScintilla::event(e); } void octave_qscintilla::keyPressEvent (QKeyEvent *key_event) { if (m_selection.isEmpty ()) QsciScintilla::keyPressEvent (key_event); else { int key = key_event->key (); Qt::KeyboardModifiers modifiers = key_event->modifiers (); if (key == Qt::Key_Return && modifiers == Qt::ShiftModifier) { replace_all (m_selection, m_selection_replacement, false, true, true); // Clear the selection. set_word_selection (); } else { // The idea here is to allow backspace to remove the last // character of the replacement text to allow minimal editing // and to also end the selection replacement action if text is // not valid as a word constituent (control characters, // etc.). Is there a better way than having special cases for // DEL and ESC here? QString text = key_event->text (); bool cancel_replacement = false; if (key == Qt::Key_Backspace) { if (m_selection_replacement.isEmpty ()) cancel_replacement = true; else m_selection_replacement.chop (1); } else if (key == Qt::Key_Delete || key == Qt::Key_Escape) cancel_replacement = true; else if (! text.isEmpty ()) m_selection_replacement += text; else if (modifiers != Qt::ShiftModifier) cancel_replacement = true; // Perform default action. QsciScintilla::keyPressEvent (key_event); if (cancel_replacement) set_word_selection (); if (! m_selection_replacement.isEmpty ()) show_replace_action_tooltip (); } } } void octave_qscintilla::auto_close (int auto_endif, int linenr, const QString& line, QString& first_word) { // Insert an "end" for an "if" etc., if needed. // (Use of "while" allows "return" to skip the rest. // It may be clearer to use "if" and "goto", // but that violates the coding standards.) bool autofill_simple_end = (auto_endif == 2); std::size_t start = line.toStdString ().find_first_not_of (" \t"); // Check if following line has the same or less indentation // Check if the following line does not start with // end* (until) (catch) if (linenr < lines () - 1) { int offset = 2; // linenr is the old line, thus, linnr+1 is the // new one and can not be taken into account std::size_t next_start; QString next_line; do // find next non-blank line { next_line = text (linenr + offset++); next_start = next_line.toStdString ().find_first_not_of (" \t\n"); } while (linenr + offset < lines () && next_start == std::string::npos); if (next_start == std::string::npos) next_start = 0; if (start == 0 && next_start == 0) return; // bug #56160, don't add at 0 if (next_start > start) // more indented => don't add "end" return; if (next_start == start) // same => check if already is "end" { QRegularExpression rx_start {R"((\w+))"}; QRegularExpressionMatch match = rx_start.match (next_line, start); if (match.hasMatch () && is_end (match.captured (1), first_word)) return; } } // If all of the above, insert a new line, with matching indent // and either 'end' or 'end...', depending on a flag. // If we insert directly after the last line, the "end" is autoindented, // so add a dummy line. if (linenr + 2 == lines ()) insertAt (QString ("\n"), linenr + 2, 0); // For try/catch/end, fill "end" first, so "catch" is top of undo stack if (first_word == "try") insertAt (QString (start, ' ') + (autofill_simple_end ? "end\n" : "end_try_catch\n"), linenr + 2, 0); else if (first_word == "unwind_protect") insertAt (QString (start, ' ') + (autofill_simple_end ? "end\n" : "end_unwind_protect\n"), linenr + 2, 0); QString next_line; if (first_word == "do") next_line = "until\n"; else if (first_word == "try") next_line = "catch\n"; else if (first_word == "unwind_protect") next_line = "unwind_protect_cleanup\n"; else if (autofill_simple_end) next_line = "end\n"; else { if (first_word == "unwind_protect") first_word = '_' + first_word; next_line = "end" + first_word + "\n"; } //insertAt (QString (start, ' ') + next_line, linenr + 2, 0); insertAt (next_line, linenr + 2, 0); setIndentation (linenr + 2, indentation (linenr)); } void octave_qscintilla::dragEnterEvent (QDragEnterEvent *e) { // if is not dragging a url, pass to qscintilla to handle, // otherwise ignore it so that it will be handled by // the parent if (!e->mimeData ()->hasUrls ()) { QsciScintilla::dragEnterEvent (e); } else { e->ignore(); } } void octave_qscintilla::handle_enter_debug_mode () { m_debug_mode = true; } void octave_qscintilla::handle_exit_debug_mode () { m_debug_mode = false; } QPointer<QTemporaryFile> octave_qscintilla::create_tmp_file (const QString& extension, const QString& contents) { QString ext = extension; if ((! ext.isEmpty ()) && (! ext.startsWith ('.'))) ext = QString (".") + ext; // Create octave dir within temp. dir QString tmp_dir = QString::fromStdString (sys::env::get_temp_directory ()); QString tmp_name = tmp_dir + QDir::separator() + "octave_XXXXXX" + ext; QPointer<QTemporaryFile> tmp_file (new QTemporaryFile (tmp_name, this)); if (! contents.isEmpty () && tmp_file && tmp_file->open ()) { tmp_file->write (contents.toUtf8 ()); tmp_file->close (); } return tmp_file; } OCTAVE_END_NAMESPACE(octave) #endif