codekingpro/portable-devtools
114k
1# Code that allows Pythonwin to pretend it is IDLE2# (at least as far as most IDLE extensions are concerned)3 4import string5import sys6 7import win32api8import win32con9import win32ui10from pywin import default_scintilla_encoding11from pywin.mfc.dialog import GetSimpleInput12 13wordchars = string.ascii_uppercase + string.ascii_lowercase + string.digits14 15 16class TextError(Exception): # When a TclError would normally be raised.17 pass18 19 20class EmptyRange(Exception): # Internally raised.21 pass22 23 24def GetIDLEModule(module):25 try:26 # First get it from Pythonwin it is exists.27 modname = "pywin.idle." + module28 __import__(modname)29 except ImportError as details:30 msg = (31 "The IDLE extension '%s' can not be located.\r\n\r\n"32 "Please correct the installation and restart the"33 " application.\r\n\r\n%s" % (module, details)34 )35 win32ui.MessageBox(msg)36 return None37 mod = sys.modules[modname]38 mod.TclError = TextError # A hack that can go soon!39 return mod40 41 42# A class that is injected into the IDLE auto-indent extension.43# It allows for decent performance when opening a new file,44# as auto-indent uses the tokenizer module to determine indents.45# The default AutoIndent readline method works OK, but it goes through46# this layer of Tk index indirection for every single line. For large files47# without indents (and even small files with indents :-) it was pretty slow!48def fast_readline(self):49 if self.finished:50 val = ""51 else:52 if "_scint_lines" not in self.__dict__:53 # XXX - note - assumes this is only called once the file is loaded!54 self._scint_lines = self.text.edit.GetTextRange().split("\n")55 sl = self._scint_lines56 i = self.i = self.i + 157 if i >= len(sl):58 val = ""59 else:60 val = sl[i] + "\n"61 return val.encode(default_scintilla_encoding)62 63 64try:65 GetIDLEModule("AutoIndent").IndentSearcher.readline = fast_readline66except AttributeError: # GetIDLEModule may return None67 pass68 69 70# A class that attempts to emulate an IDLE editor window.71# Construct with a Pythonwin view.72class IDLEEditorWindow:73 def __init__(self, edit):74 self.edit = edit75 self.text = TkText(edit)76 self.extensions = {}77 self.extension_menus = {}78 79 def close(self):80 self.edit = self.text = None81 self.extension_menus = None82 try:83 for ext in self.extensions.values():84 closer = getattr(ext, "close", None)85 if closer is not None:86 closer()87 finally:88 self.extensions = {}89 90 def IDLEExtension(self, extension):91 ext = self.extensions.get(extension)92 if ext is not None:93 return ext94 mod = GetIDLEModule(extension)95 if mod is None:96 return None97 klass = getattr(mod, extension)98 ext = self.extensions[extension] = klass(self)99 # Find and bind all the events defined in the extension.100 events = [item for item in dir(klass) if item[-6:] == "_event"]101 for event in events:102 name = "<<%s>>" % (event[:-6].replace("_", "-"),)103 self.edit.bindings.bind(name, getattr(ext, event))104 return ext105 106 def GetMenuItems(self, menu_name):107 # Get all menu items for the menu name (eg, "edit")108 bindings = self.edit.bindings109 ret = []110 for ext in self.extensions.values():111 menudefs = getattr(ext, "menudefs", [])112 for name, items in menudefs:113 if name == menu_name:114 for text, event in [item for item in items if item is not None]:115 text = text.replace("&", "&&")116 text = text.replace("_", "&")117 ret.append((text, event))118 return ret119 120 ######################################################################121 # The IDLE "Virtual UI" methods that are exposed to the IDLE extensions.122 #123 def askinteger(124 self, caption, prompt, parent=None, initialvalue=0, minvalue=None, maxvalue=None125 ):126 while 1:127 rc = GetSimpleInput(prompt, str(initialvalue), caption)128 if rc is None:129 return 0 # Correct "cancel" semantics?130 err = None131 try:132 rc = int(rc)133 except ValueError:134 err = "Please enter an integer"135 if not err and minvalue is not None and rc < minvalue:136 err = "Please enter an integer greater then or equal to %s" % (137 minvalue,138 )139 if not err and maxvalue is not None and rc > maxvalue:140 err = "Please enter an integer less then or equal to %s" % (maxvalue,)141 if err:142 win32ui.MessageBox(err, caption, win32con.MB_OK)143 continue144 return rc145 146 def askyesno(self, caption, prompt, parent=None):147 return win32ui.MessageBox(prompt, caption, win32con.MB_YESNO) == win32con.IDYES148 149 ######################################################################150 # The IDLE "Virtual Text Widget" methods that are exposed to the IDLE extensions.151 #152 153 # Is character at text_index in a Python string? Return 0 for154 # "guaranteed no", true for anything else.155 def is_char_in_string(self, text_index):156 # A helper for the code analyser - we need internal knowledge of157 # the colorizer to get this information158 # This assumes the colorizer has got to this point!159 text_index = self.text._getoffset(text_index)160 c = self.text.edit._GetColorizer()161 if c and c.GetStringStyle(text_index) is None:162 return 0163 return 1164 165 # If a selection is defined in the text widget, return166 # (start, end) as Tkinter text indices, otherwise return167 # (None, None)168 def get_selection_indices(self):169 try:170 first = self.text.index("sel.first")171 last = self.text.index("sel.last")172 return first, last173 except TextError:174 return None, None175 176 def set_tabwidth(self, width):177 self.edit.SCISetTabWidth(width)178 179 def get_tabwidth(self):180 return self.edit.GetTabWidth()181 182 183# A class providing the generic "Call Tips" interface184class CallTips:185 def __init__(self, edit):186 self.edit = edit187 188 def showtip(self, tip_text):189 self.edit.SCICallTipShow(tip_text)190 191 def hidetip(self):192 self.edit.SCICallTipCancel()193 194 195########################################196#197# Helpers for the TkText emulation.198def TkOffsetToIndex(offset, edit):199 lineoff = 0200 # May be 1 > actual end if we pretended there was a trailing '\n'201 offset = min(offset, edit.GetTextLength())202 line = edit.LineFromChar(offset)203 lineIndex = edit.LineIndex(line)204 return "%d.%d" % (line + 1, offset - lineIndex)205 206 207def _NextTok(str, pos):208 # Returns (token, endPos)209 end = len(str)210 if pos >= end:211 return None, 0212 while pos < end and str[pos] in string.whitespace:213 pos = pos + 1214 # Special case for +-215 if str[pos] in "+-":216 return str[pos], pos + 1217 # Digits also a special case.218 endPos = pos219 while endPos < end and str[endPos] in string.digits + ".":220 endPos = endPos + 1221 if pos != endPos:222 return str[pos:endPos], endPos223 endPos = pos224 while endPos < end and str[endPos] not in string.whitespace + string.digits + "+-":225 endPos = endPos + 1226 if pos != endPos:227 return str[pos:endPos], endPos228 return None, 0229 230 231def TkIndexToOffset(bm, edit, marks):232 base, nextTokPos = _NextTok(bm, 0)233 if base is None:234 raise ValueError("Empty bookmark ID!")235 if base.find(".") > 0:236 try:237 line, col = base.split(".", 2)238 if col == "first" or col == "last":239 # Tag name240 if line != "sel":241 raise ValueError("Tags arent here!")242 sel = edit.GetSel()243 if sel[0] == sel[1]:244 raise EmptyRange245 if col == "first":246 pos = sel[0]247 else:248 pos = sel[1]249 else:250 # Lines are 1 based for tkinter251 line = int(line) - 1252 if line > edit.GetLineCount():253 pos = edit.GetTextLength() + 1254 else:255 pos = edit.LineIndex(line)256 if pos == -1:257 pos = edit.GetTextLength()258 pos = pos + int(col)259 except (ValueError, IndexError):260 raise ValueError("Unexpected literal in '%s'" % base)261 elif base == "insert":262 pos = edit.GetSel()[0]263 elif base == "end":264 pos = edit.GetTextLength()265 # Pretend there is a trailing '\n' if necessary266 if pos and edit.SCIGetCharAt(pos - 1) != "\n":267 pos = pos + 1268 else:269 try:270 pos = marks[base]271 except KeyError:272 raise ValueError("Unsupported base offset or undefined mark '%s'" % base)273 274 while 1:275 word, nextTokPos = _NextTok(bm, nextTokPos)276 if word is None:277 break278 if word in ("+", "-"):279 num, nextTokPos = _NextTok(bm, nextTokPos)280 if num is None:281 raise ValueError("+/- operator needs 2 args")282 what, nextTokPos = _NextTok(bm, nextTokPos)283 if what is None:284 raise ValueError("+/- operator needs 2 args")285 if what[0] != "c":286 raise ValueError("+/- only supports chars")287 if word == "+":288 pos = pos + int(num)289 else:290 pos = pos - int(num)291 elif word == "wordstart":292 while pos > 0 and edit.SCIGetCharAt(pos - 1) in wordchars:293 pos = pos - 1294 elif word == "wordend":295 end = edit.GetTextLength()296 while pos < end and edit.SCIGetCharAt(pos) in wordchars:297 pos = pos + 1298 elif word == "linestart":299 while pos > 0 and edit.SCIGetCharAt(pos - 1) not in "\n\r":300 pos = pos - 1301 elif word == "lineend":302 end = edit.GetTextLength()303 while pos < end and edit.SCIGetCharAt(pos) not in "\n\r":304 pos = pos + 1305 else:306 raise ValueError("Unsupported relative offset '%s'" % word)307 return max(pos, 0) # Tkinter is tollerant of -ve indexes - we aren't308 309 310# A class that resembles an IDLE (ie, a Tk) text widget.311# Construct with an edit object (eg, an editor view)312class TkText:313 def __init__(self, edit):314 self.calltips = None315 self.edit = edit316 self.marks = {}317 318 ## def __getattr__(self, attr):319 ## if attr=="tk": return self # So text.tk.call works.320 ## if attr=="master": return None # ditto!321 ## raise AttributeError, attr322 ## def __getitem__(self, item):323 ## if item=="tabs":324 ## size = self.edit.GetTabWidth()325 ## if size==8: return "" # Tk default326 ## return size # correct semantics?327 ## elif item=="font": # Used for measurements we dont need to do!328 ## return "Dont know the font"329 ## raise IndexError, "Invalid index '%s'" % item330 def make_calltip_window(self):331 if self.calltips is None:332 self.calltips = CallTips(self.edit)333 return self.calltips334 335 def _getoffset(self, index):336 return TkIndexToOffset(index, self.edit, self.marks)337 338 def _getindex(self, off):339 return TkOffsetToIndex(off, self.edit)340 341 def _fix_indexes(self, start, end):342 # first some magic to handle skipping over utf8 extended chars.343 while start > 0 and ord(self.edit.SCIGetCharAt(start)) & 0xC0 == 0x80:344 start -= 1345 while (346 end < self.edit.GetTextLength()347 and ord(self.edit.SCIGetCharAt(end)) & 0xC0 == 0x80348 ):349 end += 1350 # now handling fixing \r\n->\n disparities...351 if (352 start > 0353 and self.edit.SCIGetCharAt(start) == "\n"354 and self.edit.SCIGetCharAt(start - 1) == "\r"355 ):356 start = start - 1357 if (358 end < self.edit.GetTextLength()359 and self.edit.SCIGetCharAt(end - 1) == "\r"360 and self.edit.SCIGetCharAt(end) == "\n"361 ):362 end = end + 1363 return start, end364 365 ## def get_tab_width(self):366 ## return self.edit.GetTabWidth()367 ## def call(self, *rest):368 ## # Crap to support Tk measurement hacks for tab widths369 ## if rest[0] != "font" or rest[1] != "measure":370 ## raise ValueError, "Unsupport call type"371 ## return len(rest[5])372 ## def configure(self, **kw):373 ## for name, val in kw.items():374 ## if name=="tabs":375 ## self.edit.SCISetTabWidth(int(val))376 ## else:377 ## raise ValueError, "Unsupported configuration item %s" % kw378 def bind(self, binding, handler):379 self.edit.bindings.bind(binding, handler)380 381 def get(self, start, end=None):382 try:383 start = self._getoffset(start)384 if end is None:385 end = start + 1386 else:387 end = self._getoffset(end)388 except EmptyRange:389 return ""390 # Simple semantic checks to conform to the Tk text interface391 if end <= start:392 return ""393 max = self.edit.GetTextLength()394 checkEnd = 0395 if end > max:396 end = max397 checkEnd = 1398 start, end = self._fix_indexes(start, end)399 ret = self.edit.GetTextRange(start, end)400 # pretend a trailing '\n' exists if necessary.401 if checkEnd and (not ret or ret[-1] != "\n"):402 ret = ret + "\n"403 return ret.replace("\r", "")404 405 def index(self, spec):406 try:407 return self._getindex(self._getoffset(spec))408 except EmptyRange:409 return ""410 411 def insert(self, pos, text):412 try:413 pos = self._getoffset(pos)414 except EmptyRange:415 raise TextError("Empty range")416 self.edit.SetSel((pos, pos))417 # IDLE only deals with "\n" - we will be nicer418 419 bits = text.split("\n")420 self.edit.SCIAddText(bits[0])421 for bit in bits[1:]:422 self.edit.SCINewline()423 self.edit.SCIAddText(bit)424 425 def delete(self, start, end=None):426 try:427 start = self._getoffset(start)428 if end is not None:429 end = self._getoffset(end)430 except EmptyRange:431 raise TextError("Empty range")432 # If end is specified and == start, then we must delete nothing.433 if start == end:434 return435 # If end is not specified, delete one char436 if end is None:437 end = start + 1438 else:439 # Tk says not to delete in this case, but our control would.440 if end < start:441 return442 if start == self.edit.GetTextLength():443 return # Nothing to delete.444 old = self.edit.GetSel()[0] # Lose a selection445 # Hack for partial '\r\n' and UTF-8 char removal446 start, end = self._fix_indexes(start, end)447 self.edit.SetSel((start, end))448 self.edit.Clear()449 if old >= start and old < end:450 old = start451 elif old >= end:452 old = old - (end - start)453 self.edit.SetSel(old)454 455 def bell(self):456 win32api.MessageBeep()457 458 def see(self, pos):459 # Most commands we use in Scintilla actually force the selection460 # to be seen, making this unnecessary.461 pass462 463 def mark_set(self, name, pos):464 try:465 pos = self._getoffset(pos)466 except EmptyRange:467 raise TextError("Empty range '%s'" % pos)468 if name == "insert":469 self.edit.SetSel(pos)470 else:471 self.marks[name] = pos472 473 def tag_add(self, name, start, end):474 if name != "sel":475 raise ValueError("Only sel tag is supported")476 try:477 start = self._getoffset(start)478 end = self._getoffset(end)479 except EmptyRange:480 raise TextError("Empty range")481 self.edit.SetSel(start, end)482 483 def tag_remove(self, name, start, end):484 if name != "sel" or start != "1.0" or end != "end":485 raise ValueError("Cant remove this tag")486 # Turn the sel into a cursor487 self.edit.SetSel(self.edit.GetSel()[0])488 489 def compare(self, i1, op, i2):490 try:491 i1 = self._getoffset(i1)492 except EmptyRange:493 i1 = ""494 try:495 i2 = self._getoffset(i2)496 except EmptyRange:497 i2 = ""498 return eval("%d%s%d" % (i1, op, i2))499 500 def undo_block_start(self):501 self.edit.SCIBeginUndoAction()502 503 def undo_block_stop(self):504 self.edit.SCIEndUndoAction()505 506 507######################################################################508#509# Test related code.510#511######################################################################512def TestCheck(index, edit, expected=None):513 rc = TkIndexToOffset(index, edit, {})514 if rc != expected:515 print("ERROR: Index", index, ", expected", expected, "but got", rc)516 517 518def TestGet(fr, to, t, expected):519 got = t.get(fr, to)520 if got != expected:521 print(522 "ERROR: get(%s, %s) expected %s, but got %s"523 % (repr(fr), repr(to), repr(expected), repr(got))524 )525 526 527def test():528 import pywin.framework.editor529 530 d = pywin.framework.editor.editorTemplate.OpenDocumentFile(None)531 e = d.GetFirstView()532 t = TkText(e)533 e.SCIAddText("hi there how\nare you today\r\nI hope you are well")534 e.SetSel((4, 4))535 536 skip = """537 TestCheck("insert", e, 4)538 TestCheck("insert wordstart", e, 3)539 TestCheck("insert wordend", e, 8)540 TestCheck("insert linestart", e, 0)541 TestCheck("insert lineend", e, 12)542 TestCheck("insert + 4 chars", e, 8)543 TestCheck("insert +4c", e, 8)544 TestCheck("insert - 2 chars", e, 2)545 TestCheck("insert -2c", e, 2)546 TestCheck("insert-2c", e, 2)547 TestCheck("insert-2 c", e, 2)548 TestCheck("insert- 2c", e, 2)549 TestCheck("1.1", e, 1)550 TestCheck("1.0", e, 0)551 TestCheck("2.0", e, 13)552 try:553 TestCheck("sel.first", e, 0)554 print "*** sel.first worked with an empty selection"555 except TextError:556 pass557 e.SetSel((4,5))558 TestCheck("sel.first- 2c", e, 2)559 TestCheck("sel.last- 2c", e, 3)560 """561 # Check EOL semantics562 e.SetSel((4, 4))563 TestGet("insert lineend", "insert lineend +1c", t, "\n")564 e.SetSel((20, 20))565 TestGet("insert lineend", "insert lineend +1c", t, "\n")566 e.SetSel((35, 35))567 TestGet("insert lineend", "insert lineend +1c", t, "\n")568 569 570class IDLEWrapper:571 def __init__(self, control):572 self.text = control573 574 575def IDLETest(extension):576 import os577 import sys578 579 modname = "pywin.idle." + extension580 __import__(modname)581 mod = sys.modules[modname]582 mod.TclError = TextError583 klass = getattr(mod, extension)584 585 # Create a new Scintilla Window.586 import pywin.framework.editor587 588 d = pywin.framework.editor.editorTemplate.OpenDocumentFile(None)589 v = d.GetFirstView()590 fname = os.path.splitext(__file__)[0] + ".py"591 v.SCIAddText(open(fname).read())592 d.SetModifiedFlag(0)593 r = klass(IDLEWrapper(TkText(v)))594 return r595 596 597if __name__ == "__main__":598 test()599 