# IDLEX EXTENSION ## """ ## Copyright(C) 2011 The Board of Trustees of the University of Illinois. ## All rights reserved. ## ## Developed by: Roger D. Serwy ## University of Illinois ## ## Permission is hereby granted, free of charge, to any person obtaining ## a copy of this software and associated documentation files (the ## "Software"), to deal with the Software without restriction, including ## without limitation the rights to use, copy, modify, merge, publish, ## distribute, sublicense, and/or sell copies of the Software, and to ## permit persons to whom the Software is furnished to do so, subject to ## the following conditions: ## ## + Redistributions of source code must retain the above copyright ## notice, this list of conditions and the following disclaimers. ## + Redistributions in binary form must reproduce the above copyright ## notice, this list of conditions and the following disclaimers in the ## documentation and/or other materials provided with the distribution. ## + Neither the names of Roger D. Serwy, the University of Illinois, nor ## the names of its contributors may be used to endorse or promote ## products derived from this Software without specific prior written ## permission. ## ## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS ## OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ## MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. ## IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ## ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF ## CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH ## THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE. ## ## ## ## Run Selection Extension - run parts of source code in IDLE ## ## About: ## ## This code executes the currently highlighted text ## from the editor in the shell window, or a single line. ## ## How to Use: ## ## Run Selection or Line: ## ## * To run a single statement (including indented code) ## move cursor to the line and press F9. ## ## * To run a selection of statements, select the statements ## to run and press F9 ## ## ## Run Region ## ## A Run Region behaves like a selection, but it is tagged ## with a blue background. A group of commands can be tagged ## as a Run Region and executed using Ctrl+F9. ## ## Tag/Join Run Region ## - Tags a line or selection of code as a Run Region ## - Can join separate Run Regions ## ## Untag/Split Run Region ## - Untags a line or selection of code ## - Can split a Run Region into two separate Run Regions ## ## """ config_extension_def = """ [RunSelection] enable=1 enable_shell=0 enable_editor=1 [RunSelection_cfgBindings] run-region= """ TAG_REGIONS = False # allow for selections to be tagged (EXPERIMENTAL) TAG_REGIONS = True if TAG_REGIONS: config_extension_def += """ run-tagged= region-tagged-join= region-tagged-split= """ RUNREGION_BACKGROUND = '#BBE0FF' # background color for a runregion import sys if sys.version < '3': from Tkinter import * import tkMessageBox else: from tkinter import * import tkinter.messagebox as tkMessageBox xrange = range from idlelib.EditorWindow import classifyws from idlelib.configHandler import idleConf import ast import tokenize import re jn = lambda x,y: '%i.%i' % (x,y) # join integers to text coordinates sp = lambda x: list(map(int, x.split('.'))) # convert tkinter Text coordinate to a line and column tuple def index(text, marker='insert', lineoffset=0): """ helper function to split a marker location of a text object """ line, col = sp(text.index(marker)) return (line + lineoffset, col) #--------------------- # AST helper functions #--------------------- def find_sel_range(y, sline, eline=None, first=None, last=None, EF=None): """ Returns the first and last nodes in AST for given line range. y: module in the AST sline: start line number eline: end line number first: a tuple of the first module and line last: a tuple of the last module and line EF: an optional callback for using EndFinder to find the true endline of a module. """ if EF is None: EF = lambda x: x if eline is None: eline = sline def helper(M, first=None, last=None): for n, stmt in enumerate(M): if sline <= EF(stmt.lineno): if stmt.lineno <= eline: last = (M, n) # last found statement in range if first is None: first = (M, n) # first found statement in rangef first, last = find_sel_range(stmt, sline, eline, first, last, EF) return first, last for elt in ['body', 'orelse', 'handlers', 'finalbody']: M = getattr(y, elt, None) if M is not None: first, last = helper(M, first, last) return first, last def get_module_endline(y): if not hasattr(y, 'body'): return y.lineno lastnode = y.body[-1] for elt in ['orelse', 'handlers', 'finalbody']: M = getattr(lastnode, elt, None) if M is not None: if M: lastnode = M[-1] v = get_module_endline(lastnode) if v is None: return lastnode.lineno else: return v class EndFinder(object): def __init__(self, src): self.lines = src.split('\n') self.length = len(self.lines) self.offset = 0 def _make_readline(self, offset): self.offset = offset - 1 def readline(): if self.offset < self.length: # Strip trailing whitespace to avoid issue16152 ret = self.lines[self.offset].rstrip() + '\n' self.offset += 1 else: ret = '' return ret return readline def __call__(self, lastline): endreader = self._make_readline(lastline) opener = ['(', '{', '['] closer = [')', '}', ']'] op_stack = [] lastline_offset = 1 # default value for no effective offset try: for tok in tokenize.generate_tokens(endreader): t_type, t_string, t_srow_scol, t_erow_ecol, t_line = tok if t_type == tokenize.OP: if t_string in closer: if op_stack: c = op_stack.pop() if t_string in opener: op_stack.append(t_string) if t_type == tokenize.NEWLINE and not op_stack: lastline_offset = t_erow_ecol[0] break except tokenize.TokenError: pass lastline += (lastline_offset - 1) return lastline #--------------- # The extension #---------------- class RunSelection(object): _menu = [None, ('Run Selection or Line', '<>')] if TAG_REGIONS: _menu.extend([ ('Run Tagged Region', '<>'), ('Tag/Join Region', '<>'), ('Untag/Split Region', '<>')]) menudefs = [ ('run', _menu), ] def __init__(self, editwin): self.editwin = editwin text = self.text = editwin.text text.tag_configure('RUNREGION', **{'background':RUNREGION_BACKGROUND, #'borderwidth':2, #'relief':GROOVE, }) text.tag_raise('sel') text.tag_raise('ERROR') r = editwin.rmenu_specs # See Issue1207589 patch if len(r) > 0: if len(r[0]) == 2: specs = [('Run Selection or Line', '<>')] elif len(r[0]) == 3: specs = [('Run Selection or Line', '<>', None)] for m in specs: if m not in editwin.rmenu_specs: editwin.rmenu_specs.append(m) #--------------------------------- # Event Handlers #--------------------------------- def run_region_event(self, event=None): # <> was triggered sel = self.get_sel_range() if sel: # Run the selected code. code_region = self.send_code(*sel) else: # Run the code on current line lineno, col = sp(self.text.index('insert')) code_region = self.send_code(lineno, lineno) self.text.tag_remove('sel', '1.0', END) if code_region: self.shrink_region(*code_region, tag='sel') def run_tagged_event(self, event=None): # <> was triggered lineno, col = sp(self.text.index('insert')) r = self.get_tagged_region(lineno) if r: # Run the code contained in the tagged run region. self.untag_entire_region(r[0]) self.tag_run_region(*r) self.adjust_cursor(*r) code_region = self.send_code(*r) if code_region: self.shrink_region(*code_region, tag='RUNREGION') def region_tagged_join_event(self, event=None): sel = self.get_sel_range() if sel: self.tag_run_region(*sel) self.clear_sel() else: lineno, col = sp(self.text.index('insert')) self.tag_run_region(lineno, lineno) return "break" def region_tagged_split_event(self, event=None): sel = self.get_sel_range() if sel: self.untag_run_region(*sel) self.clear_sel() else: lineno, col = sp(self.text.index('insert')) self.untag_run_region(lineno, lineno) return "break" #-------------------------- # Tag/Untag Run Region Code #-------------------------- def _tag_region(self, first, last, tag): self.text.tag_add(tag, '%i.0' % (first), '%i.0' % (last+1)) def _untag_region(self, first, last, tag): self.text.tag_remove(tag, '%i.0' % (first), '%i.0' % (last+1)) def get_tagged_region(self, line): """ Return the Region at line number. None If no region (first, last) Line numbers """ if line < 0: return None text = self.text loc = '%i.0+1c' % line p = text.tag_prevrange("RUNREGION", loc) if p: if text.compare(p[0], '<=', loc) and \ text.compare(p[1], '>=', loc): first, col = sp(p[0]) last, col = sp(p[1]) return first, last-1 return None def tag_run_region(self, first, last): """ Tag a given span of lines as a Run Region """ if first > last: first, last = last, first tree = self.get_tree() active = self.get_active_statements(first, last, tree=tree) if active: # Active statements are in the given range. # Tag this range. mod, firstline, lastline, offset = active self._tag_region(firstline, lastline, 'RUNREGION') # Check if joining regions. This may happen # if the user expanded an existing region # but the expansion contains no active code. firstregion = self.get_tagged_region(first) lastregion = self.get_tagged_region(last) if firstregion: if lastregion: # join the separated run regions r1 = self.get_active_statements(*firstregion, tree=tree) r2 = self.get_active_statements(*lastregion, tree=tree) # make sure both regions at same indentation if r1 and r2: if r1[3] == r2[3]: # make sure offsets are same firstline = firstregion[0] lastline = lastregion[0] self._tag_region(firstline, lastline, 'RUNREGION') else: #print('wrong offsets for join') msg = 'Could not join regions due to indentation mismatch.' self.show_error('Join region error', msg) pass else: # expand downward self.tag_run_region(firstregion[0], last) def untag_run_region(self, first, last): """ Untags a run region over the given range of lines. """ if first > last: first, last = last, first tag = 'RUNREGION' r1 = self.get_tagged_region(first) r2 = self.get_tagged_region(last) self._untag_region(first, last, tag) # untag the given range T = self.get_tree() # shrink the surrounding run regions if they exist firstregion = self.get_tagged_region(first-1) lastregion = self.get_tagged_region(last+2) def retag(region): if region is None: return F, L = region self._untag_region(F, L, tag) active = self.get_active_statements(F, L, tree=T) if active: mod, F, L, offset = active self._tag_region(F, L, tag) retag(firstregion) retag(lastregion) # If RUNREGION tag still exists within [first, last] # then restore prior tags t1 = self.text.tag_names('%i.0' % first) t2 = self.text.tag_names('%i.0-1c' % last) restore = False if first != last: if tag in t1 or tag in t2: #print('could not untag') restore = True else: if tag in t1: #print('could not untag2') restore = True if restore: msg = 'Could not untag because line %i is tagged.' % firstregion[0] self._untag_region(first, last, tag) retag(r1) retag(r2) self.show_error('Split region error', msg) def untag_entire_region(self, line): """ Untags an entire region containing the given line. """ r = self.get_tagged_region(line) if r: self.untag_run_region(*r) def shrink_region(self, first, last, tag): text = self.text if first == last: endsel = '%i.0 lineend' % first else: endsel = '%i.0' % (last + 1) text.tag_add(tag, '%i.0' % first, endsel) #--------------------- # Editor-handling code #--------------------- def adjust_cursor(self, start, end): """ Move the cursor in case run region shrinks """ text = self.text if text.compare('insert', '>', '%i.0' % end): text.mark_set('insert', '%i.0' % end + ' lineend') elif text.compare('insert', '<', '%i.0' % start): text.mark_set('insert','%i.0' % start) def get_sel_range(self): """ Return the first and last line containing the selection """ text = self.text sel_first = text.index('sel.first') if sel_first: firstline, firstcol = sp(sel_first) lastline, lastcol = sp(text.index('sel.last')) if lastcol == 0: lastline -= 1 return firstline, lastline else: return None def clear_sel(self): self.text.tag_remove('sel', '1.0', 'end') def focus_editor(self, ev=None): self.editwin.text.focus_set() self.editwin.top.lift() def show_error(self, title, msg): tkMessageBox.showerror(title=title, message=msg, parent = self.text) pass #------------- # Code-parsing #------------- def send_code(self, first, last): """ Sends the code contained in the selection """ text = self.text active = self.get_active_statements(first, last) if active is not None: mod, firstline, lastline, offset = active # return the code that can be executed if firstline != lastline: msg = '# Run Region [%i-%i]' % (firstline, lastline) else: msg = '# Run Region [line %i]' % firstline src = text.get('%i.0' % firstline, '%i.0' % (lastline + 1)) if offset: # dedent indented code src = re.sub(r"(? 5 ] 123 """ e = EndFinder(src) print(e(1)) root = Tk() class ignore: def __getattr__(self, name): print('ignoring %s' % name) return lambda *args, **kwargs: None class EditorWindow(ignore): text = Text(root) text.tag_raise = lambda *args, **kw: None text.insert('1.0', src) rmenu_specs = [] editwin = EditorWindow() rs = RunSelection(editwin) tree = rs.get_tree() s = rs.get_active_statements(1.0, 1.0, tree=tree) print(s)