30
30
QInputDialog, QTextBlockUserData, QLineEdit,
31
31
QKeySequence, QWidget, QVBoxLayout,
32
32
QHBoxLayout, QDialog, QIntValidator,
33
QDialogButtonBox, QGridLayout)
34
from spyderlib.qt.QtCore import (Qt, SIGNAL, QTimer, QRect, QRegExp, QSize,
33
QDialogButtonBox, QGridLayout, QPaintEvent)
34
from spyderlib.qt.QtCore import (Qt, SIGNAL, Signal, QTimer, QRect, QRegExp,
36
36
from spyderlib.qt.compat import to_qvariant
38
38
#%% This line is for cell execution testing
45
45
from spyderlib.utils.qthelpers import (add_actions, create_action, keybinding,
46
46
mimedata2url, get_icon)
47
47
from spyderlib.utils.dochelpers import getobj
48
from spyderlib.utils import encoding, sourcecode
49
from spyderlib.utils.sourcecode import ALL_LANGUAGES
48
from spyderlib.utils import encoding, sourcecode, programs
49
from spyderlib.utils.sourcecode import ALL_LANGUAGES, CELL_LANGUAGES
50
50
from spyderlib.widgets.editortools import PythonCFM
51
51
from spyderlib.widgets.sourcecode.base import TextEditBaseWidget
52
52
from spyderlib.widgets.sourcecode import syntaxhighlighters as sh
53
53
from spyderlib.py3compat import to_text_string
55
if programs.is_module_installed('IPython'):
56
import IPython.nbformat as nbformat
57
import IPython.nbformat.current # in IPython 0.13.2, current is not loaded
60
from IPython.nbconvert import PythonExporter as nbexporter # >= 1.0
55
66
#%% This line is for cell execution testing
56
67
# For debugging purpose:
57
68
LOG_FILENAME = get_conf_path('codeeditor.log')
287
314
class CodeEditor(TextEditBaseWidget):
288
315
"""Source Code Editor Widget based exclusively on Qt"""
289
LANGUAGES ={ 'Python': (sh.PythonSH, '#', PythonCFM),
317
LANGUAGES = {'Python': (sh.PythonSH, '#', PythonCFM),
290
318
'Cython': (sh.CythonSH, '#', PythonCFM),
291
319
'Fortran77': (sh.Fortran77SH, 'c', None),
292
320
'Fortran': (sh.FortranSH, '!', None),
299
327
'Css': (sh.CssSH, '', None),
300
328
'Xml': (sh.XmlSH, '', None),
301
329
'Js': (sh.JsSH, '//', None),
330
'Json': (sh.JsonSH, '', None),
302
331
'Julia': (sh.JuliaSH, '#', None),
332
'Yaml': (sh.YamlSH, '#', None),
303
333
'Cpp': (sh.CppSH, '//', None),
304
334
'OpenCL': (sh.OpenCLSH, '//', None),
305
335
'Batch': (sh.BatchSH, 'rem ', None),
306
336
'Ini': (sh.IniSH, '#', None),
307
337
'Enaml': (sh.EnamlSH, '#', PythonCFM),
310
341
import pygments # analysis:ignore
311
342
except ImportError:
354
389
self.update_linenumberarea_width)
355
390
self.connect(self, SIGNAL("updateRequest(QRect,int)"),
356
391
self.update_linenumberarea)
392
self.linenumberarea_pressed = -1
393
self.linenumberarea_released = -1
395
# Colors to be defined in _apply_highlighter_color_scheme()
396
# Currentcell color and current line color are defined in base.py
397
self.occurence_color = None
398
self.ctrl_click_color = None
399
self.sideareas_color = None
400
self.matched_p_color = None
401
self.unmatched_p_color = None
402
self.normal_color = None
403
self.comment_color = None
405
self.linenumbers_color = QColor(Qt.darkGray)
359
407
# --- Syntax highlight entrypoint ---
444
493
self.setMouseTracking(True)
445
494
self.__cursor_changed = False
446
495
self.ctrl_click_color = QColor(Qt.blue)
449
498
self.breakpoints = self.get_breakpoints()
451
500
# Keyboard shortcuts
452
501
self.shortcuts = self.create_shortcuts()
504
self.__visible_blocks = [] # Visible blocks, update with repaint
505
self.painted.connect(self._draw_editor_cell_divider)
507
self.connect(self.verticalScrollBar(), SIGNAL('valueChanged(int)'),
508
lambda value: self.rehighlight_cells())
454
510
def create_shortcuts(self):
455
511
codecomp = create_shortcut(self.do_code_completion, context='Editor',
456
512
name='Code completion', parent=self)
647
705
self._set_highlighter(sh_class)
649
707
def _set_highlighter(self, sh_class):
650
if self.highlighter_class is not sh_class:
651
self.highlighter_class = sh_class
652
if self.highlighter is not None:
653
# Removing old highlighter
654
# TODO: test if leaving parent/document as is eats memory
655
self.highlighter.setParent(None)
656
self.highlighter.setDocument(None)
657
self.highlighter = self.highlighter_class(self.document(),
658
self.font(), self.color_scheme)
659
self._apply_highlighter_color_scheme()
708
self.highlighter_class = sh_class
709
if self.highlighter is not None:
710
# Removing old highlighter
711
# TODO: test if leaving parent/document as is eats memory
712
self.highlighter.setParent(None)
713
self.highlighter.setDocument(None)
714
self.highlighter = self.highlighter_class(self.document(),
717
self._apply_highlighter_color_scheme()
720
return self.highlighter_class is sh.JsonSH
661
722
def is_python(self):
662
723
return self.highlighter_class is sh.PythonSH
964
1029
def linenumberarea_paint_event(self, event):
965
1030
"""Painting line number area"""
1031
painter = QPainter(self.linenumberarea)
1032
painter.fillRect(event.rect(), self.sideareas_color)
1033
font = painter.font()
966
1034
font_height = self.fontMetrics().height()
967
painter = QPainter(self.linenumberarea)
968
painter.fillRect(event.rect(), self.area_background_color)
970
block = self.firstVisibleBlock()
971
block_number = block.blockNumber()
972
top = self.blockBoundingGeometry(block).translated(
973
self.contentOffset()).top()
974
bottom = top + self.blockBoundingRect(block).height()
1036
active_block = self.textCursor().block()
1037
active_line_number = active_block.blockNumber() + 1
976
1039
def draw_pixmap(ytop, pixmap):
977
painter.drawPixmap(0, ytop+(font_height-pixmap.height())/2, pixmap)
978
while block.isValid() and top <= event.rect().bottom():
979
if block.isVisible() and bottom >= event.rect().top():
980
line_number = block_number+1
981
painter.setPen(Qt.darkGray)
982
if self.is_cell_separator(block):
983
painter.setPen(Qt.red)
984
painter.drawLine(0, top, self.linenumberarea.width(), top)
985
if self.linenumbers_margin:
986
painter.drawText(0, top, self.linenumberarea.width(),
987
font_height, Qt.AlignRight|Qt.AlignBottom,
988
to_text_string(line_number))
989
data = block.userData()
990
if self.markers_margin and data:
991
if data.code_analysis:
992
for _message, error in data.code_analysis:
1040
painter.drawPixmap(0, ytop + (font_height-pixmap.height()) / 2,
1043
for top, line_number, block in self.visible_blocks:
1044
if self.linenumbers_margin:
1045
if line_number == active_line_number:
1046
font.setWeight(font.Bold)
1047
painter.setFont(font)
1048
painter.setPen(self.normal_color)
1050
font.setWeight(font.Normal)
1051
painter.setFont(font)
1052
painter.setPen(self.linenumbers_color)
1054
painter.drawText(0, top, self.linenumberarea.width(),
1056
Qt.AlignRight | Qt.AlignBottom,
1057
to_text_string(line_number))
1059
data = block.userData()
1060
if self.markers_margin and data:
1061
if data.code_analysis:
1062
for _message, error in data.code_analysis:
996
draw_pixmap(top, self.error_pixmap)
998
draw_pixmap(top, self.warning_pixmap)
1000
draw_pixmap(top, self.todo_pixmap)
1002
if data.breakpoint_condition is None:
1003
draw_pixmap(top, self.bp_pixmap)
1005
draw_pixmap(top, self.bpc_pixmap)
1007
block = block.next()
1009
bottom = top + self.blockBoundingRect(block).height()
1066
draw_pixmap(top, self.error_pixmap)
1068
draw_pixmap(top, self.warning_pixmap)
1070
draw_pixmap(top, self.todo_pixmap)
1072
if data.breakpoint_condition is None:
1073
draw_pixmap(top, self.bp_pixmap)
1075
draw_pixmap(top, self.bpc_pixmap)
1012
1077
def __get_linenumber_from_mouse_event(self, event):
1013
1078
"""Return line number from mouse event"""
1030
1095
line_number = self.__get_linenumber_from_mouse_event(event)
1031
1096
block = self.document().findBlockByNumber(line_number-1)
1032
1097
data = block.userData()
1033
if data and data.code_analysis:
1099
# this disables pyflakes messages if there is an active drag/selection
1101
check = self.linenumberarea_released == -1
1102
if data and data.code_analysis and check:
1034
1103
self.__show_code_analysis_results(line_number, data.code_analysis)
1105
if event.buttons() == Qt.LeftButton:
1106
self.linenumberarea_released = line_number
1107
self.linenumberarea_select_lines(self.linenumberarea_pressed,
1108
self.linenumberarea_released)
1036
1110
def linenumberarea_mousedoubleclick_event(self, event):
1037
1111
"""Handling line number area mouse double-click event"""
1038
1112
line_number = self.__get_linenumber_from_mouse_event(event)
1039
1113
shift = event.modifiers() & Qt.ShiftModifier
1040
1114
self.add_remove_breakpoint(line_number, edit_condition=shift)
1116
def linenumberarea_mousepress_event(self, event):
1117
"""Handling line number area mouse double press event"""
1118
line_number = self.__get_linenumber_from_mouse_event(event)
1119
self.linenumberarea_pressed = line_number
1120
self.linenumberarea_released = line_number
1121
self.linenumberarea_select_lines(self.linenumberarea_pressed,
1122
self.linenumberarea_released)
1124
def linenumberarea_mouserelease_event(self, event):
1125
"""Handling line number area mouse release event"""
1126
self.linenumberarea_released = -1
1127
self.linenumberarea_pressed = -1
1129
def linenumberarea_select_lines(self, linenumber_pressed,
1130
linenumber_released):
1131
"""Select line(s) after a mouse press/mouse press drag event"""
1132
find_block_by_line_number = self.document().findBlockByLineNumber
1133
move_n_blocks = (linenumber_released - linenumber_pressed)
1134
start_line = linenumber_pressed
1135
start_block = find_block_by_line_number(start_line - 1)
1137
cursor = self.textCursor()
1138
cursor.setPosition(start_block.position())
1140
# Select/drag downwards
1141
if move_n_blocks > 0:
1142
for n in range(abs(move_n_blocks) + 1):
1143
cursor.movePosition(cursor.NextBlock, cursor.KeepAnchor)
1144
# Select/drag upwards or select single line
1146
cursor.movePosition(cursor.NextBlock)
1147
for n in range(abs(move_n_blocks) + 1):
1148
cursor.movePosition(cursor.PreviousBlock, cursor.KeepAnchor)
1150
# Account for last line case
1151
if linenumber_released == self.blockCount():
1152
cursor.movePosition(cursor.EndOfBlock, cursor.KeepAnchor)
1154
cursor.movePosition(cursor.StartOfBlock, cursor.KeepAnchor)
1156
self.setTextCursor(cursor)
1042
1158
#------Breakpoints
1043
1159
def add_remove_breakpoint(self, line_number=None, condition=None,
1044
1160
edit_condition=False):
1052
1168
data = block.userData()
1054
1170
data.breakpoint = not data.breakpoint
1171
old_breakpoint_condition = data.breakpoint_condition
1055
1172
data.breakpoint_condition = None
1057
1174
data = BlockUserData(self)
1058
1175
data.breakpoint = True
1176
old_breakpoint_condition = None
1059
1177
if condition is not None:
1060
1178
data.breakpoint_condition = condition
1061
1179
if edit_condition:
1062
1180
data.breakpoint = True
1063
1181
condition = data.breakpoint_condition
1064
if condition is None:
1182
if old_breakpoint_condition is not None:
1183
condition = old_breakpoint_condition
1066
1184
condition, valid = QInputDialog.getText(self,
1067
1185
_('Breakpoint'),
1068
1186
_("Condition:"),
1718
1839
cursor.endEditBlock()
1842
def clear_all_output(self):
1843
"""removes all ouput in the ipynb format (Json only)"""
1844
if self.is_json() and nbformat is not None:
1845
nb = nbformat.current.reads(self.toPlainText(), 'json')
1847
for cell in nb.worksheets[0].cells:
1848
if 'outputs' in cell:
1849
cell['outputs'] = []
1850
if 'prompt_number' in cell:
1851
cell['prompt_number'] = None
1852
# We do the following rather than using self.setPlainText
1853
# to benefit from QTextEdit's undo/redo feature.
1855
self.insertPlainText(nbformat.current.writes(nb, 'json'))
1859
sig_new_file = Signal(str)
1861
def convert_notebook(self):
1862
"""Convert an IPython notebook to a Python script in editor"""
1863
if nbformat is not None:
1864
nb = nbformat.current.reads(self.toPlainText(), 'json')
1865
# Use writes_py if nbconvert is not available
1866
if nbexporter is None:
1867
script = nbformat.current.writes_py(nb)
1869
script = nbexporter().from_notebook_node(nb)[0]
1870
self.sig_new_file.emit(script)
1721
1872
def indent(self, force=False):
1723
1874
Indent current line or selection
2099
2250
_("Comment")+"/"+_("Uncomment"),
2100
2251
icon=get_icon("comment.png"),
2101
2252
triggered=self.toggle_comment)
2253
self.clear_all_output_action = create_action(self,
2254
_("Clear all ouput"), icon='ipython_console.png',
2255
triggered=self.clear_all_output)
2256
self.ipynb_convert_action = create_action(self, _("Convert to Python script"),
2257
triggered=self.convert_notebook, icon='python.png')
2102
2258
self.gotodef_action = create_action(self, _("Go to definition"),
2103
2259
triggered=self.go_to_definition_from_cursor)
2104
run_selection_action = create_action(self,
2260
self.run_selection_action = create_action(self,
2105
2261
_("Run &selection or current line"),
2106
2262
icon='run_selection.png',
2107
2263
triggered=lambda: self.emit(SIGNAL('run_selection()')))
2112
2268
QKeySequence(QKeySequence.ZoomOut), icon='zoom_out.png',
2113
2269
triggered=lambda: self.emit(SIGNAL('zoom_out()')))
2114
2270
self.menu = QMenu(self)
2115
add_actions(self.menu, (self.undo_action, self.redo_action, None,
2116
self.cut_action, self.copy_action,
2117
paste_action, self.delete_action,
2118
None, selectall_action, None, zoom_in_action,
2119
zoom_out_action, None, toggle_comment_action,
2120
None, run_selection_action,
2121
self.gotodef_action))
2271
if nbformat is not None:
2272
add_actions(self.menu, (self.undo_action, self.redo_action, None,
2273
self.cut_action, self.copy_action,
2274
paste_action, self.delete_action,
2275
None, self.clear_all_output_action,
2276
self.ipynb_convert_action, None,
2277
selectall_action, None, zoom_in_action,
2278
zoom_out_action, None, toggle_comment_action,
2279
None, self.run_selection_action,
2280
self.gotodef_action))
2282
add_actions(self.menu, (self.undo_action, self.redo_action, None,
2283
self.cut_action, self.copy_action,
2284
paste_action, self.delete_action,
2285
None, selectall_action, None, zoom_in_action,
2286
zoom_out_action, None, toggle_comment_action,
2287
None, self.run_selection_action,
2288
self.gotodef_action))
2123
2291
# Read-only context-menu
2124
2292
self.readonly_menu = QMenu(self)
2125
2293
add_actions(self.readonly_menu,
2126
2294
(self.copy_action, None, selectall_action,
2127
2295
self.gotodef_action))
2129
2297
def keyPressEvent(self, event):
2130
2298
"""Reimplement Qt method"""
2131
2299
key = event.key()
2349
2517
def contextMenuEvent(self, event):
2350
2518
"""Reimplement Qt method"""
2351
state = self.has_selected_text()
2352
self.copy_action.setEnabled(state)
2353
self.cut_action.setEnabled(state)
2354
self.delete_action.setEnabled(state)
2355
self.undo_action.setEnabled( self.document().isUndoAvailable() )
2356
self.redo_action.setEnabled( self.document().isRedoAvailable() )
2519
nonempty_selection = self.has_selected_text()
2520
self.copy_action.setEnabled(nonempty_selection)
2521
self.cut_action.setEnabled(nonempty_selection)
2522
self.delete_action.setEnabled(nonempty_selection)
2523
self.clear_all_output_action.setVisible(self.is_json())
2524
self.ipynb_convert_action.setVisible(self.is_json())
2525
self.run_selection_action.setEnabled(nonempty_selection)
2526
self.run_selection_action.setVisible(self.is_python())
2527
self.gotodef_action.setVisible(self.go_to_definition_enabled\
2528
and self.is_python_like())
2530
# Code duplication go_to_definition_from_cursor and mouse_move_event
2531
cursor = self.textCursor()
2532
text = to_text_string(cursor.selectedText())
2534
cursor.select(QTextCursor.WordUnderCursor)
2535
text = to_text_string(cursor.selectedText())
2536
self.gotodef_action.setEnabled(sourcecode.is_keyword(text))
2538
self.undo_action.setEnabled( self.document().isUndoAvailable())
2539
self.redo_action.setEnabled( self.document().isRedoAvailable())
2357
2540
menu = self.menu
2358
2541
if self.isReadOnly():
2359
2542
menu = self.readonly_menu
2380
2563
TextEditBaseWidget.dropEvent(self, event)
2566
def paintEvent(self, event):
2567
"""Overrides paint event to update the list of visible blocks"""
2568
self.update_visible_blocks(event)
2569
TextEditBaseWidget.paintEvent(self, event)
2570
self.painted.emit(event)
2572
def update_visible_blocks(self, event):
2573
"""Update the list of visible blocks/lines position"""
2574
self.__visible_blocks[:] = []
2575
block = self.firstVisibleBlock()
2576
blockNumber = block.blockNumber()
2577
top = int(self.blockBoundingGeometry(block).translated(
2578
self.contentOffset()).top())
2579
bottom = top + int(self.blockBoundingRect(block).height())
2581
ebottom_bottom = self.height()
2583
while block.isValid():
2584
visible = (top >= ebottom_top and bottom <= ebottom_bottom)
2587
if block.isVisible():
2588
self.__visible_blocks.append((top, blockNumber+1, block))
2589
block = block.next()
2591
bottom = top + int(self.blockBoundingRect(block).height())
2592
blockNumber = block.blockNumber()
2594
def _draw_editor_cell_divider(self):
2595
"""Draw a line on top of a define cell"""
2596
if self.supported_cell_language:
2597
cell_line_color = self.comment_color
2598
painter = QPainter(self.viewport())
2600
pen.setStyle(Qt.SolidLine)
2601
pen.setBrush(cell_line_color)
2604
for top, line_number, block in self.visible_blocks:
2605
if self.is_cell_separator(block):
2606
painter.drawLine(4, top, self.width(), top)
2609
def visible_blocks(self):
2611
Returns the list of visible blocks.
2613
Each element in the list is a tuple made up of the line top position,
2614
the line number (already 1 based), and the QTextBlock itself.
2616
:return: A list of tuple(top position, line number, block)
2617
:rtype: List of tuple(int, int, QtGui.QTextBlock)
2619
return self.__visible_blocks
2383
2621
#===============================================================================
2384
2622
# CodeEditor's Printer