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