IDLEenvironment.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. # Code that allows Pythonwin to pretend it is IDLE
  2. # (at least as far as most IDLE extensions are concerned)
  3. import string
  4. import win32api
  5. import win32ui
  6. import win32con
  7. import sys
  8. from pywin.mfc.dialog import GetSimpleInput
  9. from pywin import default_scintilla_encoding
  10. wordchars = string.ascii_uppercase + string.ascii_lowercase + string.digits
  11. class TextError(Exception): # When a TclError would normally be raised.
  12. pass
  13. class EmptyRange(Exception): # Internally raised.
  14. pass
  15. def GetIDLEModule(module):
  16. try:
  17. # First get it from Pythonwin it is exists.
  18. modname = "pywin.idle." + module
  19. __import__(modname)
  20. except ImportError as details:
  21. msg = "The IDLE extension '%s' can not be located.\r\n\r\n" \
  22. "Please correct the installation and restart the" \
  23. " application.\r\n\r\n%s" % (module, details)
  24. win32ui.MessageBox(msg)
  25. return None
  26. mod=sys.modules[modname]
  27. mod.TclError = TextError # A hack that can go soon!
  28. return mod
  29. # A class that is injected into the IDLE auto-indent extension.
  30. # It allows for decent performance when opening a new file,
  31. # as auto-indent uses the tokenizer module to determine indents.
  32. # The default AutoIndent readline method works OK, but it goes through
  33. # this layer of Tk index indirection for every single line. For large files
  34. # without indents (and even small files with indents :-) it was pretty slow!
  35. def fast_readline(self):
  36. if self.finished:
  37. val = ""
  38. else:
  39. if "_scint_lines" not in self.__dict__:
  40. # XXX - note - assumes this is only called once the file is loaded!
  41. self._scint_lines = self.text.edit.GetTextRange().split("\n")
  42. sl = self._scint_lines
  43. i = self.i = self.i + 1
  44. if i >= len(sl):
  45. val = ""
  46. else:
  47. val = sl[i]+"\n"
  48. return val.encode(default_scintilla_encoding)
  49. try:
  50. GetIDLEModule("AutoIndent").IndentSearcher.readline = fast_readline
  51. except AttributeError: # GetIDLEModule may return None
  52. pass
  53. # A class that attempts to emulate an IDLE editor window.
  54. # Construct with a Pythonwin view.
  55. class IDLEEditorWindow:
  56. def __init__(self, edit):
  57. self.edit = edit
  58. self.text = TkText(edit)
  59. self.extensions = {}
  60. self.extension_menus = {}
  61. def close(self):
  62. self.edit = self.text = None
  63. self.extension_menus = None
  64. try:
  65. for ext in self.extensions.values():
  66. closer = getattr(ext, "close", None)
  67. if closer is not None:
  68. closer()
  69. finally:
  70. self.extensions = {}
  71. def IDLEExtension(self, extension):
  72. ext = self.extensions.get(extension)
  73. if ext is not None: return ext
  74. mod = GetIDLEModule(extension)
  75. if mod is None: return None
  76. klass = getattr(mod, extension)
  77. ext = self.extensions[extension] = klass(self)
  78. # Find and bind all the events defined in the extension.
  79. events = [item for item in dir(klass) if item[-6:]=="_event"]
  80. for event in events:
  81. name = "<<%s>>" % (event[:-6].replace("_", "-"), )
  82. self.edit.bindings.bind(name, getattr(ext, event))
  83. return ext
  84. def GetMenuItems(self, menu_name):
  85. # Get all menu items for the menu name (eg, "edit")
  86. bindings = self.edit.bindings
  87. ret = []
  88. for ext in self.extensions.values():
  89. menudefs = getattr(ext, "menudefs", [])
  90. for name, items in menudefs:
  91. if name == menu_name:
  92. for text, event in [item for item in items if item is not None]:
  93. text = text.replace("&", "&&")
  94. text = text.replace("_", "&")
  95. ret.append((text, event))
  96. return ret
  97. ######################################################################
  98. # The IDLE "Virtual UI" methods that are exposed to the IDLE extensions.
  99. #
  100. def askinteger(self, caption, prompt, parent=None, initialvalue=0, minvalue=None, maxvalue=None):
  101. while 1:
  102. rc = GetSimpleInput(prompt, str(initialvalue), caption)
  103. if rc is None: return 0 # Correct "cancel" semantics?
  104. err = None
  105. try:
  106. rc = int(rc)
  107. except ValueError:
  108. err = "Please enter an integer"
  109. if not err and minvalue is not None and rc < minvalue:
  110. err = "Please enter an integer greater then or equal to %s" % (minvalue,)
  111. if not err and maxvalue is not None and rc > maxvalue:
  112. err = "Please enter an integer less then or equal to %s" % (maxvalue,)
  113. if err:
  114. win32ui.MessageBox(err, caption, win32con.MB_OK)
  115. continue
  116. return rc
  117. def askyesno(self, caption, prompt, parent=None):
  118. return win32ui.MessageBox(prompt, caption, win32con.MB_YESNO)==win32con.IDYES
  119. ######################################################################
  120. # The IDLE "Virtual Text Widget" methods that are exposed to the IDLE extensions.
  121. #
  122. # Is character at text_index in a Python string? Return 0 for
  123. # "guaranteed no", true for anything else.
  124. def is_char_in_string(self, text_index):
  125. # A helper for the code analyser - we need internal knowledge of
  126. # the colorizer to get this information
  127. # This assumes the colorizer has got to this point!
  128. text_index = self.text._getoffset(text_index)
  129. c = self.text.edit._GetColorizer()
  130. if c and c.GetStringStyle(text_index) is None:
  131. return 0
  132. return 1
  133. # If a selection is defined in the text widget, return
  134. # (start, end) as Tkinter text indices, otherwise return
  135. # (None, None)
  136. def get_selection_indices(self):
  137. try:
  138. first = self.text.index("sel.first")
  139. last = self.text.index("sel.last")
  140. return first, last
  141. except TextError:
  142. return None, None
  143. def set_tabwidth(self, width ):
  144. self.edit.SCISetTabWidth(width)
  145. def get_tabwidth(self):
  146. return self.edit.GetTabWidth()
  147. # A class providing the generic "Call Tips" interface
  148. class CallTips:
  149. def __init__(self, edit):
  150. self.edit = edit
  151. def showtip(self, tip_text):
  152. self.edit.SCICallTipShow(tip_text)
  153. def hidetip(self):
  154. self.edit.SCICallTipCancel()
  155. ########################################
  156. #
  157. # Helpers for the TkText emulation.
  158. def TkOffsetToIndex(offset, edit):
  159. lineoff = 0
  160. # May be 1 > actual end if we pretended there was a trailing '\n'
  161. offset = min(offset, edit.GetTextLength())
  162. line = edit.LineFromChar(offset)
  163. lineIndex = edit.LineIndex(line)
  164. return "%d.%d" % (line+1, offset-lineIndex)
  165. def _NextTok(str, pos):
  166. # Returns (token, endPos)
  167. end = len(str)
  168. if pos>=end: return None, 0
  169. while pos < end and str[pos] in string.whitespace:
  170. pos = pos + 1
  171. # Special case for +-
  172. if str[pos] in '+-':
  173. return str[pos],pos+1
  174. # Digits also a special case.
  175. endPos = pos
  176. while endPos < end and str[endPos] in string.digits+".":
  177. endPos = endPos + 1
  178. if pos!=endPos: return str[pos:endPos], endPos
  179. endPos = pos
  180. while endPos < end and str[endPos] not in string.whitespace + string.digits + "+-":
  181. endPos = endPos + 1
  182. if pos!=endPos: return str[pos:endPos], endPos
  183. return None, 0
  184. def TkIndexToOffset(bm, edit, marks):
  185. base, nextTokPos = _NextTok(bm, 0)
  186. if base is None: raise ValueError("Empty bookmark ID!")
  187. if base.find(".")>0:
  188. try:
  189. line, col = base.split(".", 2)
  190. if col=="first" or col=="last":
  191. # Tag name
  192. if line != "sel": raise ValueError("Tags arent here!")
  193. sel = edit.GetSel()
  194. if sel[0]==sel[1]:
  195. raise EmptyRange
  196. if col=="first":
  197. pos = sel[0]
  198. else:
  199. pos = sel[1]
  200. else:
  201. # Lines are 1 based for tkinter
  202. line = int(line)-1
  203. if line > edit.GetLineCount():
  204. pos = edit.GetTextLength()+1
  205. else:
  206. pos = edit.LineIndex(line)
  207. if pos==-1: pos = edit.GetTextLength()
  208. pos = pos + int(col)
  209. except (ValueError, IndexError):
  210. raise ValueError("Unexpected literal in '%s'" % base)
  211. elif base == 'insert':
  212. pos = edit.GetSel()[0]
  213. elif base=='end':
  214. pos = edit.GetTextLength()
  215. # Pretend there is a trailing '\n' if necessary
  216. if pos and edit.SCIGetCharAt(pos-1) != "\n":
  217. pos = pos+1
  218. else:
  219. try:
  220. pos = marks[base]
  221. except KeyError:
  222. raise ValueError("Unsupported base offset or undefined mark '%s'" % base)
  223. while 1:
  224. word, nextTokPos = _NextTok(bm, nextTokPos)
  225. if word is None: break
  226. if word in ['+','-']:
  227. num, nextTokPos = _NextTok(bm, nextTokPos)
  228. if num is None: raise ValueError("+/- operator needs 2 args")
  229. what, nextTokPos = _NextTok(bm, nextTokPos)
  230. if what is None: raise ValueError("+/- operator needs 2 args")
  231. if what[0] != "c": raise ValueError("+/- only supports chars")
  232. if word=='+':
  233. pos = pos + int(num)
  234. else:
  235. pos = pos - int(num)
  236. elif word=='wordstart':
  237. while pos > 0 and edit.SCIGetCharAt(pos-1) in wordchars:
  238. pos = pos - 1
  239. elif word=='wordend':
  240. end = edit.GetTextLength()
  241. while pos < end and edit.SCIGetCharAt(pos) in wordchars:
  242. pos = pos + 1
  243. elif word=='linestart':
  244. while pos > 0 and edit.SCIGetCharAt(pos-1) not in '\n\r':
  245. pos = pos - 1
  246. elif word=='lineend':
  247. end = edit.GetTextLength()
  248. while pos < end and edit.SCIGetCharAt(pos) not in '\n\r':
  249. pos = pos + 1
  250. else:
  251. raise ValueError("Unsupported relative offset '%s'" % word)
  252. return max(pos, 0) # Tkinter is tollerant of -ve indexes - we aren't
  253. # A class that resembles an IDLE (ie, a Tk) text widget.
  254. # Construct with an edit object (eg, an editor view)
  255. class TkText:
  256. def __init__(self, edit):
  257. self.calltips = None
  258. self.edit = edit
  259. self.marks = {}
  260. ## def __getattr__(self, attr):
  261. ## if attr=="tk": return self # So text.tk.call works.
  262. ## if attr=="master": return None # ditto!
  263. ## raise AttributeError, attr
  264. ## def __getitem__(self, item):
  265. ## if item=="tabs":
  266. ## size = self.edit.GetTabWidth()
  267. ## if size==8: return "" # Tk default
  268. ## return size # correct semantics?
  269. ## elif item=="font": # Used for measurements we dont need to do!
  270. ## return "Dont know the font"
  271. ## raise IndexError, "Invalid index '%s'" % item
  272. def make_calltip_window(self):
  273. if self.calltips is None:
  274. self.calltips = CallTips(self.edit)
  275. return self.calltips
  276. def _getoffset(self, index):
  277. return TkIndexToOffset(index, self.edit, self.marks)
  278. def _getindex(self, off):
  279. return TkOffsetToIndex(off, self.edit)
  280. def _fix_indexes(self, start, end):
  281. # first some magic to handle skipping over utf8 extended chars.
  282. while start > 0 and ord(self.edit.SCIGetCharAt(start)) & 0xC0 == 0x80:
  283. start -= 1
  284. while end < self.edit.GetTextLength() and ord(self.edit.SCIGetCharAt(end)) & 0xC0 == 0x80:
  285. end += 1
  286. # now handling fixing \r\n->\n disparities...
  287. if start>0 and self.edit.SCIGetCharAt(start)=='\n' and self.edit.SCIGetCharAt(start-1)=='\r':
  288. start = start - 1
  289. if end < self.edit.GetTextLength() and self.edit.SCIGetCharAt(end-1)=='\r' and self.edit.SCIGetCharAt(end)=='\n':
  290. end = end + 1
  291. return start, end
  292. ## def get_tab_width(self):
  293. ## return self.edit.GetTabWidth()
  294. ## def call(self, *rest):
  295. ## # Crap to support Tk measurement hacks for tab widths
  296. ## if rest[0] != "font" or rest[1] != "measure":
  297. ## raise ValueError, "Unsupport call type"
  298. ## return len(rest[5])
  299. ## def configure(self, **kw):
  300. ## for name, val in kw.items():
  301. ## if name=="tabs":
  302. ## self.edit.SCISetTabWidth(int(val))
  303. ## else:
  304. ## raise ValueError, "Unsupported configuration item %s" % kw
  305. def bind(self, binding, handler):
  306. self.edit.bindings.bind(binding, handler)
  307. def get(self, start, end = None):
  308. try:
  309. start = self._getoffset(start)
  310. if end is None:
  311. end = start+1
  312. else:
  313. end = self._getoffset(end)
  314. except EmptyRange:
  315. return ""
  316. # Simple semantic checks to conform to the Tk text interface
  317. if end <= start: return ""
  318. max = self.edit.GetTextLength()
  319. checkEnd = 0
  320. if end > max:
  321. end = max
  322. checkEnd = 1
  323. start, end = self._fix_indexes(start, end)
  324. ret = self.edit.GetTextRange(start, end)
  325. # pretend a trailing '\n' exists if necessary.
  326. if checkEnd and (not ret or ret[-1] != '\n'): ret = ret + '\n'
  327. return ret.replace("\r", "")
  328. def index(self, spec):
  329. try:
  330. return self._getindex(self._getoffset(spec))
  331. except EmptyRange:
  332. return ""
  333. def insert(self, pos, text):
  334. try:
  335. pos = self._getoffset(pos)
  336. except EmptyRange:
  337. raise TextError("Empty range")
  338. self.edit.SetSel((pos, pos))
  339. # IDLE only deals with "\n" - we will be nicer
  340. bits = text.split('\n')
  341. self.edit.SCIAddText(bits[0])
  342. for bit in bits[1:]:
  343. self.edit.SCINewline()
  344. self.edit.SCIAddText(bit)
  345. def delete(self, start, end=None):
  346. try:
  347. start = self._getoffset(start)
  348. if end is not None: end = self._getoffset(end)
  349. except EmptyRange:
  350. raise TextError("Empty range")
  351. # If end is specified and == start, then we must delete nothing.
  352. if start==end: return
  353. # If end is not specified, delete one char
  354. if end is None:
  355. end = start+1
  356. else:
  357. # Tk says not to delete in this case, but our control would.
  358. if end<start: return
  359. if start==self.edit.GetTextLength(): return # Nothing to delete.
  360. old = self.edit.GetSel()[0] # Lose a selection
  361. # Hack for partial '\r\n' and UTF-8 char removal
  362. start, end = self._fix_indexes(start, end)
  363. self.edit.SetSel((start, end))
  364. self.edit.Clear()
  365. if old>=start and old<end:
  366. old=start
  367. elif old>=end:
  368. old = old - (end-start)
  369. self.edit.SetSel(old)
  370. def bell(self):
  371. win32api.MessageBeep()
  372. def see(self, pos):
  373. # Most commands we use in Scintilla actually force the selection
  374. # to be seen, making this unnecessary.
  375. pass
  376. def mark_set(self, name, pos):
  377. try:
  378. pos = self._getoffset(pos)
  379. except EmptyRange:
  380. raise TextError("Empty range '%s'" % pos)
  381. if name == "insert":
  382. self.edit.SetSel( pos )
  383. else:
  384. self.marks[name]=pos
  385. def tag_add(self, name, start, end):
  386. if name != "sel": raise ValueError("Only sel tag is supported")
  387. try:
  388. start = self._getoffset(start)
  389. end = self._getoffset(end)
  390. except EmptyRange:
  391. raise TextError("Empty range")
  392. self.edit.SetSel( start, end )
  393. def tag_remove(self, name, start, end):
  394. if name !="sel" or start != "1.0" or end != "end":
  395. raise ValueError("Cant remove this tag")
  396. # Turn the sel into a cursor
  397. self.edit.SetSel(self.edit.GetSel()[0])
  398. def compare(self, i1, op, i2):
  399. try:
  400. i1=self._getoffset(i1)
  401. except EmptyRange:
  402. i1 = ""
  403. try:
  404. i2=self._getoffset(i2)
  405. except EmptyRange:
  406. i2 = ""
  407. return eval("%d%s%d" % (i1,op,i2))
  408. def undo_block_start(self):
  409. self.edit.SCIBeginUndoAction()
  410. def undo_block_stop(self):
  411. self.edit.SCIEndUndoAction()
  412. ######################################################################
  413. #
  414. # Test related code.
  415. #
  416. ######################################################################
  417. def TestCheck(index, edit, expected=None):
  418. rc = TkIndexToOffset(index, edit, {})
  419. if rc != expected:
  420. print("ERROR: Index", index,", expected", expected, "but got", rc)
  421. def TestGet(fr, to, t, expected):
  422. got = t.get(fr, to)
  423. if got != expected:
  424. print("ERROR: get(%s, %s) expected %s, but got %s" % (repr(fr), repr(to), repr(expected), repr(got)))
  425. def test():
  426. import pywin.framework.editor
  427. d=pywin.framework.editor.editorTemplate.OpenDocumentFile(None)
  428. e=d.GetFirstView()
  429. t = TkText(e)
  430. e.SCIAddText("hi there how\nare you today\r\nI hope you are well")
  431. e.SetSel((4,4))
  432. skip = """
  433. TestCheck("insert", e, 4)
  434. TestCheck("insert wordstart", e, 3)
  435. TestCheck("insert wordend", e, 8)
  436. TestCheck("insert linestart", e, 0)
  437. TestCheck("insert lineend", e, 12)
  438. TestCheck("insert + 4 chars", e, 8)
  439. TestCheck("insert +4c", e, 8)
  440. TestCheck("insert - 2 chars", e, 2)
  441. TestCheck("insert -2c", e, 2)
  442. TestCheck("insert-2c", e, 2)
  443. TestCheck("insert-2 c", e, 2)
  444. TestCheck("insert- 2c", e, 2)
  445. TestCheck("1.1", e, 1)
  446. TestCheck("1.0", e, 0)
  447. TestCheck("2.0", e, 13)
  448. try:
  449. TestCheck("sel.first", e, 0)
  450. print "*** sel.first worked with an empty selection"
  451. except TextError:
  452. pass
  453. e.SetSel((4,5))
  454. TestCheck("sel.first- 2c", e, 2)
  455. TestCheck("sel.last- 2c", e, 3)
  456. """
  457. # Check EOL semantics
  458. e.SetSel((4,4))
  459. TestGet("insert lineend", "insert lineend +1c", t, "\n")
  460. e.SetSel((20, 20))
  461. TestGet("insert lineend", "insert lineend +1c", t, "\n")
  462. e.SetSel((35, 35))
  463. TestGet("insert lineend", "insert lineend +1c", t, "\n")
  464. class IDLEWrapper:
  465. def __init__(self, control):
  466. self.text = control
  467. def IDLETest(extension):
  468. import sys, os
  469. modname = "pywin.idle." + extension
  470. __import__(modname)
  471. mod=sys.modules[modname]
  472. mod.TclError = TextError
  473. klass = getattr(mod, extension)
  474. # Create a new Scintilla Window.
  475. import pywin.framework.editor
  476. d=pywin.framework.editor.editorTemplate.OpenDocumentFile(None)
  477. v=d.GetFirstView()
  478. fname=os.path.splitext(__file__)[0] + ".py"
  479. v.SCIAddText(open(fname).read())
  480. d.SetModifiedFlag(0)
  481. r=klass( IDLEWrapper( TkText(v) ) )
  482. return r
  483. if __name__=='__main__':
  484. test()