123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598 |
- # 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()
|