1 //////////////////////////////////////////////////////////////////////// 2 // 3 // Copyright (C) 2013-2021 The Octave Project Developers 4 // 5 // See the file COPYRIGHT.md in the top-level directory of this 6 // distribution or <https://octave.org/copyright/>. 7 // 8 // This file is part of Octave. 9 // 10 // Octave is free software: you can redistribute it and/or modify it 11 // under the terms of the GNU General Public License as published by 12 // the Free Software Foundation, either version 3 of the License, or 13 // (at your option) any later version. 14 // 15 // Octave is distributed in the hope that it will be useful, but 16 // WITHOUT ANY WARRANTY; without even the implied warranty of 17 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 // GNU General Public License for more details. 19 // 20 // You should have received a copy of the GNU General Public License 21 // along with Octave; see the file COPYING. If not, see 22 // <https://www.gnu.org/licenses/>. 23 // 24 //////////////////////////////////////////////////////////////////////// 25 26 #if defined (HAVE_CONFIG_H) 27 # include "config.h" 28 #endif 29 30 #if defined (HAVE_QSCINTILLA) 31 32 #include <Qsci/qscilexer.h> 33 34 #include <QDir> 35 #include <QKeySequence> 36 #include <QMessageBox> 37 #include <QMimeData> 38 #include <QShortcut> 39 #include <QToolTip> 40 #include <QVBoxLayout> 41 #if defined (HAVE_QSCI_QSCILEXEROCTAVE_H) 42 # define HAVE_LEXER_OCTAVE 1 43 # include <Qsci/qscilexeroctave.h> 44 #elif defined (HAVE_QSCI_QSCILEXERMATLAB_H) 45 # define HAVE_LEXER_MATLAB 1 46 # include <Qsci/qscilexermatlab.h> 47 #endif 48 #include <Qsci/qscicommandset.h> 49 #include <Qsci/qscilexerbash.h> 50 #include <Qsci/qscilexerbatch.h> 51 #include <Qsci/qscilexercpp.h> 52 #include <Qsci/qscilexerdiff.h> 53 #include <Qsci/qscilexerperl.h> 54 55 #include "file-editor-tab.h" 56 #include "gui-preferences-ed.h" 57 // FIXME: hardwired marker numbers? 58 #include "marker.h" 59 #include "octave-qobject.h" 60 #include "octave-qscintilla.h" 61 #include "shortcut-manager.h" 62 63 #include "builtin-defun-decls.h" 64 #include "cmd-edit.h" 65 #include "interpreter-private.h" 66 #include "interpreter.h" 67 68 // Return true if CANDIDATE is a "closing" that matches OPENING, 69 // such as "end" or "endif" for "if", or "catch" for "try". 70 // Used for testing the last word of an "if" etc. line, 71 // or the first word of the following line. 72 73 namespace octave 74 { 75 static bool is_end(const QString & candidate,const QString & opening)76 is_end (const QString& candidate, const QString& opening) 77 { 78 bool retval = false; 79 80 if (opening == "do") // The only one that can't be ended by "end" 81 { 82 if (candidate == "until") 83 retval = true; 84 } 85 else 86 { 87 if (candidate == "end") 88 retval = true; 89 else 90 { 91 if (opening == "try") 92 { 93 if (candidate == "catch" || candidate == "end_try_catch") 94 retval = true; 95 } 96 else if (opening == "unwind_protect") 97 { 98 if (candidate == "unwind_protect_cleanup" 99 || candidate == "end_unwind_protect") 100 retval = true; 101 } 102 else if (candidate == "end" + opening) 103 retval = true; 104 else if (opening == "if" && candidate == "else") 105 retval = true; 106 } 107 } 108 109 return retval; 110 } 111 octave_qscintilla(QWidget * p,base_qobject & oct_qobj)112 octave_qscintilla::octave_qscintilla (QWidget *p, base_qobject& oct_qobj) 113 : QsciScintilla (p), m_octave_qobj (oct_qobj), m_word_at_cursor (), 114 m_selection (), m_selection_replacement (), m_selection_line (-1), 115 m_selection_col (-1), m_indicator_id (1) 116 { 117 connect (this, SIGNAL (textChanged (void)), 118 this, SLOT (text_changed (void))); 119 120 connect (this, SIGNAL (cursorPositionChanged (int, int)), 121 this, SLOT (cursor_position_changed (int, int))); 122 123 connect (this, SIGNAL (ctx_menu_run_finished_signal (bool, int, QTemporaryFile*, 124 QTemporaryFile*, bool, bool)), 125 this, SLOT (ctx_menu_run_finished (bool, int, QTemporaryFile*, 126 QTemporaryFile*, bool, bool)), 127 Qt::QueuedConnection); 128 129 // clear scintilla edit shortcuts that are handled by the editor 130 QsciCommandSet *cmd_set = standardCommands (); 131 132 // Disable buffered drawing on all systems 133 SendScintilla (SCI_SETBUFFEREDDRAW, false); 134 135 #if defined (HAVE_QSCI_VERSION_2_6_0) 136 // find () was added in QScintilla 2.6 137 cmd_set->find (QsciCommand::SelectionCopy)->setKey (0); 138 cmd_set->find (QsciCommand::SelectionCut)->setKey (0); 139 cmd_set->find (QsciCommand::Paste)->setKey (0); 140 cmd_set->find (QsciCommand::SelectAll)->setKey (0); 141 cmd_set->find (QsciCommand::SelectionDuplicate)->setKey (0); 142 cmd_set->find (QsciCommand::LineTranspose)->setKey (0); 143 cmd_set->find (QsciCommand::Undo)->setKey (0); 144 cmd_set->find (QsciCommand::Redo)->setKey (0); 145 cmd_set->find (QsciCommand::SelectionUpperCase)->setKey (0); 146 cmd_set->find (QsciCommand::SelectionLowerCase)->setKey (0); 147 cmd_set->find (QsciCommand::ZoomIn)->setKey (0); 148 cmd_set->find (QsciCommand::ZoomOut)->setKey (0); 149 cmd_set->find (QsciCommand::DeleteWordLeft)->setKey (0); 150 cmd_set->find (QsciCommand::DeleteWordRight)->setKey (0); 151 cmd_set->find (QsciCommand::DeleteLineLeft)->setKey (0); 152 cmd_set->find (QsciCommand::DeleteLineRight)->setKey (0); 153 cmd_set->find (QsciCommand::LineDelete)->setKey (0); 154 cmd_set->find (QsciCommand::LineCut)->setKey (0); 155 cmd_set->find (QsciCommand::LineCopy)->setKey (0); 156 #else 157 // find commands via its default key (tricky way without find ()) 158 QList< QsciCommand * > cmd_list = cmd_set->commands (); 159 for (int i = 0; i < cmd_list.length (); i++) 160 { 161 int cmd_key = cmd_list.at (i)->key (); 162 switch (cmd_key) 163 { 164 case Qt::Key_C | Qt::CTRL : // SelectionCopy 165 case Qt::Key_X | Qt::CTRL : // SelectionCut 166 case Qt::Key_V | Qt::CTRL : // Paste 167 case Qt::Key_A | Qt::CTRL : // SelectAll 168 case Qt::Key_D | Qt::CTRL : // SelectionDuplicate 169 case Qt::Key_T | Qt::CTRL : // LineTranspose 170 case Qt::Key_Z | Qt::CTRL : // Undo 171 case Qt::Key_Y | Qt::CTRL : // Redo 172 case Qt::Key_Z | Qt::CTRL | Qt::SHIFT : // Redo 173 case Qt::Key_U | Qt::CTRL : // SelectionLowerCase 174 case Qt::Key_U | Qt::CTRL | Qt::SHIFT : // SelectionUpperCase 175 case Qt::Key_Plus | Qt::CTRL : // ZoomIn 176 case Qt::Key_Minus | Qt::CTRL : // ZoomOut 177 case Qt::Key_Backspace | Qt::CTRL | Qt::SHIFT : // DeleteLineLeft 178 case Qt::Key_Delete | Qt::CTRL | Qt::SHIFT : // DeleteLineRight 179 case Qt::Key_K | Qt::META : // DeleteLineRight 180 case Qt::Key_Backspace | Qt::CTRL : // DeleteWordLeft 181 case Qt::Key_Delete | Qt::CTRL : // DeleteWordRight 182 case Qt::Key_L | Qt::CTRL | Qt::SHIFT : // LineDelete 183 case Qt::Key_L | Qt::CTRL : // LineCut 184 case Qt::Key_T | Qt::CTRL | Qt::SHIFT : // LineCopy 185 cmd_list.at (i)->setKey (0); 186 } 187 } 188 #endif 189 190 #if defined (Q_OS_MAC) 191 // Octave interprets Cmd key as Meta whereas Qscintilla interprets it 192 // as Ctrl. We thus invert Meta/Ctrl in Qscintilla's shortcuts list. 193 QList< QsciCommand * > cmd_list_mac = cmd_set->commands (); 194 for (int i = 0; i < cmd_list_mac.length (); i++) 195 { 196 // Primary key 197 int key = cmd_list_mac.at (i)->key (); 198 199 if (static_cast<int> (key | Qt::META) == key 200 && static_cast<int> (key | Qt::CTRL) != key) 201 key = (key ^ Qt::META) | Qt::CTRL; 202 else if (static_cast<int> (key | Qt::CTRL) == key) 203 key = (key ^ Qt::CTRL) | Qt::META; 204 205 cmd_list_mac.at (i)->setKey (key); 206 207 // Alternate key 208 key = cmd_list_mac.at (i)->alternateKey (); 209 210 if (static_cast<int> (key | Qt::META) == key 211 && static_cast<int> (key | Qt::CTRL) != key) 212 key = (key ^ Qt::META) | Qt::CTRL; 213 else if (static_cast<int> (key | Qt::CTRL) == key) 214 key = (key ^ Qt::CTRL) | Qt::META; 215 216 cmd_list_mac.at (i)->setAlternateKey (key); 217 } 218 #endif 219 220 // selection markers 221 222 m_indicator_id = indicatorDefine (QsciScintilla::StraightBoxIndicator); 223 if (m_indicator_id == -1) 224 m_indicator_id = 1; 225 226 setIndicatorDrawUnder (true, m_indicator_id); 227 228 markerDefine (QsciScintilla::Minus, marker::selection); 229 230 // init state of undo/redo action for this tab 231 emit status_update (isUndoAvailable (), isRedoAvailable ()); 232 } 233 set_selection_marker_color(const QColor & c)234 void octave_qscintilla::set_selection_marker_color (const QColor& c) 235 { 236 QColor ic = c; 237 ic.setAlphaF (0.25); 238 setIndicatorForegroundColor (ic, m_indicator_id); 239 setIndicatorOutlineColor (ic, m_indicator_id); 240 241 setMarkerForegroundColor (c, marker::selection); 242 setMarkerBackgroundColor (c, marker::selection); 243 } 244 245 // context menu requested contextMenuEvent(QContextMenuEvent * e)246 void octave_qscintilla::contextMenuEvent (QContextMenuEvent *e) 247 { 248 #if defined (HAVE_QSCI_VERSION_2_6_0) 249 QPoint global_pos, local_pos; // the menu's position 250 QMenu *context_menu = createStandardContextMenu (); // standard menu 251 252 bool in_left_margin = false; 253 254 // determine position depending on mouse or keyboard event 255 if (e->reason () == QContextMenuEvent::Mouse) 256 { 257 // context menu by mouse 258 global_pos = e->globalPos (); // global mouse position 259 local_pos = e->pos (); // local mouse position 260 if (e->x () < marginWidth (1) + marginWidth (2)) 261 in_left_margin = true; 262 } 263 else 264 { 265 // context menu by keyboard or other: get point of text cursor 266 get_global_textcursor_pos (&global_pos, &local_pos); 267 QRect editor_rect = geometry (); // editor rect mapped to global 268 editor_rect.moveTopLeft 269 (parentWidget ()->mapToGlobal (editor_rect.topLeft ())); 270 if (! editor_rect.contains (global_pos)) // is cursor outside editor? 271 global_pos = editor_rect.topLeft (); // yes, take top left corner 272 } 273 274 #if defined (HAVE_QSCI_VERSION_2_6_0) 275 if (! in_left_margin) 276 #endif 277 { 278 // fill context menu with editor's standard actions 279 emit create_context_menu_signal (context_menu); 280 281 // additional custom entries of the context menu 282 context_menu->addSeparator (); // separator before custom entries 283 284 // help menu: get the position of the mouse or the text cursor 285 // (only for octave files) 286 QString lexer_name = lexer ()->lexer (); 287 if (lexer_name == "octave" || lexer_name == "matlab") 288 { 289 m_word_at_cursor = wordAtPoint (local_pos); 290 if (! m_word_at_cursor.isEmpty ()) 291 { 292 context_menu->addAction (tr ("Help on") + ' ' + m_word_at_cursor, 293 this, SLOT (contextmenu_help (bool))); 294 context_menu->addAction (tr ("Documentation on") 295 + ' ' + m_word_at_cursor, 296 this, SLOT (contextmenu_doc (bool))); 297 context_menu->addAction (tr ("Edit") + ' ' + m_word_at_cursor, 298 this, SLOT (contextmenu_edit (bool))); 299 } 300 } 301 } 302 #if defined (HAVE_QSCI_VERSION_2_6_0) 303 else 304 { 305 // remove all standard actions from scintilla 306 QList<QAction *> all_actions = context_menu->actions (); 307 308 for (auto *a : all_actions) 309 context_menu->removeAction (a); 310 311 QAction *act 312 = context_menu->addAction (tr ("dbstop if ..."), this, 313 SLOT (contextmenu_break_condition (bool))); 314 act->setData (local_pos); 315 } 316 #endif 317 318 // finally show the menu 319 context_menu->exec (global_pos); 320 #endif 321 } 322 323 // common function with flag for documentation contextmenu_help_doc(bool documentation)324 void octave_qscintilla::contextmenu_help_doc (bool documentation) 325 { 326 if (documentation) 327 emit show_doc_signal (m_word_at_cursor); 328 else 329 emit execute_command_in_terminal_signal ("help " + m_word_at_cursor); 330 } 331 332 // call edit the function related to the current word context_edit(void)333 void octave_qscintilla::context_edit (void) 334 { 335 if (get_actual_word ()) 336 contextmenu_edit (true); 337 } 338 339 // call edit the function related to the current word context_run(void)340 void octave_qscintilla::context_run (void) 341 { 342 if (hasSelectedText ()) 343 { 344 contextmenu_run (true); 345 346 emit interpreter_event 347 ([] (interpreter&) 348 { command_editor::erase_empty_line (false); }); 349 } 350 } 351 get_global_textcursor_pos(QPoint * global_pos,QPoint * local_pos)352 void octave_qscintilla::get_global_textcursor_pos (QPoint *global_pos, 353 QPoint *local_pos) 354 { 355 long position = SendScintilla (SCI_GETCURRENTPOS); 356 long point_x = SendScintilla (SCI_POINTXFROMPOSITION,0,position); 357 long point_y = SendScintilla (SCI_POINTYFROMPOSITION,0,position); 358 *local_pos = QPoint (point_x,point_y); // local cursor position 359 *global_pos = mapToGlobal (*local_pos); // global position of cursor 360 } 361 362 // determine the actual word and whether we are in an octave or matlab script get_actual_word(void)363 bool octave_qscintilla::get_actual_word (void) 364 { 365 QPoint global_pos, local_pos; 366 get_global_textcursor_pos (&global_pos, &local_pos); 367 m_word_at_cursor = wordAtPoint (local_pos); 368 QString lexer_name = lexer ()->lexer (); 369 return ((lexer_name == "octave" || lexer_name == "matlab") 370 && ! m_word_at_cursor.isEmpty ()); 371 } 372 373 // helper function for clearing all indicators of a specific style clear_selection_markers(void)374 void octave_qscintilla::clear_selection_markers (void) 375 { 376 int end_pos = text ().length (); 377 int end_line, end_col; 378 lineIndexFromPosition (end_pos, &end_line, &end_col); 379 clearIndicatorRange (0, 0, end_line, end_col, m_indicator_id); 380 381 markerDeleteAll (marker::selection); 382 } 383 384 // Function returning the true cursor position where the tab length 385 // is taken into account. get_current_position(int * pos,int * line,int * col)386 void octave_qscintilla::get_current_position (int *pos, int *line, int *col) 387 { 388 *pos = SendScintilla (QsciScintillaBase::SCI_GETCURRENTPOS); 389 *line = SendScintilla (QsciScintillaBase::SCI_LINEFROMPOSITION, *pos); 390 *col = SendScintilla (QsciScintillaBase::SCI_GETCOLUMN, *pos); 391 } 392 393 // Function returning the comment string of the current lexer comment_string(bool comment)394 QStringList octave_qscintilla::comment_string (bool comment) 395 { 396 int lexer = SendScintilla (SCI_GETLEXER); 397 398 switch (lexer) 399 { 400 #if defined (HAVE_LEXER_OCTAVE) || defined (HAVE_LEXER_MATLAB) 401 #if defined (HAVE_LEXER_OCTAVE) 402 case SCLEX_OCTAVE: 403 #else 404 case SCLEX_MATLAB: 405 #endif 406 { 407 resource_manager& rmgr = m_octave_qobj.get_resource_manager (); 408 gui_settings *settings = rmgr.get_settings (); 409 int comment_string; 410 411 if (comment) 412 { 413 // The commenting string is requested 414 if (settings->contains (ed_comment_str.key)) 415 // new version (radio buttons) 416 comment_string = settings->value (ed_comment_str).toInt (); 417 else 418 // old version (combo box) 419 comment_string = settings->value (ed_comment_str_old.key, 420 ed_comment_str.def).toInt (); 421 422 return (QStringList (ed_comment_strings.at (comment_string))); 423 } 424 else 425 { 426 QStringList c_str; 427 428 // The possible uncommenting string(s) are requested 429 comment_string = settings->value (ed_uncomment_str).toInt (); 430 431 for (int i = 0; i < ed_comment_strings_count; i++) 432 { 433 if (1 << i & comment_string) 434 c_str.append (ed_comment_strings.at (i)); 435 } 436 437 return c_str; 438 } 439 440 } 441 #endif 442 443 case SCLEX_PERL: 444 case SCLEX_BASH: 445 case SCLEX_DIFF: 446 return QStringList ("#"); 447 448 case SCLEX_CPP: 449 return QStringList ("//"); 450 451 case SCLEX_BATCH: 452 return QStringList ("REM "); 453 } 454 455 return QStringList ("%"); // should never happen 456 } 457 458 459 // provide the style at a specific position get_style(int pos)460 int octave_qscintilla::get_style (int pos) 461 { 462 int position; 463 if (pos < 0) 464 // The positition has to be reduced by 2 for getting the real style (?) 465 position = SendScintilla (QsciScintillaBase::SCI_GETCURRENTPOS) - 2; 466 else 467 position = pos; 468 469 return SendScintilla (QsciScintillaBase::SCI_GETSTYLEAT, position); 470 } 471 472 // Is a specific cursor position in a line or block comment? is_style_comment(int pos)473 int octave_qscintilla::is_style_comment (int pos) 474 { 475 int lexer = SendScintilla (QsciScintillaBase::SCI_GETLEXER); 476 int style = get_style (pos); 477 478 switch (lexer) 479 { 480 case SCLEX_CPP: 481 return (ST_LINE_COMMENT * (style == QsciLexerCPP::CommentLine 482 || style == QsciLexerCPP::CommentLineDoc) 483 + ST_BLOCK_COMMENT * (style == QsciLexerCPP::Comment 484 || style == QsciLexerCPP::CommentDoc 485 || style == QsciLexerCPP::CommentDocKeyword 486 || style == QsciLexerCPP::CommentDocKeywordError)); 487 488 #if defined (HAVE_LEXER_MATLAB) 489 case SCLEX_MATLAB: 490 return (ST_LINE_COMMENT * (style == QsciLexerMatlab::Comment)); 491 #endif 492 #if defined (HAVE_LEXER_OCTAVE) 493 case SCLEX_OCTAVE: 494 return (ST_LINE_COMMENT * (style == QsciLexerOctave::Comment)); 495 #endif 496 497 case SCLEX_PERL: 498 return (ST_LINE_COMMENT * (style == QsciLexerPerl::Comment)); 499 500 case SCLEX_BATCH: 501 return (ST_LINE_COMMENT * (style == QsciLexerBatch::Comment)); 502 503 case SCLEX_DIFF: 504 return (ST_LINE_COMMENT * (style == QsciLexerDiff::Comment)); 505 506 case SCLEX_BASH: 507 return (ST_LINE_COMMENT * (style == QsciLexerBash::Comment)); 508 509 } 510 511 return ST_NONE; 512 } 513 514 // Do smart indentation after if, for, ... smart_indent(bool do_smart_indent,int do_auto_close,int line,int ind_char_width)515 void octave_qscintilla::smart_indent (bool do_smart_indent, int do_auto_close, 516 int line, int ind_char_width) 517 { 518 QString prevline = text (line); 519 520 QRegExp bkey = QRegExp ("^[\t ]*(if|for|while|switch" 521 "|do|function|properties|events|classdef" 522 "|unwind_protect|try" 523 "|parfor|methods)" 524 "[\r]?[\n\t #%]"); 525 // last word except for comments, assuming no ' or " in comment. 526 // rx_end = QRegExp ("(\\w+)[ \t;\r\n]*([%#][^\"']*)?$"); 527 528 // last word except for comments, 529 // allowing % and # in single or double quoted strings 530 // FIXME: This will get confused by transpose. 531 QRegExp ekey = QRegExp ("(?:(?:['\"][^'\"]*['\"])?[^%#]*)*" 532 "(\\w+)[ \t;\r\n]*(?:[%#].*)?$"); 533 534 int bpos = bkey.indexIn (prevline, 0); 535 int epos; 536 537 if (bpos > -1) 538 { 539 // Found keyword after that indentation should be added 540 541 // Check for existing end statement in the same line 542 epos = ekey.indexIn (prevline, bpos); 543 QString first_word = bkey.cap(1); 544 bool inline_end = (epos > -1) && is_end (ekey.cap(1), first_word); 545 546 if (do_smart_indent && ! inline_end) 547 { 548 // Do smart indent in the current line (line+1) 549 indent (line+1); 550 setCursorPosition (line+1, indentation (line+1) / ind_char_width); 551 } 552 553 if (do_auto_close 554 && ! inline_end 555 && ! first_word.contains (QRegExp ("(?:case|otherwise|unwind_protect_cleanup)"))) 556 { 557 // Do auto close 558 auto_close (do_auto_close, line, prevline, first_word); 559 } 560 561 return; 562 } 563 564 QRegExp mkey = QRegExp ("^[\t ]*(?:else|elseif|catch|unwind_protect_cleanup)" 565 "[\r]?[\t #%\n]"); 566 if (prevline.contains (mkey)) 567 { 568 int prev_ind = indentation (line-1); 569 int act_ind = indentation (line); 570 571 if (prev_ind == act_ind) 572 unindent (line); 573 else if (prev_ind > act_ind) 574 { 575 setIndentation (line+1, prev_ind); 576 setCursorPosition (line+1, prev_ind); 577 } 578 return; 579 } 580 581 QRegExp case_key = QRegExp ("^[\t ]*(?:case|otherwise)[\r]?[\t #%\n]"); 582 if (prevline.contains (case_key) && do_smart_indent) 583 { 584 QString last_line = text (line-1); 585 int prev_ind = indentation (line-1); 586 int act_ind = indentation (line); 587 588 if (last_line.contains (QRegExp ("^[\t ]*switch"))) 589 { 590 indent (line+1); 591 act_ind = indentation (line+1); 592 } 593 else 594 { 595 if (prev_ind == act_ind) 596 unindent (line); 597 else if (prev_ind > act_ind) 598 act_ind = prev_ind; 599 } 600 601 setIndentation (line+1, act_ind); 602 setCursorPosition (line+1, act_ind); 603 } 604 605 ekey = QRegExp ("^[\t ]*(?:end|endif|endfor|endwhile|until|endfunction" 606 "|endswitch|end_try_catch|end_unwind_protect)[\r]?[\t #%\n(;]"); 607 if (prevline.contains (ekey)) 608 { 609 if (indentation (line-1) <= indentation (line)) 610 { 611 unindent (line+1); 612 unindent (line); 613 if (prevline.contains ("endswitch")) 614 { 615 // endswitch has to me unndented twice 616 unindent (line+1); 617 unindent (line); 618 } 619 setCursorPosition (line+1, 620 indentation (line)); 621 } 622 return; 623 } 624 } 625 626 // Do smart indentation of current selection or line. smart_indent_line_or_selected_text(int lineFrom,int lineTo)627 void octave_qscintilla::smart_indent_line_or_selected_text (int lineFrom, 628 int lineTo) 629 { 630 QRegExp blank_line_regexp = QRegExp ("^[\t ]*$"); 631 632 // end[xxxxx] [# comment] at end of a line 633 QRegExp end_word_regexp 634 = QRegExp ("(?:(?:['\"][^'\"]*['\"])?[^%#]*)*" 635 "(?:end\\w*)[\r\n\t ;]*(?:[%#].*)?$"); 636 637 QRegExp begin_block_regexp 638 = QRegExp ("^[\t ]*(?:if|elseif|else" 639 "|for|while|do|parfor" 640 "|switch|case|otherwise" 641 "|function" 642 "|classdef|properties|events|enumeration|methods" 643 "|unwind_protect|unwind_protect_cleanup|try|catch)" 644 "[\r\n\t #%]"); 645 646 QRegExp mid_block_regexp 647 = QRegExp ("^[\t ]*(?:elseif|else" 648 "|unwind_protect_cleanup|catch)" 649 "[\r\n\t #%]"); 650 651 QRegExp end_block_regexp 652 = QRegExp ("^[\t ]*(?:end" 653 "|end(for|function|if|parfor|switch|while" 654 "|classdef|enumeration|events|methods|properties)" 655 "|end_(try_catch|unwind_protect)" 656 "|until)" 657 "[\r\n\t #%]"); 658 659 QRegExp case_block_regexp 660 = QRegExp ("^[\t ]*(?:case|otherwise)" 661 "[\r\n\t #%]"); 662 663 int indent_column = -1; 664 int indent_increment = indentationWidth (); 665 bool in_switch = false; 666 667 for (int line = lineFrom-1; line >= 0; line--) 668 { 669 QString line_text = text (line); 670 671 if (blank_line_regexp.indexIn (line_text) < 0) 672 { 673 // Found first non-blank line above beginning of region or 674 // current line. Base indentation from this line, increasing 675 // indentation by indentationWidth if it looks like the 676 // beginning of a code block. 677 678 indent_column = indentation (line); 679 680 if (begin_block_regexp.indexIn (line_text) > -1) 681 { 682 indent_column += indent_increment; 683 if (line_text.contains ("switch")) 684 in_switch = true; 685 } 686 687 break; 688 } 689 } 690 691 if (indent_column < 0) 692 indent_column = indentation (lineFrom); 693 694 QString prev_line; 695 for (int line = lineFrom; line <= lineTo; line++) 696 { 697 QString line_text = text (line); 698 699 if (end_block_regexp.indexIn (line_text) > -1) 700 { 701 indent_column -= indent_increment; 702 if (line_text.contains ("endswitch")) 703 { 704 // need a double de-indent for endswitch 705 if (in_switch) 706 indent_column -= indent_increment; 707 in_switch = false; 708 } 709 } 710 711 if (mid_block_regexp.indexIn (line_text) > -1) 712 indent_column -= indent_increment; 713 714 if (case_block_regexp.indexIn (line_text) > -1) 715 { 716 if (case_block_regexp.indexIn (prev_line) < 0 717 && !prev_line.contains("switch")) 718 indent_column -= indent_increment; 719 in_switch = true; 720 } 721 722 setIndentation (line, indent_column); 723 724 725 int bpos = begin_block_regexp.indexIn (line_text); 726 if (bpos > -1) 727 { 728 // Check for existing end statement in the same line 729 int epos = end_word_regexp.indexIn (line_text, bpos); 730 if (epos == -1) 731 indent_column += indent_increment; 732 if (line_text.contains ("switch")) 733 in_switch = true; 734 } 735 736 if (blank_line_regexp.indexIn (line_text) < 0) 737 prev_line = line_text; 738 } 739 } 740 set_word_selection(const QString & word)741 void octave_qscintilla::set_word_selection (const QString& word) 742 { 743 m_selection = word; 744 745 if (word.isEmpty ()) 746 { 747 m_selection_line = -1; 748 m_selection_col = -1; 749 750 m_selection_replacement = ""; 751 752 clear_selection_markers (); 753 754 QToolTip::hideText (); 755 } 756 else 757 { 758 int pos; 759 get_current_position (&pos, &m_selection_line, &m_selection_col); 760 } 761 } 762 show_selection_markers(int l1,int c1,int l2,int c2)763 void octave_qscintilla::show_selection_markers (int l1, int c1, int l2, int c2) 764 { 765 fillIndicatorRange (l1, c1, l2, c2, m_indicator_id); 766 767 if (l1 == l2) 768 markerAdd (l1, marker::selection); 769 } 770 contextmenu_help(bool)771 void octave_qscintilla::contextmenu_help (bool) 772 { 773 contextmenu_help_doc (false); 774 } 775 contextmenu_doc(bool)776 void octave_qscintilla::contextmenu_doc (bool) 777 { 778 contextmenu_help_doc (true); 779 } 780 context_help_doc(bool documentation)781 void octave_qscintilla::context_help_doc (bool documentation) 782 { 783 if (get_actual_word ()) 784 contextmenu_help_doc (documentation); 785 } 786 contextmenu_edit(bool)787 void octave_qscintilla::contextmenu_edit (bool) 788 { 789 emit context_menu_edit_signal (m_word_at_cursor); 790 } 791 contextmenu_run_temp_error(void)792 void octave_qscintilla::contextmenu_run_temp_error (void) 793 { 794 QMessageBox::critical (this, tr ("Octave Editor"), 795 tr ("Creating temporary files failed.\n" 796 "Make sure you have write access to temp. directory\n" 797 "%1\n\n" 798 "\"Run Selection\" requires temporary files.").arg (QDir::tempPath ())); 799 } 800 contextmenu_run(bool)801 void octave_qscintilla::contextmenu_run (bool) 802 { 803 resource_manager& rmgr = m_octave_qobj.get_resource_manager (); 804 805 // Take selected code and extend it by commands for echoing each 806 // evaluated line and for adding the line to the history (use script) 807 QString code = QString (); 808 QString hist = QString (); 809 810 // Split contents into single lines and complete commands 811 QStringList lines = selectedText ().split (QRegExp ("[\r\n]"), 812 #if defined (HAVE_QT_SPLITBEHAVIOR_ENUM) 813 Qt::SkipEmptyParts); 814 #else 815 QString::SkipEmptyParts); 816 #endif 817 for (int i = 0; i < lines.count (); i++) 818 { 819 QString line = lines.at (i); 820 if (line.trimmed ().isEmpty ()) 821 continue; 822 QString line_escaped = line; 823 line_escaped.replace (QString ("'"), QString ("''")); 824 QString line_history = line; 825 826 // Prevent output of breakpoint in temp. file for keyboard 827 QString next_bp_quiet; 828 QString next_bp_quiet_reset; 829 if (line.contains ("keyboard")) 830 { 831 // Define commands for not showing bp location and for resetting 832 // this in case "keyboard" was within a comment 833 next_bp_quiet = "__db_next_breakpoint_quiet__;\n"; 834 next_bp_quiet_reset = "\n__db_next_breakpoint_quiet__(false);"; 835 } 836 837 // Add codeline 838 code += next_bp_quiet + line + next_bp_quiet_reset + "\n"; 839 hist += line_history + "\n"; 840 } 841 842 octave_stdout << hist.toStdString (); 843 844 // Create tmp file with the code to be executed by the interpreter 845 QPointer<QTemporaryFile> tmp_file 846 = rmgr.create_tmp_file ("m", code); 847 848 bool tmp = (tmp_file && tmp_file->open ()); 849 if (! tmp) 850 { 851 // tmp files not working: use old way to run selection 852 contextmenu_run_temp_error (); 853 return; 854 } 855 856 tmp_file->close (); 857 858 // Create tmp file required for adding command to history 859 QPointer<QTemporaryFile> tmp_hist 860 = rmgr.create_tmp_file ("", hist); // empty tmp file for history 861 862 tmp = (tmp_hist && tmp_hist->open ()); 863 if (! tmp) 864 { 865 // tmp files not working: use old way to run selection 866 contextmenu_run_temp_error (); 867 return; 868 } 869 870 tmp_hist->close (); 871 872 // Add commands to the history 873 emit interpreter_event 874 ([tmp_hist] (interpreter& interp) 875 { 876 // INTERPRETER THREAD 877 878 std::string opt = "-r"; 879 std::string path = tmp_hist->fileName ().toStdString (); 880 881 Fhistory (interp, ovl (opt, path)); 882 }); 883 884 // Disable opening a file at a breakpoint in case keyboard () is used 885 gui_settings* settings = rmgr.get_settings (); 886 bool show_dbg_file = settings->value (ed_show_dbg_file).toBool (); 887 settings->setValue (ed_show_dbg_file.key, false); 888 889 // Let the interpreter execute the tmp file 890 emit interpreter_event 891 ([this, tmp_file, tmp_hist, show_dbg_file] (interpreter& interp) 892 { 893 // INTERPRETER THREAD 894 895 std::string file = tmp_file->fileName ().toStdString (); 896 897 std::string pending_input = command_editor::get_current_line (); 898 899 int err_line = -1; // For storing the line of a poss. error 900 901 // Get current state of auto command repeat in debug mode 902 octave_value_list ovl_dbg = Fisdebugmode (interp); 903 bool dbg = ovl_dbg(0).bool_value (); 904 octave_value_list ovl_auto_repeat = ovl (true); 905 if (dbg) 906 ovl_auto_repeat = Fauto_repeat_debug_command (interp, ovl (false), 1); 907 bool auto_repeat = ovl_auto_repeat(0).bool_value (); 908 909 try 910 { 911 // Do the job 912 interp.source_file (file); 913 } 914 catch (const execution_exception& e) 915 { 916 // Catch errors otherwise the rest of the interpreter 917 // will not be executed (cleaning up). 918 919 // New error message and error stack 920 QString new_msg = QString::fromStdString (e.message ()); 921 std::list<frame_info> stack = e.stack_info (); 922 923 // Remove line and column from first line of error message only 924 // if it is related to the tmp itself, i.e. only if the 925 // the error stack size is 0 or 1 926 if (stack.size () < 2) 927 { 928 QRegExp rx ("source: error sourcing file [^\n]*$"); 929 if (new_msg.contains (rx)) 930 { 931 // Selected code has syntax errors 932 new_msg.replace (rx, "error sourcing selected code"); 933 err_line = 0; // Nothing into history? 934 } 935 else 936 { 937 // Normal error, detect line and remove file 938 // name from message 939 QStringList rx_list; 940 rx_list << "near line (\\d+),[^\n]*\n"; 941 rx_list << "near line (\\d+),[^\n]*$"; 942 943 QStringList replace_list; 944 replace_list << "\n"; 945 replace_list << ""; 946 947 for (int i = 0; i < rx_list.length (); i++) 948 { 949 int pos = 0; 950 rx = QRegExp (rx_list.at (i)); 951 pos = rx.indexIn (new_msg, pos); 952 if (pos != -1) 953 { 954 err_line = rx.cap (1).toInt (); 955 new_msg = new_msg.replace (rx, replace_list.at (i)); 956 } 957 } 958 } 959 } 960 961 // Drop first stack level, i.e. temporary function file 962 if (stack.size () > 0) 963 stack.pop_back (); 964 965 // Clean up before throwing the modified error. 966 emit ctx_menu_run_finished_signal (show_dbg_file, err_line, 967 tmp_file, tmp_hist, 968 dbg, auto_repeat); 969 970 // New exception with updated message and stack 971 octave::execution_exception ee (e.err_type (),e.identifier (), 972 new_msg.toStdString (), stack); 973 974 // Throw 975 throw (ee); 976 } 977 978 // Clean up 979 980 emit ctx_menu_run_finished_signal (show_dbg_file, err_line, 981 tmp_file, tmp_hist, 982 dbg, auto_repeat); 983 984 command_editor::erase_empty_line (true); 985 command_editor::replace_line (""); 986 command_editor::set_initial_input (pending_input); 987 command_editor::redisplay (); 988 command_editor::interrupt_event_loop (); 989 command_editor::accept_line (); 990 command_editor::erase_empty_line (true); 991 992 }); 993 } 994 ctx_menu_run_finished(bool show_dbg_file,int,QTemporaryFile * tmp_file,QTemporaryFile * tmp_hist,bool dbg,bool auto_repeat)995 void octave_qscintilla::ctx_menu_run_finished (bool show_dbg_file, int, 996 QTemporaryFile* tmp_file, QTemporaryFile* tmp_hist, 997 bool dbg, bool auto_repeat) 998 { 999 emit focus_console_after_command_signal (); 1000 1001 // TODO: Use line nr. (int argument) of possible error for removing 1002 // lines from history that were never executed. For this, 1003 // possible lines from commands at a debug prompt must be 1004 // taken into consideration. 1005 resource_manager& rmgr = m_octave_qobj.get_resource_manager (); 1006 gui_settings *settings = rmgr.get_settings (); 1007 settings->setValue (ed_show_dbg_file.key, show_dbg_file); 1008 rmgr.remove_tmp_file (tmp_file); 1009 rmgr.remove_tmp_file (tmp_hist); 1010 1011 emit interpreter_event 1012 ([this, dbg, auto_repeat] (interpreter& interp) 1013 { 1014 // INTERPRETER THREAD 1015 if (dbg) 1016 Fauto_repeat_debug_command (interp, ovl (auto_repeat)); 1017 }); 1018 } 1019 1020 1021 // wrappers for dbstop related context menu items 1022 1023 // FIXME: Why can't the data be sent as the argument to the function??? contextmenu_break_condition(bool)1024 void octave_qscintilla::contextmenu_break_condition (bool) 1025 { 1026 #if defined (HAVE_QSCI_VERSION_2_6_0) 1027 QAction *action = qobject_cast<QAction *>(sender ()); 1028 QPoint local_pos = action->data ().value<QPoint> (); 1029 1030 // pick point just right of margins, so lineAt doesn't give -1 1031 int margins = marginWidth (1) + marginWidth (2) + marginWidth (3); 1032 local_pos = QPoint (margins + 1, local_pos.y ()); 1033 1034 emit context_menu_break_condition_signal (lineAt (local_pos)); 1035 #endif 1036 } 1037 contextmenu_break_once(const QPoint & local_pos)1038 void octave_qscintilla::contextmenu_break_once (const QPoint& local_pos) 1039 { 1040 #if defined (HAVE_QSCI_VERSION_2_6_0) 1041 emit context_menu_break_once (lineAt (local_pos)); 1042 #endif 1043 } 1044 text_changed(void)1045 void octave_qscintilla::text_changed (void) 1046 { 1047 emit status_update (isUndoAvailable (), isRedoAvailable ()); 1048 } 1049 cursor_position_changed(int line,int col)1050 void octave_qscintilla::cursor_position_changed (int line, int col) 1051 { 1052 // Clear the selection if we move away from it. We have to check the 1053 // position, because we allow entering text at the point of the 1054 // selection to trigger a search and replace that does not clear the 1055 // selection until it is complete. 1056 1057 if (! m_selection.isEmpty () 1058 && (line != m_selection_line || col != m_selection_col)) 1059 set_word_selection (); 1060 } 1061 1062 // when edit area gets focus update information on undo/redo actions focusInEvent(QFocusEvent * focusEvent)1063 void octave_qscintilla::focusInEvent (QFocusEvent *focusEvent) 1064 { 1065 emit status_update (isUndoAvailable (), isRedoAvailable ()); 1066 1067 QsciScintilla::focusInEvent (focusEvent); 1068 } 1069 show_replace_action_tooltip(void)1070 void octave_qscintilla::show_replace_action_tooltip (void) 1071 { 1072 int pos; 1073 get_current_position (&pos, &m_selection_line, &m_selection_col); 1074 1075 // Offer to replace other instances. 1076 1077 QKeySequence keyseq = Qt::SHIFT + Qt::Key_Return; 1078 1079 QString msg = (tr ("Press '%1' to replace all occurrences of '%2' with '%3'.") 1080 . arg (keyseq.toString ()) 1081 . arg (m_selection) 1082 . arg (m_selection_replacement)); 1083 1084 QPoint global_pos; 1085 QPoint local_pos; 1086 1087 get_global_textcursor_pos (&global_pos, &local_pos); 1088 1089 QFontMetrics ttfm (QToolTip::font ()); 1090 1091 // Try to avoid overlapping with the text completion dialog 1092 // and the text that is currently being edited. 1093 1094 global_pos += QPoint (2*ttfm.maxWidth (), -3*ttfm.height ()); 1095 1096 QToolTip::showText (global_pos, msg); 1097 } 1098 keyPressEvent(QKeyEvent * key_event)1099 void octave_qscintilla::keyPressEvent (QKeyEvent *key_event) 1100 { 1101 if (m_selection.isEmpty ()) 1102 QsciScintilla::keyPressEvent (key_event); 1103 else 1104 { 1105 int key = key_event->key (); 1106 Qt::KeyboardModifiers modifiers = key_event->modifiers (); 1107 1108 if (key == Qt::Key_Return && modifiers == Qt::ShiftModifier) 1109 { 1110 // get the resulting cursor position 1111 // (required if click was beyond a line ending) 1112 int pos, line, col; 1113 get_current_position (&pos, &line, &col); 1114 1115 // remember first visible line for restoring the view afterwards 1116 int first_line = firstVisibleLine (); 1117 1118 // search for first occurrence of the detected word 1119 bool find_result_available 1120 = findFirst (m_selection, 1121 false, // no regexp 1122 true, // case sensitive 1123 true, // whole words only 1124 false, // do not wrap 1125 true, // forward 1126 0, 0, // from the beginning 1127 false 1128 #if defined (HAVE_QSCI_VERSION_2_6_0) 1129 , true 1130 #endif 1131 ); 1132 1133 while (find_result_available) 1134 { 1135 replace (m_selection_replacement); 1136 1137 // FIXME: is this the right thing to do? findNext doesn't 1138 // work properly if the length of the replacement text is 1139 // different from the original. 1140 1141 int new_line, new_col; 1142 get_current_position (&pos, &new_line, &new_col); 1143 1144 find_result_available 1145 = findFirst (m_selection, 1146 false, // no regexp 1147 true, // case sensitive 1148 true, // whole words only 1149 false, // do not wrap 1150 true, // forward 1151 new_line, new_col, // from new pos 1152 false 1153 #if defined (HAVE_QSCI_VERSION_2_6_0) 1154 , true 1155 #endif 1156 ); 1157 } 1158 1159 // restore the visible area of the file, the cursor position, 1160 // and the selection 1161 setFirstVisibleLine (first_line); 1162 setCursorPosition (line, col); 1163 1164 // Clear the selection. 1165 set_word_selection (); 1166 } 1167 else 1168 { 1169 // The idea here is to allow backspace to remove the last 1170 // character of the replacement text to allow minimal editing 1171 // and to also end the selection replacement action if text is 1172 // not valid as a word constituent (control characters, 1173 // etc.). Is there a better way than having special cases for 1174 // DEL and ESC here? 1175 1176 QString text = key_event->text (); 1177 1178 bool cancel_replacement = false; 1179 1180 if (key == Qt::Key_Backspace) 1181 { 1182 if (m_selection_replacement.isEmpty ()) 1183 cancel_replacement = true; 1184 else 1185 m_selection_replacement.chop (1); 1186 } 1187 else if (key == Qt::Key_Delete || key == Qt::Key_Escape) 1188 cancel_replacement = true; 1189 else if (! text.isEmpty ()) 1190 m_selection_replacement += text; 1191 else if (modifiers != Qt::ShiftModifier) 1192 cancel_replacement = true; 1193 1194 // Perform default action. 1195 1196 QsciScintilla::keyPressEvent (key_event); 1197 1198 if (cancel_replacement) 1199 set_word_selection (); 1200 1201 if (! m_selection_replacement.isEmpty ()) 1202 show_replace_action_tooltip (); 1203 } 1204 } 1205 } 1206 auto_close(int auto_endif,int linenr,const QString & line,QString & first_word)1207 void octave_qscintilla::auto_close (int auto_endif, int linenr, 1208 const QString& line, QString& first_word) 1209 { 1210 // Insert an "end" for an "if" etc., if needed. 1211 // (Use of "while" allows "return" to skip the rest. 1212 // It may be clearer to use "if" and "goto", 1213 // but that violates the coding standards.) 1214 1215 bool autofill_simple_end = (auto_endif == 2); 1216 1217 std::size_t start = line.toStdString ().find_first_not_of (" \t"); 1218 1219 // Check if following line has the same or less indentation 1220 // Check if the following line does not start with 1221 // end* (until) (catch) 1222 if (linenr < lines () - 1) 1223 { 1224 int offset = 2; // linenr is the old line, thus, linnr+1 is the 1225 // new one and can not be taken into account 1226 std::size_t next_start; 1227 QString next_line; 1228 1229 do // find next non-blank line 1230 { 1231 next_line = text (linenr + offset++); 1232 next_start = next_line.toStdString ().find_first_not_of (" \t\n"); 1233 } 1234 while (linenr + offset < lines () 1235 && next_start == std::string::npos); 1236 1237 if (next_start == std::string::npos) 1238 next_start = 0; 1239 if (start == 0 && next_start == 0) 1240 return; // bug #56160, don't add at 0 1241 if (next_start > start) // more indented => don't add "end" 1242 return; 1243 if (next_start == start) // same => check if already is "end" 1244 { 1245 QRegExp rx_start = QRegExp (R"((\w+))"); 1246 int tmp = rx_start.indexIn (next_line, start); 1247 if (tmp != -1 && is_end (rx_start.cap(1), first_word)) 1248 return; 1249 } 1250 } 1251 1252 // If all of the above, insert a new line, with matching indent 1253 // and either 'end' or 'end...', depending on a flag. 1254 1255 // If we insert directly after the last line, the "end" is autoindented, 1256 // so add a dummy line. 1257 if (linenr + 2 == lines ()) 1258 insertAt (QString ("\n"), linenr + 2, 0); 1259 1260 // For try/catch/end, fill "end" first, so "catch" is top of undo stack 1261 if (first_word == "try") 1262 insertAt (QString (start, ' ') 1263 + (autofill_simple_end ? "end\n" : "end_try_catch\n"), 1264 linenr + 2, 0); 1265 else if (first_word == "unwind_protect") 1266 insertAt (QString (start, ' ') 1267 + (autofill_simple_end ? "end\n" : "end_unwind_protect\n"), 1268 linenr + 2, 0); 1269 1270 QString next_line; 1271 if (first_word == "do") 1272 next_line = "until\n"; 1273 else if (first_word == "try") 1274 next_line = "catch\n"; 1275 else if (first_word == "unwind_protect") 1276 next_line = "unwind_protect_cleanup\n"; 1277 else if (autofill_simple_end) 1278 next_line = "end\n"; 1279 else 1280 { 1281 if (first_word == "unwind_protect") 1282 first_word = '_' + first_word; 1283 next_line = "end" + first_word + "\n"; 1284 } 1285 1286 //insertAt (QString (start, ' ') + next_line, linenr + 2, 0); 1287 insertAt (next_line, linenr + 2, 0); 1288 setIndentation (linenr + 2, indentation (linenr)); 1289 } 1290 dragEnterEvent(QDragEnterEvent * e)1291 void octave_qscintilla::dragEnterEvent (QDragEnterEvent *e) 1292 { 1293 // if is not dragging a url, pass to qscintilla to handle, 1294 // otherwise ignore it so that it will be handled by 1295 // the parent 1296 if (!e->mimeData ()->hasUrls ()) 1297 { 1298 QsciScintilla::dragEnterEvent (e); 1299 } 1300 else 1301 { 1302 e->ignore(); 1303 } 1304 } 1305 } 1306 1307 #endif 1308