def __init__(self, parent, **kwds): super().__init__(parent, showLineNumbers=True, **kwds) # Init filename and name self._filename = '' self._name = '<TMP>' # View settings self.setShowWhitespace(iep.config.view.showWhitespace) #TODO: self.setViewWrapSymbols(view.showWrapSymbols) self.setShowLineEndings(iep.config.view.showLineEndings) self.setShowIndentationGuides(iep.config.view.showIndentationGuides) # self.setWrap(bool(iep.config.view.wrap)) self.setHighlightCurrentLine(iep.config.view.highlightCurrentLine) self.setLongLineIndicatorPosition(iep.config.view.edgeColumn) #TODO: self.setFolding( int(view.codeFolding)*5 ) # bracematch is set in baseTextCtrl, since it also applies to shells # dito for zoom and tabWidth # Set line endings to default self.lineEndings = iep.config.settings.defaultLineEndings # Set encoding to default self.encoding = 'UTF-8' # Modification time to test file change self._modifyTime = 0 self.modificationChanged.connect(self._onModificationChanged) # To see whether the doc has changed to update the parser. self.textChanged.connect(self._onModified) # This timer is used to hide the marker that shows which code is executed self._showRunCursorTimer = QtCore.QTimer() # Add context menu (the offset is to prevent accidental auto-clicking) self._menu = EditorContextMenu(self) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.customContextMenuRequested.connect(lambda p: self._menu.popup( self.mapToGlobal(p) + QtCore.QPoint(0, 3)))
def __init__(self, parent, **kwds): super().__init__(parent, showLineNumbers = True, **kwds) # Init filename and name self._filename = '' self._name = '<TMP>' # View settings self.setShowWhitespace(iep.config.view.showWhitespace) #TODO: self.setViewWrapSymbols(view.showWrapSymbols) self.setShowLineEndings(iep.config.view.showLineEndings) self.setShowIndentationGuides(iep.config.view.showIndentationGuides) # self.setWrap(bool(iep.config.view.wrap)) self.setHighlightCurrentLine(iep.config.view.highlightCurrentLine) self.setLongLineIndicatorPosition(iep.config.view.edgeColumn) #TODO: self.setFolding( int(view.codeFolding)*5 ) # bracematch is set in baseTextCtrl, since it also applies to shells # dito for zoom and tabWidth # Set line endings to default self.lineEndings = iep.config.settings.defaultLineEndings # Set encoding to default self.encoding = 'UTF-8' # Modification time to test file change self._modifyTime = 0 self.modificationChanged.connect(self._onModificationChanged) # To see whether the doc has changed to update the parser. self.textChanged.connect(self._onModified) # This timer is used to hide the marker that shows which code is executed self._showRunCursorTimer = QtCore.QTimer() # Add context menu (the offset is to prevent accidental auto-clicking) self._menu = EditorContextMenu(self) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.customContextMenuRequested.connect(lambda p: self._menu.popup(self.mapToGlobal(p)+QtCore.QPoint(0,3)))
class IepEditor(BaseTextCtrl): # called when dirty changed or filename changed, etc somethingChanged = QtCore.Signal() def __init__(self, parent, **kwds): super().__init__(parent, showLineNumbers = True, **kwds) # Init filename and name self._filename = '' self._name = '<TMP>' # View settings self.setShowWhitespace(iep.config.view.showWhitespace) #TODO: self.setViewWrapSymbols(view.showWrapSymbols) self.setShowLineEndings(iep.config.view.showLineEndings) self.setShowIndentationGuides(iep.config.view.showIndentationGuides) # self.setWrap(bool(iep.config.view.wrap)) self.setHighlightCurrentLine(iep.config.view.highlightCurrentLine) self.setLongLineIndicatorPosition(iep.config.view.edgeColumn) #TODO: self.setFolding( int(view.codeFolding)*5 ) # bracematch is set in baseTextCtrl, since it also applies to shells # dito for zoom and tabWidth # Set line endings to default self.lineEndings = iep.config.settings.defaultLineEndings # Set encoding to default self.encoding = 'UTF-8' # Modification time to test file change self._modifyTime = 0 self.modificationChanged.connect(self._onModificationChanged) # To see whether the doc has changed to update the parser. self.textChanged.connect(self._onModified) # This timer is used to hide the marker that shows which code is executed self._showRunCursorTimer = QtCore.QTimer() # Add context menu (the offset is to prevent accidental auto-clicking) self._menu = EditorContextMenu(self) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.customContextMenuRequested.connect(lambda p: self._menu.popup(self.mapToGlobal(p)+QtCore.QPoint(0,3))) ## Properties @property def name(self): return self._name @property def filename(self): return self._filename @property def lineEndings(self): """ Line-endings style of this file. Setter accepts machine-readable (e.g. '\r') and human-readable (e.g. 'CR') input """ return self._lineEndings @lineEndings.setter def lineEndings(self,value): if value in ('\r','\n','\r\n'): self._lineEndings = value return try: self._lineEndings = {'CR': '\r', 'LF': '\n', 'CRLF': '\r\n'}[value] except KeyError: raise ValueError('Invalid line endings style %r' % value) @property def lineEndingsHumanReadable(self): """ Current line-endings style, human readable (e.g. 'CR') """ return {'\r': 'CR', '\n': 'LF', '\r\n': 'CRLF'}[self.lineEndings] @property def encoding(self): """ Encoding used to convert the text of this file to bytes. """ return self._encoding @encoding.setter def encoding(self, value): # Test given value, correct name if it exists try: c = codecs.lookup(value) value = c.name except Exception: value = codecs.lookup('UTF-8').name # Store self._encoding = value ## def justifyText(self): """ Overloaded version of justifyText to make it use our configurable justificationwidth. """ super().justifyText(iep.config.settings.justificationWidth) def showRunCursor(self, cursor): """ Momentarily highlight a piece of code to show that this is being executed """ extraSelection = QtGui.QTextEdit.ExtraSelection() extraSelection.cursor = cursor extraSelection.format.setBackground(QtCore.Qt.gray) self.setExtraSelections([extraSelection]) self._showRunCursorTimer.singleShot(200, lambda: self.setExtraSelections([])) def id(self): """ Get an id of this editor. This is the filename, or for tmp files, the name. """ if self._filename: return self._filename else: return self._name def focusInEvent(self, event): """ Test whether the file has been changed 'behind our back' """ # Act normally to the focus event BaseTextCtrl.focusInEvent(self, event) # Test file change self.testWhetherFileWasChanged() def testWhetherFileWasChanged(self): """ testWhetherFileWasChanged() Test to see whether the file was changed outside our backs, and let the user decide what to do. Returns True if it was changed. """ # get the path path = self._filename if not os.path.isfile(path): # file is deleted from the outside return # test the modification time... mtime = os.path.getmtime(path) if mtime != self._modifyTime: # ask user dlg = QtGui.QMessageBox(self) dlg.setWindowTitle('File was changed') dlg.setText("File has been modified outside of the editor:\n"+ self._filename) dlg.setInformativeText("Do you want to reload?") t=dlg.addButton("Reload", QtGui.QMessageBox.AcceptRole) #0 dlg.addButton("Keep this version", QtGui.QMessageBox.RejectRole) #1 dlg.setDefaultButton(t) # whatever the result, we will reset the modified time self._modifyTime = os.path.getmtime(path) # get result and act result = dlg.exec_() if result == QtGui.QMessageBox.AcceptRole: self.reload() else: pass # when cancelled or explicitly said, do nothing # Return that indeed the file was changes return True def _onModificationChanged(self,changed): """Handler for the modificationChanged signal. Emit somethingChanged for the editorStack to update the modification notice.""" self.somethingChanged.emit() def _onModified(self): iep.parser.parseThis(self) def dragMoveEvent(self, event): """ Otherwise cursor can get stuck. https://bitbucket.org/iep-project/iep/issue/252 https://qt-project.org/forums/viewthread/3180 """ if event.mimeData().hasUrls(): event.acceptProposedAction() else: BaseTextCtrl.dropEvent(self, event) def dropEvent(self, event): """ Drop files in the list. """ if event.mimeData().hasUrls(): # file: let the editorstack do the work. iep.editors.dropEvent(event) else: # text: act normal BaseTextCtrl.dropEvent(self, event) def showEvent(self, event=None): """ Capture show event to change title. """ # Act normally if event: BaseTextCtrl.showEvent(self, event) # Make parser update iep.parser.parseThis(self) def setTitleInMainWindow(self): """ set the title text in the main window to show filename. """ # compose title name, path = self._name, self._filename if path: iep.main.setMainTitle(path) else: iep.main.setMainTitle(name) def save(self, filename=None): """ Save the file. No checking is done. """ # get filename if filename is None: filename = self._filename if not filename: raise ValueError("No filename specified, and no filename known.") # Test whether it was changed without us knowing. If so, dont save now. if self.testWhetherFileWasChanged(): return # Get text and remember where we are text = self.toPlainText() cursor = self.textCursor() linenr = cursor.blockNumber() + 1 index = cursor.positionInBlock() scroll = self.verticalScrollBar().value() # Convert line endings (optionally remove trailing whitespace if iep.config.settings.removeTrailingWhitespaceWhenSaving: lines = [line.rstrip() for line in text.split('\n')] if lines[-1]: lines.append('') # Ensure the file ends in an empty line text = self.lineEndings.join(lines) self.setPlainText(text) # Go back to where we were cursor = self.textCursor() cursor.movePosition(cursor.Start) # move to begin of the document cursor.movePosition(cursor.NextBlock,n=linenr-1) # n blocks down index = min(index, cursor.block().length()-1) cursor.movePosition(cursor.Right,n=index) # n chars right self.setTextCursor(cursor) self.verticalScrollBar().setValue(scroll) else: text = text.replace('\n', self.lineEndings) # Make bytes bb = text.encode(self.encoding) # Store f = open(filename, 'wb') try: f.write(bb) finally: f.close() # Update stats self._filename = normalizePath( filename ) self._name = os.path.split(self._filename)[1] self.document().setModified(False) self._modifyTime = os.path.getmtime(self._filename) # update title (in case of a rename) self.setTitleInMainWindow() # allow item to update its texts (no need: onModifiedChanged does this) #self.somethingChanged.emit() def reload(self): """ Reload text using the self._filename. We do not have a load method; we first try to load the file and only when we succeed create an editor to show it in... This method is only for reloading in case the file was changed outside of the editor. """ # We can only load if the filename is known if not self._filename: return filename = self._filename # Remember where we are cursor = self.textCursor() linenr = cursor.blockNumber() + 1 index = cursor.positionInBlock() # Load file (as bytes) with open(filename, 'rb') as f: bb = f.read() # Convert to text text = bb.decode('UTF-8') # Process line endings (before setting the text) self.lineEndings= determineLineEnding(text) # Set text self.setPlainText(text) self.document().setModified(False) # Go where we were (approximately) self.gotoLine(linenr) def deleteLines(self): cursor = self.textCursor() # Find start and end of selection start = cursor.selectionStart() end = cursor.selectionEnd() # Expand selection: from start of first block to start of next block cursor.setPosition(start) cursor.movePosition(cursor.StartOfBlock) cursor.setPosition(end, cursor.KeepAnchor) cursor.movePosition(cursor.NextBlock, cursor.KeepAnchor) cursor.removeSelectedText() def commentCode(self): """ Comment the lines that are currently selected """ indents = [] def getIndent(cursor): text = cursor.block().text().rstrip() if text: indents.append(len(text) - len(text.lstrip())) def commentBlock(cursor): cursor.setPosition(cursor.block().position() + minindent) cursor.insertText('# ') self.doForSelectedBlocks(getIndent) minindent = min(indents) if indents else 0 self.doForSelectedBlocks(commentBlock) def uncommentCode(self): """ Uncomment the lines that are currently selected """ #TODO: this should not be applied to lines that are part of a multi-line string #Define the uncomment function to be applied to all blocks def uncommentBlock(cursor): """ Find the first # on the line; if there is just whitespace before it, remove the # and if it is followed by a space remove the space, too """ text = cursor.block().text() commentStart = text.find('#') if commentStart == -1: return #No comment on this line if text[:commentStart].strip() != '': return #Text before the # #Move the cursor to the beginning of the comment cursor.setPosition(cursor.block().position() + commentStart) cursor.deleteChar() if text[commentStart:].startswith('# '): cursor.deleteChar() #Apply this function to all blocks self.doForSelectedBlocks(uncommentBlock) # def gotoDef(self): """ Goto the definition for the word under the cursor """ # Get name of object to go to cursor = self.textCursor() if not cursor.hasSelection(): cursor.select(cursor.WordUnderCursor) word = cursor.selection().toPlainText() # Send the open command to the shell s = iep.shells.getCurrentShell() if s is not None: if word and word.isidentifier(): s.executeCommand('open %s\n'%word) else: s.write('Invalid identifier %r\n' % word) ## Introspection processing methods def processCallTip(self, cto): """ Processes a calltip request using a CallTipObject instance. """ # Try using buffer first if cto.tryUsingBuffer(): return # Try obtaining calltip from the source sig = iep.parser.getFictiveSignature(cto.name, self, True) if sig: # Done cto.finish(sig) else: # Try the shell shell = iep.shells.getCurrentShell() if shell: shell.processCallTip(cto) def processAutoComp(self, aco): """ Processes an autocomp request using an AutoCompObject instance. """ # Try using buffer first if aco.tryUsingBuffer(): return # Init name to poll by remote process (can be changed!) nameForShell = aco.name # Get normal fictive namespace fictiveNS = iep.parser.getFictiveNameSpace(self) fictiveNS = set(fictiveNS) # Add names if not aco.name: # "root" names aco.addNames(fictiveNS) # imports importNames, importLines = iep.parser.getFictiveImports(self) aco.addNames(importNames) else: # Prepare list of class names to check out classNames = [aco.name] handleSelf = True # Unroll supers while classNames: className = classNames.pop(0) if not className: continue if handleSelf or (className in fictiveNS): # Only the self list (only first iter) fictiveClass = iep.parser.getFictiveClass( className, self, handleSelf) handleSelf = False if fictiveClass: aco.addNames( fictiveClass.members ) classNames.extend(fictiveClass.supers) else: nameForShell = className break # If there's a shell, let it finish the autocompletion shell = iep.shells.getCurrentShell() if shell: aco.name = nameForShell # might be the same or a base class shell.processAutoComp(aco) else: # Otherwise we finish it ourselves aco.finish()
class IepEditor(BaseTextCtrl): # called when dirty changed or filename changed, etc somethingChanged = QtCore.Signal() def __init__(self, parent, **kwds): super().__init__(parent, showLineNumbers=True, **kwds) # Init filename and name self._filename = '' self._name = '<TMP>' # View settings self.setShowWhitespace(iep.config.view.showWhitespace) #TODO: self.setViewWrapSymbols(view.showWrapSymbols) self.setShowLineEndings(iep.config.view.showLineEndings) self.setShowIndentationGuides(iep.config.view.showIndentationGuides) # self.setWrap(bool(iep.config.view.wrap)) self.setHighlightCurrentLine(iep.config.view.highlightCurrentLine) self.setLongLineIndicatorPosition(iep.config.view.edgeColumn) #TODO: self.setFolding( int(view.codeFolding)*5 ) # bracematch is set in baseTextCtrl, since it also applies to shells # dito for zoom and tabWidth # Set line endings to default self.lineEndings = iep.config.settings.defaultLineEndings # Set encoding to default self.encoding = 'UTF-8' # Modification time to test file change self._modifyTime = 0 self.modificationChanged.connect(self._onModificationChanged) # To see whether the doc has changed to update the parser. self.textChanged.connect(self._onModified) # This timer is used to hide the marker that shows which code is executed self._showRunCursorTimer = QtCore.QTimer() # Add context menu (the offset is to prevent accidental auto-clicking) self._menu = EditorContextMenu(self) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.customContextMenuRequested.connect(lambda p: self._menu.popup( self.mapToGlobal(p) + QtCore.QPoint(0, 3))) ## Properties @property def name(self): return self._name @property def filename(self): return self._filename @property def lineEndings(self): """ Line-endings style of this file. Setter accepts machine-readable (e.g. '\r') and human-readable (e.g. 'CR') input """ return self._lineEndings @lineEndings.setter def lineEndings(self, value): if value in ('\r', '\n', '\r\n'): self._lineEndings = value return try: self._lineEndings = {'CR': '\r', 'LF': '\n', 'CRLF': '\r\n'}[value] except KeyError: raise ValueError('Invalid line endings style %r' % value) @property def lineEndingsHumanReadable(self): """ Current line-endings style, human readable (e.g. 'CR') """ return {'\r': 'CR', '\n': 'LF', '\r\n': 'CRLF'}[self.lineEndings] @property def encoding(self): """ Encoding used to convert the text of this file to bytes. """ return self._encoding @encoding.setter def encoding(self, value): # Test given value, correct name if it exists try: c = codecs.lookup(value) value = c.name except Exception: value = codecs.lookup('UTF-8').name # Store self._encoding = value ## def showRunCursor(self, cursor): """ Momentarily highlight a piece of code to show that this is being executed """ extraSelection = QtGui.QTextEdit.ExtraSelection() extraSelection.cursor = cursor extraSelection.format.setBackground(QtCore.Qt.gray) self.setExtraSelections([extraSelection]) self._showRunCursorTimer.singleShot( 200, lambda: self.setExtraSelections([])) def id(self): """ Get an id of this editor. This is the filename, or for tmp files, the name. """ if self._filename: return self._filename else: return self._name def focusInEvent(self, event): """ Test whether the file has been changed 'behind our back' """ # Act normally to the focus event BaseTextCtrl.focusInEvent(self, event) # Test file change self.testWhetherFileWasChanged() def testWhetherFileWasChanged(self): """ testWhetherFileWasChanged() Test to see whether the file was changed outside our backs, and let the user decide what to do. Returns True if it was changed. """ # get the path path = self._filename if not os.path.isfile(path): # file is deleted from the outside return # test the modification time... mtime = os.path.getmtime(path) if mtime != self._modifyTime: # ask user dlg = QtGui.QMessageBox(self) dlg.setWindowTitle('File was changed') dlg.setText("File has been modified outside of the editor:\n" + self._filename) dlg.setInformativeText("Do you want to reload?") t = dlg.addButton("Reload", QtGui.QMessageBox.AcceptRole) #0 dlg.addButton("Keep this version", QtGui.QMessageBox.RejectRole) #1 dlg.setDefaultButton(t) # whatever the result, we will reset the modified time self._modifyTime = os.path.getmtime(path) # get result and act result = dlg.exec_() if result == QtGui.QMessageBox.AcceptRole: self.reload() else: pass # when cancelled or explicitly said, do nothing # Return that indeed the file was changes return True def _onModificationChanged(self, changed): """Handler for the modificationChanged signal. Emit somethingChanged for the editorStack to update the modification notice.""" self.somethingChanged.emit() def _onModified(self): iep.parser.parseThis(self) def dropEvent(self, event): """ Drop files in the list. """ if event.mimeData().hasUrls(): # file: let the editorstack do the work. iep.editors.dropEvent(event) else: # text: act normal BaseTextCtrl.dropEvent(self, event) def showEvent(self, event=None): """ Capture show event to change title. """ # Act normally if event: BaseTextCtrl.showEvent(self, event) # Make parser update iep.parser.parseThis(self) def setTitleInMainWindow(self): """ set the title text in the main window to show filename. """ # compose title name, path = self._name, self._filename if not path: path = 'no location on disk' tmp = { 'fileName': name, 'filename': name, 'name': name, 'fullPath': path, 'fullpath': path, 'path': path } title = iep.config.advanced.titleText.format(**tmp) # set title iep.main.setWindowTitle(title) def save(self, filename=None): """ Save the file. No checking is done. """ # get filename if filename is None: filename = self._filename if not filename: raise ValueError("No filename specified, and no filename known.") # Test whether it was changed without us knowing. If so, dont save now. if self.testWhetherFileWasChanged(): return # Get text, convert line endings text = self.toPlainText() text = text.replace('\n', self.lineEndings) # Make bytes bb = text.encode(self.encoding) # Store f = open(filename, 'wb') try: f.write(bb) finally: f.close() # Update stats self._filename = normalizePath(filename) self._name = os.path.split(self._filename)[1] self.document().setModified(False) self._modifyTime = os.path.getmtime(self._filename) # update title (in case of a rename) self.setTitleInMainWindow() # allow item to update its texts (no need: onModifiedChanged does this) #self.somethingChanged.emit() def reload(self): """ Reload text using the self._filename. We do not have a load method; we first try to load the file and only when we succeed create an editor to show it in... This method is only for reloading in case the file was changed outside of the editor. """ # We can only load if the filename is known if not self._filename: return filename = self._filename # Remember where we are cursor = self.textCursor() linenr = cursor.blockNumber() + 1 index = cursor.positionInBlock() # Load file (as bytes) with open(filename, 'rb') as f: bb = f.read() # Convert to text text = bb.decode('UTF-8') # Process line endings (before setting the text) self.lineEndings = determineLineEnding(text) # Set text self.setPlainText(text) self.document().setModified(False) # Go where we were (approximately) self.gotoLine(linenr) def deleteLines(self): cursor = self.textCursor() # Find start and end of selection start = cursor.selectionStart() end = cursor.selectionEnd() # Expand selection: from start of first block to start of next block cursor.setPosition(start) cursor.movePosition(cursor.StartOfBlock) cursor.setPosition(end, cursor.KeepAnchor) cursor.movePosition(cursor.NextBlock, cursor.KeepAnchor) cursor.removeSelectedText() def commentCode(self): """ Comment the lines that are currently selected """ self.doForSelectedBlocks(lambda cursor: cursor.insertText('# ')) def uncommentCode(self): """ Uncomment the lines that are currently selected """ #TODO: this should not be applied to lines that are part of a multi-line string #Define the uncomment function to be applied to all blocks def uncommentBlock(cursor): """ Find the first # on the line; if there is just whitespace before it, remove the # and if it is followed by a space remove the space, too """ text = cursor.block().text() commentStart = text.find('#') if commentStart == -1: return #No comment on this line if text[:commentStart].strip() != '': return #Text before the # #Move the cursor to the beginning of the comment cursor.setPosition(cursor.block().position() + commentStart) cursor.deleteChar() if text[commentStart:].startswith('# '): cursor.deleteChar() #Apply this function to all blocks self.doForSelectedBlocks(uncommentBlock) ## Introspection processing methods def processCallTip(self, cto): """ Processes a calltip request using a CallTipObject instance. """ # Try using buffer first if cto.tryUsingBuffer(): return # Try obtaining calltip from the source sig = iep.parser.getFictiveSignature(cto.name, self, True) if sig: # Done cto.finish(sig) else: # Try the shell shell = iep.shells.getCurrentShell() if shell: shell.processCallTip(cto) def processAutoComp(self, aco): """ Processes an autocomp request using an AutoCompObject instance. """ # Try using buffer first if aco.tryUsingBuffer(): return # Init name to poll by remote process (can be changed!) nameForShell = aco.name # Get normal fictive namespace fictiveNS = iep.parser.getFictiveNameSpace(self) fictiveNS = set(fictiveNS) # Add names if not aco.name: # "root" names aco.addNames(fictiveNS) # imports importNames, importLines = iep.parser.getFictiveImports(self) aco.addNames(importNames) else: # Prepare list of class names to check out classNames = [aco.name] handleSelf = True # Unroll supers while classNames: className = classNames.pop(0) if not className: continue if handleSelf or (className in fictiveNS): # Only the self list (only first iter) fictiveClass = iep.parser.getFictiveClass( className, self, handleSelf) handleSelf = False if fictiveClass: aco.addNames(fictiveClass.members) classNames.extend(fictiveClass.supers) else: nameForShell = className break # If there's a shell, let it finish the autocompletion shell = iep.shells.getCurrentShell() if shell: aco.name = nameForShell # might be the same or a base class shell.processAutoComp(aco) else: # Otherwise we finish it ourselves aco.finish()