# Code that allows Pythonwin to pretend it is IDLE # (at least as far as most IDLE extensions are concerned) import string import sys import win32api import win32con import win32ui from pywin import default_scintilla_encoding from pywin.mfc.dialog import GetSimpleInput wordchars = string.ascii_uppercase + string.ascii_lowercase + string.digits class TextError(Exception): # When a TclError would normally be raised. pass class EmptyRange(Exception): # Internally raised. pass def GetIDLEModule(module): try: # First get it from Pythonwin it is exists. modname = "pywin.idle." + module __import__(modname) except ImportError as details: msg = ( "The IDLE extension '%s' can not be located.\r\n\r\n" "Please correct the installation and restart the" " application.\r\n\r\n%s" % (module, details) ) win32ui.MessageBox(msg) return None mod = sys.modules[modname] mod.TclError = TextError # A hack that can go soon! return mod # A class that is injected into the IDLE auto-indent extension. # It allows for decent performance when opening a new file, # as auto-indent uses the tokenizer module to determine indents. # The default AutoIndent readline method works OK, but it goes through # this layer of Tk index indirection for every single line. For large files # without indents (and even small files with indents :-) it was pretty slow! def fast_readline(self): if self.finished: val = "" else: if "_scint_lines" not in self.__dict__: # XXX - note - assumes this is only called once the file is loaded! self._scint_lines = self.text.edit.GetTextRange().split("\n") sl = self._scint_lines i = self.i = self.i + 1 if i >= len(sl): val = "" else: val = sl[i] + "\n" return val.encode(default_scintilla_encoding) try: GetIDLEModule("AutoIndent").IndentSearcher.readline = fast_readline except AttributeError: # GetIDLEModule may return None pass # A class that attempts to emulate an IDLE editor window. # Construct with a Pythonwin view. class IDLEEditorWindow: def __init__(self, edit): self.edit = edit self.text = TkText(edit) self.extensions = {} self.extension_menus = {} def close(self): self.edit = self.text = None self.extension_menus = None try: for ext in self.extensions.values(): closer = getattr(ext, "close", None) if closer is not None: closer() finally: self.extensions = {} def IDLEExtension(self, extension): ext = self.extensions.get(extension) if ext is not None: return ext mod = GetIDLEModule(extension) if mod is None: return None klass = getattr(mod, extension) ext = self.extensions[extension] = klass(self) # Find and bind all the events defined in the extension. events = [item for item in dir(klass) if item[-6:] == "_event"] for event in events: name = "<<%s>>" % (event[:-6].replace("_", "-"),) self.edit.bindings.bind(name, getattr(ext, event)) return ext def GetMenuItems(self, menu_name): # Get all menu items for the menu name (eg, "edit") bindings = self.edit.bindings ret = [] for ext in self.extensions.values(): menudefs = getattr(ext, "menudefs", []) for name, items in menudefs: if name == menu_name: for text, event in [item for item in items if item is not None]: text = text.replace("&", "&&") text = text.replace("_", "&") ret.append((text, event)) return ret ###################################################################### # The IDLE "Virtual UI" methods that are exposed to the IDLE extensions. # def askinteger( self, caption, prompt, parent=None, initialvalue=0, minvalue=None, maxvalue=None ): while 1: rc = GetSimpleInput(prompt, str(initialvalue), caption) if rc is None: return 0 # Correct "cancel" semantics? err = None try: rc = int(rc) except ValueError: err = "Please enter an integer" if not err and minvalue is not None and rc < minvalue: err = "Please enter an integer greater then or equal to %s" % ( minvalue, ) if not err and maxvalue is not None and rc > maxvalue: err = "Please enter an integer less then or equal to %s" % (maxvalue,) if err: win32ui.MessageBox(err, caption, win32con.MB_OK) continue return rc def askyesno(self, caption, prompt, parent=None): return win32ui.MessageBox(prompt, caption, win32con.MB_YESNO) == win32con.IDYES ###################################################################### # The IDLE "Virtual Text Widget" methods that are exposed to the IDLE extensions. # # Is character at text_index in a Python string? Return 0 for # "guaranteed no", true for anything else. def is_char_in_string(self, text_index): # A helper for the code analyser - we need internal knowledge of # the colorizer to get this information # This assumes the colorizer has got to this point! text_index = self.text._getoffset(text_index) c = self.text.edit._GetColorizer() if c and c.GetStringStyle(text_index) is None: return 0 return 1 # If a selection is defined in the text widget, return # (start, end) as Tkinter text indices, otherwise return # (None, None) def get_selection_indices(self): try: first = self.text.index("sel.first") last = self.text.index("sel.last") return first, last except TextError: return None, None def set_tabwidth(self, width): self.edit.SCISetTabWidth(width) def get_tabwidth(self): return self.edit.GetTabWidth() # A class providing the generic "Call Tips" interface class CallTips: def __init__(self, edit): self.edit = edit def showtip(self, tip_text): self.edit.SCICallTipShow(tip_text) def hidetip(self): self.edit.SCICallTipCancel() ######################################## # # Helpers for the TkText emulation. def TkOffsetToIndex(offset, edit): lineoff = 0 # May be 1 > actual end if we pretended there was a trailing '\n' offset = min(offset, edit.GetTextLength()) line = edit.LineFromChar(offset) lineIndex = edit.LineIndex(line) return "%d.%d" % (line + 1, offset - lineIndex) def _NextTok(str, pos): # Returns (token, endPos) end = len(str) if pos >= end: return None, 0 while pos < end and str[pos] in string.whitespace: pos = pos + 1 # Special case for +- if str[pos] in "+-": return str[pos], pos + 1 # Digits also a special case. endPos = pos while endPos < end and str[endPos] in string.digits + ".": endPos = endPos + 1 if pos != endPos: return str[pos:endPos], endPos endPos = pos while endPos < end and str[endPos] not in string.whitespace + string.digits + "+-": endPos = endPos + 1 if pos != endPos: return str[pos:endPos], endPos return None, 0 def TkIndexToOffset(bm, edit, marks): base, nextTokPos = _NextTok(bm, 0) if base is None: raise ValueError("Empty bookmark ID!") if base.find(".") > 0: try: line, col = base.split(".", 2) if col == "first" or col == "last": # Tag name if line != "sel": raise ValueError("Tags arent here!") sel = edit.GetSel() if sel[0] == sel[1]: raise EmptyRange if col == "first": pos = sel[0] else: pos = sel[1] else: # Lines are 1 based for tkinter line = int(line) - 1 if line > edit.GetLineCount(): pos = edit.GetTextLength() + 1 else: pos = edit.LineIndex(line) if pos == -1: pos = edit.GetTextLength() pos = pos + int(col) except (ValueError, IndexError): raise ValueError("Unexpected literal in '%s'" % base) elif base == "insert": pos = edit.GetSel()[0] elif base == "end": pos = edit.GetTextLength() # Pretend there is a trailing '\n' if necessary if pos and edit.SCIGetCharAt(pos - 1) != "\n": pos = pos + 1 else: try: pos = marks[base] except KeyError: raise ValueError("Unsupported base offset or undefined mark '%s'" % base) while 1: word, nextTokPos = _NextTok(bm, nextTokPos) if word is None: break if word in ("+", "-"): num, nextTokPos = _NextTok(bm, nextTokPos) if num is None: raise ValueError("+/- operator needs 2 args") what, nextTokPos = _NextTok(bm, nextTokPos) if what is None: raise ValueError("+/- operator needs 2 args") if what[0] != "c": raise ValueError("+/- only supports chars") if word == "+": pos = pos + int(num) else: pos = pos - int(num) elif word == "wordstart": while pos > 0 and edit.SCIGetCharAt(pos - 1) in wordchars: pos = pos - 1 elif word == "wordend": end = edit.GetTextLength() while pos < end and edit.SCIGetCharAt(pos) in wordchars: pos = pos + 1 elif word == "linestart": while pos > 0 and edit.SCIGetCharAt(pos - 1) not in "\n\r": pos = pos - 1 elif word == "lineend": end = edit.GetTextLength() while pos < end and edit.SCIGetCharAt(pos) not in "\n\r": pos = pos + 1 else: raise ValueError("Unsupported relative offset '%s'" % word) return max(pos, 0) # Tkinter is tollerant of -ve indexes - we aren't # A class that resembles an IDLE (ie, a Tk) text widget. # Construct with an edit object (eg, an editor view) class TkText: def __init__(self, edit): self.calltips = None self.edit = edit self.marks = {} ## def __getattr__(self, attr): ## if attr=="tk": return self # So text.tk.call works. ## if attr=="master": return None # ditto! ## raise AttributeError, attr ## def __getitem__(self, item): ## if item=="tabs": ## size = self.edit.GetTabWidth() ## if size==8: return "" # Tk default ## return size # correct semantics? ## elif item=="font": # Used for measurements we dont need to do! ## return "Dont know the font" ## raise IndexError, "Invalid index '%s'" % item def make_calltip_window(self): if self.calltips is None: self.calltips = CallTips(self.edit) return self.calltips def _getoffset(self, index): return TkIndexToOffset(index, self.edit, self.marks) def _getindex(self, off): return TkOffsetToIndex(off, self.edit) def _fix_indexes(self, start, end): # first some magic to handle skipping over utf8 extended chars. while start > 0 and ord(self.edit.SCIGetCharAt(start)) & 0xC0 == 0x80: start -= 1 while ( end < self.edit.GetTextLength() and ord(self.edit.SCIGetCharAt(end)) & 0xC0 == 0x80 ): end += 1 # now handling fixing \r\n->\n disparities... if ( start > 0 and self.edit.SCIGetCharAt(start) == "\n" and self.edit.SCIGetCharAt(start - 1) == "\r" ): start = start - 1 if ( end < self.edit.GetTextLength() and self.edit.SCIGetCharAt(end - 1) == "\r" and self.edit.SCIGetCharAt(end) == "\n" ): end = end + 1 return start, end ## def get_tab_width(self): ## return self.edit.GetTabWidth() ## def call(self, *rest): ## # Crap to support Tk measurement hacks for tab widths ## if rest[0] != "font" or rest[1] != "measure": ## raise ValueError, "Unsupport call type" ## return len(rest[5]) ## def configure(self, **kw): ## for name, val in kw.items(): ## if name=="tabs": ## self.edit.SCISetTabWidth(int(val)) ## else: ## raise ValueError, "Unsupported configuration item %s" % kw def bind(self, binding, handler): self.edit.bindings.bind(binding, handler) def get(self, start, end=None): try: start = self._getoffset(start) if end is None: end = start + 1 else: end = self._getoffset(end) except EmptyRange: return "" # Simple semantic checks to conform to the Tk text interface if end <= start: return "" max = self.edit.GetTextLength() checkEnd = 0 if end > max: end = max checkEnd = 1 start, end = self._fix_indexes(start, end) ret = self.edit.GetTextRange(start, end) # pretend a trailing '\n' exists if necessary. if checkEnd and (not ret or ret[-1] != "\n"): ret = ret + "\n" return ret.replace("\r", "") def index(self, spec): try: return self._getindex(self._getoffset(spec)) except EmptyRange: return "" def insert(self, pos, text): try: pos = self._getoffset(pos) except EmptyRange: raise TextError("Empty range") self.edit.SetSel((pos, pos)) # IDLE only deals with "\n" - we will be nicer bits = text.split("\n") self.edit.SCIAddText(bits[0]) for bit in bits[1:]: self.edit.SCINewline() self.edit.SCIAddText(bit) def delete(self, start, end=None): try: start = self._getoffset(start) if end is not None: end = self._getoffset(end) except EmptyRange: raise TextError("Empty range") # If end is specified and == start, then we must delete nothing. if start == end: return # If end is not specified, delete one char if end is None: end = start + 1 else: # Tk says not to delete in this case, but our control would. if end < start: return if start == self.edit.GetTextLength(): return # Nothing to delete. old = self.edit.GetSel()[0] # Lose a selection # Hack for partial '\r\n' and UTF-8 char removal start, end = self._fix_indexes(start, end) self.edit.SetSel((start, end)) self.edit.Clear() if old >= start and old < end: old = start elif old >= end: old = old - (end - start) self.edit.SetSel(old) def bell(self): win32api.MessageBeep() def see(self, pos): # Most commands we use in Scintilla actually force the selection # to be seen, making this unnecessary. pass def mark_set(self, name, pos): try: pos = self._getoffset(pos) except EmptyRange: raise TextError("Empty range '%s'" % pos) if name == "insert": self.edit.SetSel(pos) else: self.marks[name] = pos def tag_add(self, name, start, end): if name != "sel": raise ValueError("Only sel tag is supported") try: start = self._getoffset(start) end = self._getoffset(end) except EmptyRange: raise TextError("Empty range") self.edit.SetSel(start, end) def tag_remove(self, name, start, end): if name != "sel" or start != "1.0" or end != "end": raise ValueError("Cant remove this tag") # Turn the sel into a cursor self.edit.SetSel(self.edit.GetSel()[0]) def compare(self, i1, op, i2): try: i1 = self._getoffset(i1) except EmptyRange: i1 = "" try: i2 = self._getoffset(i2) except EmptyRange: i2 = "" return eval("%d%s%d" % (i1, op, i2)) def undo_block_start(self): self.edit.SCIBeginUndoAction() def undo_block_stop(self): self.edit.SCIEndUndoAction() ###################################################################### # # Test related code. # ###################################################################### def TestCheck(index, edit, expected=None): rc = TkIndexToOffset(index, edit, {}) if rc != expected: print("ERROR: Index", index, ", expected", expected, "but got", rc) def TestGet(fr, to, t, expected): got = t.get(fr, to) if got != expected: print( "ERROR: get(%s, %s) expected %s, but got %s" % (repr(fr), repr(to), repr(expected), repr(got)) ) def test(): import pywin.framework.editor d = pywin.framework.editor.editorTemplate.OpenDocumentFile(None) e = d.GetFirstView() t = TkText(e) e.SCIAddText("hi there how\nare you today\r\nI hope you are well") e.SetSel((4, 4)) skip = """ TestCheck("insert", e, 4) TestCheck("insert wordstart", e, 3) TestCheck("insert wordend", e, 8) TestCheck("insert linestart", e, 0) TestCheck("insert lineend", e, 12) TestCheck("insert + 4 chars", e, 8) TestCheck("insert +4c", e, 8) TestCheck("insert - 2 chars", e, 2) TestCheck("insert -2c", e, 2) TestCheck("insert-2c", e, 2) TestCheck("insert-2 c", e, 2) TestCheck("insert- 2c", e, 2) TestCheck("1.1", e, 1) TestCheck("1.0", e, 0) TestCheck("2.0", e, 13) try: TestCheck("sel.first", e, 0) print "*** sel.first worked with an empty selection" except TextError: pass e.SetSel((4,5)) TestCheck("sel.first- 2c", e, 2) TestCheck("sel.last- 2c", e, 3) """ # Check EOL semantics e.SetSel((4, 4)) TestGet("insert lineend", "insert lineend +1c", t, "\n") e.SetSel((20, 20)) TestGet("insert lineend", "insert lineend +1c", t, "\n") e.SetSel((35, 35)) TestGet("insert lineend", "insert lineend +1c", t, "\n") class IDLEWrapper: def __init__(self, control): self.text = control def IDLETest(extension): import os import sys modname = "pywin.idle." + extension __import__(modname) mod = sys.modules[modname] mod.TclError = TextError klass = getattr(mod, extension) # Create a new Scintilla Window. import pywin.framework.editor d = pywin.framework.editor.editorTemplate.OpenDocumentFile(None) v = d.GetFirstView() fname = os.path.splitext(__file__)[0] + ".py" v.SCIAddText(open(fname).read()) d.SetModifiedFlag(0) r = klass(IDLEWrapper(TkText(v))) return r if __name__ == "__main__": test()