QScintilla/Editor.py

changeset 158
6a561f87bc07
parent 156
478787b5607e
child 162
28f235c426c4
equal deleted inserted replaced
157:c8d5916b9934 158:6a561f87bc07
7 Module implementing the editor component of the eric5 IDE. 7 Module implementing the editor component of the eric5 IDE.
8 """ 8 """
9 9
10 import os 10 import os
11 import re 11 import re
12 import difflib
12 13
13 from PyQt4.Qsci import QsciScintilla, QsciMacro 14 from PyQt4.Qsci import QsciScintilla, QsciMacro
14 from PyQt4.QtCore import * 15 from PyQt4.QtCore import *
15 from PyQt4.QtGui import * 16 from PyQt4.QtGui import *
16 17
94 TemplateImageID = 100 95 TemplateImageID = 100
95 96
96 # Cooperation related definitions 97 # Cooperation related definitions
97 Separator = "@@@" 98 Separator = "@@@"
98 99
99 SelectionToken = "SELECT" 100 StartEditToken = "START_EDIT"
101 EndEditToken = "END_EDIT"
102 CancelEditToken = "CANCEL_EDIT"
103 RequestSyncToken = "REQUEST_SYNC"
104 SyncToken = "SYNC"
100 105
101 def __init__(self, dbs, fn = None, vm = None, 106 def __init__(self, dbs, fn = None, vm = None,
102 filetype = "", editor = None, tv = None): 107 filetype = "", editor = None, tv = None):
103 """ 108 """
104 Constructor 109 Constructor
187 self.spell = None 192 self.spell = None
188 self.lastLine = 0 193 self.lastLine = 0
189 self.lastIndex = 0 194 self.lastIndex = 0
190 195
191 # initialize some cooperation stuff 196 # initialize some cooperation stuff
192 self.__lastSelection = (-1, -1, -1, -1) 197 self.__isSyncing = False
198 self.__receivedWhileSyncing = []
199 self.__savedText = ""
200 self.__inSharedEdit = False
201 self.__isShared = False
202 self.__inRemoteSharedEdit = False
193 203
194 self.connect(self, SIGNAL('modificationChanged(bool)'), 204 self.connect(self, SIGNAL('modificationChanged(bool)'),
195 self.__modificationChanged) 205 self.__modificationChanged)
196 self.connect(self, SIGNAL('cursorPositionChanged(int,int)'), 206 self.connect(self, SIGNAL('cursorPositionChanged(int,int)'),
197 self.__cursorPositionChanged) 207 self.__cursorPositionChanged)
198 self.connect(self, SIGNAL('modificationAttempted()'), 208 self.connect(self, SIGNAL('modificationAttempted()'),
199 self.__modificationReadOnly) 209 self.__modificationReadOnly)
200 self.connect(self, SIGNAL('userListActivated(int, const QString)'), 210 self.connect(self, SIGNAL('userListActivated(int, const QString)'),
201 self.__completionListSelected) 211 self.__completionListSelected)
202 self.connect(self, SIGNAL('selectionChanged()'),
203 self.__selectionChanged)
204 212
205 # margins layout 213 # margins layout
206 if QSCINTILLA_VERSION() >= 0x020301: 214 if QSCINTILLA_VERSION() >= 0x020301:
207 self.__unifiedMargins = Preferences.getEditor("UnifiedMargins") 215 self.__unifiedMargins = Preferences.getEditor("UnifiedMargins")
208 else: 216 else:
4871 @param bForce True to force change, False to only update and emit 4879 @param bForce True to force change, False to only update and emit
4872 signal if there was an attribute change. 4880 signal if there was an attribute change.
4873 """ 4881 """
4874 if self.fileName is None: 4882 if self.fileName is None:
4875 return 4883 return
4876 readOnly = not QFileInfo(self.fileName).isWritable() 4884 readOnly = not QFileInfo(self.fileName).isWritable() or self.isReadOnly()
4877 if not bForce and (readOnly == self.isReadOnly()): 4885 if not bForce and (readOnly == self.isReadOnly()):
4878 return 4886 return
4879 cap = self.fileName 4887 cap = self.fileName
4880 if readOnly: 4888 if readOnly:
4881 cap = self.trUtf8("{0} (ro)".format(cap)) 4889 cap = self.trUtf8("{0} (ro)".format(cap))
5477 5485
5478 ####################################################################### 5486 #######################################################################
5479 ## Cooperation related methods 5487 ## Cooperation related methods
5480 ####################################################################### 5488 #######################################################################
5481 5489
5482 def send(self, token, args): 5490 def getSharingStatus(self):
5483 """ 5491 """
5484 Public method to send an editor command to remote editors. 5492 Public method to get some share status info.
5493
5494 @return tuple indicating, if the editor is sharable, the sharing status,
5495 if it is inside a locally initiated shared edit session and
5496 if it is inside a remotely initiated shared edit session
5497 (boolean, boolean, boolean, boolean)
5498 """
5499 project = e5App().getObject("Project")
5500 return project.isOpen() and project.isProjectFile(self.fileName), \
5501 self.__isShared, self.__inSharedEdit, self.__inRemoteSharedEdit
5502
5503 def shareConnected(self, connected):
5504 """
5505 Public slot to handle a change of the connected state.
5506
5507 @param connected flag indicating the connected state (boolean)
5508 """
5509 if not connected:
5510 self.__inRemoteSharedEdit = False
5511 self.setReadOnly(False)
5512 self.__updateReadOnly()
5513 self.cancelSharedEdit(send = False)
5514 self.__isSyncing = False
5515 self.__receivedWhileSyncing = []
5516
5517 def shareEditor(self, share):
5518 """
5519 Public slot to set the shared status of the editor.
5520
5521 @param share flag indicating the share status (boolean)
5522 """
5523 self.__isShared = share
5524 if not share:
5525 self.shareConnected(False)
5526
5527 def startSharedEdit(self):
5528 """
5529 Public slot to start a shared edit session for the editor.
5530 """
5531 self.__inSharedEdit = True
5532 self.__savedText = self.text()
5533 hash = str(
5534 QCryptographicHash.hash(
5535 Utilities.encode(self.__savedText, self.encoding)[0],
5536 QCryptographicHash.Sha1).toHex(),
5537 encoding = "utf-8")
5538 self.__send(Editor.StartEditToken, hash)
5539
5540 def sendSharedEdit(self):
5541 """
5542 Public slot to end a shared edit session for the editor and
5543 send the changes.
5544 """
5545 commands = self.__calculateChanges(self.__savedText, self.text())
5546 self.__send(Editor.EndEditToken, commands)
5547 self.__inSharedEdit = False
5548 self.__savedText = ""
5549
5550 def cancelSharedEdit(self, send = True):
5551 """
5552 Public slot to cancel a shared edit session for the editor.
5553
5554 @keyparam send flag indicating to send the CancelEdit command (boolean)
5555 """
5556 self.__inSharedEdit = False
5557 self.__savedText = ""
5558 if send:
5559 self.__send(Editor.CancelEditToken)
5560
5561 def __send(self, token, args = None):
5562 """
5563 Private method to send an editor command to remote editors.
5485 5564
5486 @param token command token (string) 5565 @param token command token (string)
5487 @param args arguments for the command (string) 5566 @param args arguments for the command (string)
5488 """ 5567 """
5489 msg = "" 5568 if self.vm.isConnected():
5490 if token == Editor.SelectionToken: 5569 msg = ""
5491 msg = "{0}{1}{2} {3} {4} {5}".format( 5570 if token in (Editor.StartEditToken,
5492 token, 5571 Editor.EndEditToken,
5493 Editor.Separator, 5572 Editor.RequestSyncToken,
5494 *args 5573 Editor.SyncToken):
5495 ) 5574 msg = "{0}{1}{2}".format(
5496 5575 token,
5497 self.vm.send(self.fileName, msg) 5576 Editor.Separator,
5577 args
5578 )
5579 elif token == Editor.CancelEditToken:
5580 msg = "{0}{1}c".format(
5581 token,
5582 Editor.Separator
5583 )
5584
5585 self.vm.send(self.fileName, msg)
5498 5586
5499 def receive(self, command): 5587 def receive(self, command):
5500 """ 5588 """
5501 Public slot to handle received editor commands. 5589 Public slot to handle received editor commands.
5502 5590
5503 @param command command string (string) 5591 @param command command string (string)
5504 """ 5592 """
5505 token, argsString = command.split(Editor.Separator) 5593 if self.__isShared:
5506 if token == Editor.SelectionToken: 5594 if self.__isSyncing and \
5507 self.__processSelectionCommand(argsString) 5595 not command.startswith(Editor.SyncToken + Editor.Separator):
5508 5596 self.__receivedWhileSyncing.append(command)
5509 def __selectionChanged(self): 5597 else:
5510 """ 5598 self.__dispatchCommand(command)
5511 Private slot to handle a change of the selection. 5599
5512 """ 5600 def __dispatchCommand(self, command):
5513 if self.vm.isConnected(): 5601 """
5514 sel = self.getSelection() 5602 Private method to dispatch received commands.
5515 if sel != self.__lastSelection: 5603
5516 self.send(Editor.SelectionToken, args = sel) 5604 @param command command to be processed (string)
5517 self.__lastSelection = sel 5605 """
5518 5606 token, argsString = command.split(Editor.Separator, 1)
5519 def __processSelectionCommand(self, argsString): 5607 if token == Editor.StartEditToken:
5520 """ 5608 self.__processStartEditCommand(argsString)
5521 Private slot to process a remote selection command 5609 elif token == Editor.CancelEditToken:
5522 5610 self.shareConnected(False)
5523 @param argsString string containing the selection parameters (string) 5611 elif token == Editor.EndEditToken:
5524 """ 5612 self.__processEndEditCommand(argsString)
5525 self.selectionChanged.disconnect(self.__selectionChanged) 5613 elif token == Editor.RequestSyncToken:
5526 args = argsString.split() 5614 self.__processRequestSyncCommand(argsString)
5527 self.setSelection(int(args[0]), int(args[1]), int(args[2]), int(args[3])) 5615 elif token == Editor.SyncToken:
5528 self.ensureLineVisible(int(args[0])) 5616 self.__processSyncCommand(argsString)
5529 self.selectionChanged.connect(self.__selectionChanged) 5617
5618 def __processStartEditCommand(self, argsString):
5619 """
5620 Private slot to process a remote StartEdit command
5621
5622 @param argsString string containing the command parameters (string)
5623 """
5624 if not self.__inSharedEdit and not self.__inRemoteSharedEdit:
5625 self.__inRemoteSharedEdit = True
5626 self.setReadOnly(True)
5627 self.__updateReadOnly()
5628 hash = str(
5629 QCryptographicHash.hash(
5630 Utilities.encode(self.text(), self.encoding)[0],
5631 QCryptographicHash.Sha1).toHex(),
5632 encoding = "utf-8")
5633 if hash != argsString:
5634 # text is different to the remote site, request to sync it
5635 self.__isSyncing = True
5636 self.__send(Editor.RequestSyncToken, argsString)
5637
5638 def __calculateChanges(self, old, new):
5639 """
5640 Private method to determine change commands to convert old text into
5641 new text.
5642
5643 @param old old text (string)
5644 @param new new text (string)
5645 @return commands to change old into new (string)
5646 """
5647 oldL = old.splitlines()
5648 newL = new.splitlines()
5649 matcher = difflib.SequenceMatcher(None, oldL, newL)
5650
5651 formatStr = "@@{0} {1} {2} {3}"
5652 commands = []
5653 for token, i1, i2, j1, j2 in matcher.get_opcodes():
5654 if token == "insert":
5655 commands.append(formatStr.format("i", j1, j2 - j1, -1))
5656 commands.extend(newL[j1:j2])
5657 elif token == "delete":
5658 commands.append(formatStr.format("d", j1, i2 - i1, -1))
5659 elif token == "replace":
5660 commands.append(formatStr.format("r", j1, i2 - i1, j2 - j1))
5661 commands.extend(newL[j1:j2])
5662
5663 return "\n".join(commands) + "\n"
5664
5665 def __processEndEditCommand(self, argsString):
5666 """
5667 Private slot to process a remote EndEdit command
5668
5669 @param argsString string containing the command parameters (string)
5670 """
5671 commands = argsString.splitlines()
5672 sep = self.getLineSeparator()
5673 cur = self.getCursorPosition()
5674
5675 self.setReadOnly(False)
5676 self.beginUndoAction()
5677 while commands:
5678 commandLine = commands.pop(0)
5679 if not commandLine.startswith("@@"):
5680 continue
5681
5682 command, *args = commandLine.split()
5683 pos, l1, l2 = [int(arg) for arg in args]
5684 if command == "@@i":
5685 txt = sep.join(commands[0:l1]) + sep
5686 self.insertAt(txt, pos, 0)
5687 del commands[0:l1]
5688 elif command == "@@d":
5689 self.setSelection(pos, 0, pos + l1, 0)
5690 self.removeSelectedText()
5691 elif command == "@@r":
5692 self.setSelection(pos, 0, pos + l1, 0)
5693 self.removeSelectedText()
5694 txt = sep.join(commands[0:l2]) + sep
5695 self.insertAt(txt, pos, 0)
5696 del commands[0:l2]
5697 self.endUndoAction()
5698 self.__updateReadOnly()
5699 self.__inRemoteSharedEdit = False
5700
5701 self.setCursorPosition(*cur)
5702
5703 def __processRequestSyncCommand(self, argsString):
5704 """
5705 Private slot to process a remote RequestSync command
5706
5707 @param argsString string containing the command parameters (string)
5708 """
5709 if self.__inSharedEdit:
5710 hash = str(
5711 QCryptographicHash.hash(
5712 Utilities.encode(self.__savedText, self.encoding)[0],
5713 QCryptographicHash.Sha1).toHex(),
5714 encoding = "utf-8")
5715
5716 if hash == argsString:
5717 self.__send(Editor.SyncToken, self.__savedText)
5718
5719 def __processSyncCommand(self, argsString):
5720 """
5721 Private slot to process a remote Sync command
5722
5723 @param argsString string containing the command parameters (string)
5724 """
5725 if self.__isSyncing:
5726 cur = self.getCursorPosition()
5727
5728 self.setReadOnly(False)
5729 self.beginUndoAction()
5730 self.selectAll()
5731 self.removeSelectedText()
5732 self.insertAt(argsString, 0, 0)
5733 self.endUndoAction()
5734 self.setReadOnly(True)
5735
5736 self.setCursorPosition(*cur)
5737
5738 while self.__receivedWhileSyncing:
5739 command = self.__receivedWhileSyncing.pop(0)
5740 self.__dispatchCommand(command)
5741
5742 self.__isSyncing = False

eric ide

mercurial