You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ORPA-pyOpenRPA/WPy32-3720/python-3.7.2/Lib/site-packages/idlexlib/extensions/RunSelection.py

680 lines
20 KiB

# 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=<F9>
"""
TAG_REGIONS = False # allow for selections to be tagged (EXPERIMENTAL)
TAG_REGIONS = True
if TAG_REGIONS:
config_extension_def += """
run-tagged=<Control-F9>
region-tagged-join=<Control-j>
region-tagged-split=<Control-k>
"""
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', '<<run-region>>')]
if TAG_REGIONS:
_menu.extend([
('Run Tagged Region', '<<run-tagged>>'),
('Tag/Join Region', '<<region-tagged-join>>'),
('Untag/Split Region', '<<region-tagged-split>>')])
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', '<<run-region>>')]
elif len(r[0]) == 3:
specs = [('Run Selection or Line', '<<run-region>>', 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):
# <<run-region>> 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):
# <<run-tagged>> 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"(?<![^\n]) {%i,%i}" % (offset, offset), "", src)
src = ('\n'*(firstline-1)) + src
shell = self.editwin.flist.open_shell()
self.shell_run(src, message=msg)
return firstline, lastline
else:
return None
def get_tree(self):
src = self.text.get('1.0', END)
try:
tree = ast.parse(src)
return tree
except Exception as e:
self.handle_error(e)
return None
def get_active_statements(self, first, last, tree=None):
""" Parse the AST to get the code in the selection range """
if tree is None:
tree = self.get_tree()
src = self.text.get('1.0', 'end')
ef = EndFinder(src)
sline = first
eline = last
F, L = find_sel_range(tree, sline, eline, EF=ef)
if F is not None:
fbody, findex = F
body = []
lastn = None
for n in range(findex, len(fbody)):
if fbody[n].lineno <= eline:
body.append(fbody[n])
lastn = n
mod = ast.Module()
mod.body = body
lineno = body[0].lineno
offset = body[0].col_offset
lastline = ef(get_module_endline(mod))
return mod, lineno, lastline, offset
else:
return None
#--------------------
# Shell-handling code
#--------------------
def shell_run(self, code, message=None):
""" Returns True is code is actually executed. """
shell = self.editwin.flist.open_shell()
self.focus_editor()
try:
if shell.interp.tkconsole.executing:
return False # shell is busy
except:
return False # shell is not in a valid state
if message:
console = shell.interp.tkconsole
console.text.insert('insert', '%s\n' % message)
endpos = 'insert +%ic' % (len(message)+1)
console.text.mark_set('iomark', endpos)
try:
shell.interp.runcode(code)
except AttributeError as e:
pass
self.focus_editor()
return True
def handle_error(self, e, depth=0): # Same as SubCode.py
""" Highlight the error and display it on the shell prompt """
text = self.editwin.text
try:
msg, lineno, offset = e.msg, e.lineno, e.offset
message = '\n There is an error (%s) at line %i, column %i.\n' % \
(msg, lineno, offset)
self._highlight_error(lineno, offset, depth)
except Exception as err:
msg = e
lineno = 0
offset = 0
message = '\n There is an error: %s' % e
# send error feedback to shell
shell = self.editwin.flist.open_shell()
shell.interp.tkconsole.stderr.write(message)
shell.showprompt()
self.focus_editor()
def _highlight_error(self, lineno, offset, depth): # Same as SubCode.py
text = self.text
if offset is None:
offset = 0
offset += depth # for dedented code
pos = '0.0 + %i lines + %i chars' % (lineno - 1, offset - 1)
pos = '%i.%i' % (lineno, offset-1)
text.tag_add("ERROR", pos)
if text.get(pos) != '\n':
pos += '+1c'
text.mark_set("insert", pos)
text.see(pos)
if __name__ == '__main__':
src = """print(
)
"""
src = """try:
print(123)
except:
print(456)
finally:
print(789)
"""
src = """b = [i for i in [1,2,3] if
i > 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)