bpo-37903: IDLE: Shell sidebar with prompts (GH-22682)

The first followup will change shell indents to spaces.
More are expected.

Co-authored-by: Terry Jan Reedy <tjreedy@udel.edu>
This commit is contained in:
Tal Einat 2021-04-29 01:27:55 +03:00 committed by GitHub
parent 103d5e420d
commit 15d3861856
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 888 additions and 132 deletions

View file

@ -48,15 +48,20 @@ import warnings
from idlelib.colorizer import ColorDelegator
from idlelib.config import idleConf
from idlelib.delegator import Delegator
from idlelib import debugger
from idlelib import debugger_r
from idlelib.editor import EditorWindow, fixwordbreaks
from idlelib.filelist import FileList
from idlelib.outwin import OutputWindow
from idlelib import replace
from idlelib import rpc
from idlelib.run import idle_formatwarning, StdInputFile, StdOutputFile
from idlelib.undo import UndoDelegator
# Default for testing; defaults to True in main() for running.
use_subprocess = False
HOST = '127.0.0.1' # python execution server on localhost loopback
PORT = 0 # someday pass in host, port for remote debug capability
@ -335,34 +340,19 @@ class PyShellFileList(FileList):
class ModifiedColorDelegator(ColorDelegator):
"Extend base class: colorizer for the shell window itself"
def __init__(self):
ColorDelegator.__init__(self)
self.LoadTagDefs()
def recolorize_main(self):
self.tag_remove("TODO", "1.0", "iomark")
self.tag_add("SYNC", "1.0", "iomark")
ColorDelegator.recolorize_main(self)
def LoadTagDefs(self):
ColorDelegator.LoadTagDefs(self)
theme = idleConf.CurrentTheme()
self.tagdefs.update({
"stdin": {'background':None,'foreground':None},
"stdout": idleConf.GetHighlight(theme, "stdout"),
"stderr": idleConf.GetHighlight(theme, "stderr"),
"console": idleConf.GetHighlight(theme, "console"),
})
def removecolors(self):
# Don't remove shell color tags before "iomark"
for tag in self.tagdefs:
self.tag_remove(tag, "iomark", "end")
class ModifiedUndoDelegator(UndoDelegator):
"Extend base class: forbid insert/delete before the I/O mark"
def insert(self, index, chars, tags=None):
try:
if self.delegate.compare(index, "<", "iomark"):
@ -381,6 +371,27 @@ class ModifiedUndoDelegator(UndoDelegator):
pass
UndoDelegator.delete(self, index1, index2)
def undo_event(self, event):
# Temporarily monkey-patch the delegate's .insert() method to
# always use the "stdin" tag. This is needed for undo-ing
# deletions to preserve the "stdin" tag, because UndoDelegator
# doesn't preserve tags for deleted text.
orig_insert = self.delegate.insert
self.delegate.insert = \
lambda index, chars: orig_insert(index, chars, "stdin")
try:
super().undo_event(event)
finally:
self.delegate.insert = orig_insert
class UserInputTaggingDelegator(Delegator):
"""Delegator used to tag user input with "stdin"."""
def insert(self, index, chars, tags=None):
if tags is None:
tags = "stdin"
self.delegate.insert(index, chars, tags)
class MyRPCClient(rpc.RPCClient):
@ -832,6 +843,7 @@ class ModifiedInterpreter(InteractiveInterpreter):
class PyShell(OutputWindow):
from idlelib.squeezer import Squeezer
shell_title = "IDLE Shell " + python_version()
@ -855,9 +867,11 @@ class PyShell(OutputWindow):
]
allow_line_numbers = False
user_input_insert_tags = "stdin"
# New classes
from idlelib.history import History
from idlelib.sidebar import ShellSidebar
def __init__(self, flist=None):
if use_subprocess:
@ -871,6 +885,8 @@ class PyShell(OutputWindow):
root.withdraw()
flist = PyShellFileList(root)
self.shell_sidebar = None # initialized below
OutputWindow.__init__(self, flist, None, None)
self.usetabs = True
@ -893,9 +909,9 @@ class PyShell(OutputWindow):
if use_subprocess:
text.bind("<<view-restart>>", self.view_restart_mark)
text.bind("<<restart-shell>>", self.restart_shell)
squeezer = self.Squeezer(self)
self.squeezer = self.Squeezer(self)
text.bind("<<squeeze-current-text>>",
squeezer.squeeze_current_text_event)
self.squeeze_current_text_event)
self.save_stdout = sys.stdout
self.save_stderr = sys.stderr
@ -926,6 +942,40 @@ class PyShell(OutputWindow):
#
self.pollinterval = 50 # millisec
self.shell_sidebar = self.ShellSidebar(self)
# Insert UserInputTaggingDelegator at the top of the percolator,
# but make calls to text.insert() skip it. This causes only insert
# events generated in Tcl/Tk to go through this delegator.
self.text.insert = self.per.top.insert
self.per.insertfilter(UserInputTaggingDelegator())
def ResetFont(self):
super().ResetFont()
if self.shell_sidebar is not None:
self.shell_sidebar.update_font()
def ResetColorizer(self):
super().ResetColorizer()
theme = idleConf.CurrentTheme()
tag_colors = {
"stdin": {'background': None, 'foreground': None},
"stdout": idleConf.GetHighlight(theme, "stdout"),
"stderr": idleConf.GetHighlight(theme, "stderr"),
"console": idleConf.GetHighlight(theme, "normal"),
}
for tag, tag_colors_config in tag_colors.items():
self.text.tag_configure(tag, **tag_colors_config)
if self.shell_sidebar is not None:
self.shell_sidebar.update_colors()
def replace_event(self, event):
replace.replace(self.text, insert_tags="stdin")
return "break"
def get_standard_extension_names(self):
return idleConf.GetExtensions(shell_only=True)
@ -1166,13 +1216,30 @@ class PyShell(OutputWindow):
# the current line, less a leading prompt, less leading or
# trailing whitespace
if self.text.compare("insert", "<", "iomark linestart"):
# Check if there's a relevant stdin range -- if so, use it
# Check if there's a relevant stdin range -- if so, use it.
# Note: "stdin" blocks may include several successive statements,
# so look for "console" tags on the newline before each statement
# (and possibly on prompts).
prev = self.text.tag_prevrange("stdin", "insert")
if prev and self.text.compare("insert", "<", prev[1]):
if (
prev and
self.text.compare("insert", "<", prev[1]) and
# The following is needed to handle empty statements.
"console" not in self.text.tag_names("insert")
):
prev_cons = self.text.tag_prevrange("console", "insert")
if prev_cons and self.text.compare(prev_cons[1], ">=", prev[0]):
prev = (prev_cons[1], prev[1])
next_cons = self.text.tag_nextrange("console", "insert")
if next_cons and self.text.compare(next_cons[0], "<", prev[1]):
prev = (prev[0], self.text.index(next_cons[0] + "+1c"))
self.recall(self.text.get(prev[0], prev[1]), event)
return "break"
next = self.text.tag_nextrange("stdin", "insert")
if next and self.text.compare("insert lineend", ">=", next[0]):
next_cons = self.text.tag_nextrange("console", "insert lineend")
if next_cons and self.text.compare(next_cons[0], "<", next[1]):
next = (next[0], self.text.index(next_cons[0] + "+1c"))
self.recall(self.text.get(next[0], next[1]), event)
return "break"
# No stdin mark -- just get the current line, less any prompt
@ -1204,7 +1271,6 @@ class PyShell(OutputWindow):
self.text.see("insert")
else:
self.newline_and_indent_event(event)
self.text.tag_add("stdin", "iomark", "end-1c")
self.text.update_idletasks()
if self.reading:
self.top.quit() # Break out of recursive mainloop()
@ -1214,7 +1280,7 @@ class PyShell(OutputWindow):
def recall(self, s, event):
# remove leading and trailing empty or whitespace lines
s = re.sub(r'^\s*\n', '' , s)
s = re.sub(r'^\s*\n', '', s)
s = re.sub(r'\n\s*$', '', s)
lines = s.split('\n')
self.text.undo_block_start()
@ -1225,7 +1291,8 @@ class PyShell(OutputWindow):
if prefix.rstrip().endswith(':'):
self.newline_and_indent_event(event)
prefix = self.text.get("insert linestart", "insert")
self.text.insert("insert", lines[0].strip())
self.text.insert("insert", lines[0].strip(),
self.user_input_insert_tags)
if len(lines) > 1:
orig_base_indent = re.search(r'^([ \t]*)', lines[0]).group(0)
new_base_indent = re.search(r'^([ \t]*)', prefix).group(0)
@ -1233,24 +1300,24 @@ class PyShell(OutputWindow):
if line.startswith(orig_base_indent):
# replace orig base indentation with new indentation
line = new_base_indent + line[len(orig_base_indent):]
self.text.insert('insert', '\n'+line.rstrip())
self.text.insert('insert', '\n' + line.rstrip(),
self.user_input_insert_tags)
finally:
self.text.see("insert")
self.text.undo_block_stop()
_last_newline_re = re.compile(r"[ \t]*(\n[ \t]*)?\Z")
def runit(self):
index_before = self.text.index("end-2c")
line = self.text.get("iomark", "end-1c")
# Strip off last newline and surrounding whitespace.
# (To allow you to hit return twice to end a statement.)
i = len(line)
while i > 0 and line[i-1] in " \t":
i = i-1
if i > 0 and line[i-1] == "\n":
i = i-1
while i > 0 and line[i-1] in " \t":
i = i-1
line = line[:i]
self.interp.runsource(line)
line = self._last_newline_re.sub("", line)
input_is_complete = self.interp.runsource(line)
if not input_is_complete:
if self.text.get(index_before) == '\n':
self.text.tag_remove(self.user_input_insert_tags, index_before)
self.shell_sidebar.update_sidebar()
def open_stack_viewer(self, event=None):
if self.interp.rpcclt:
@ -1276,7 +1343,14 @@ class PyShell(OutputWindow):
def showprompt(self):
self.resetoutput()
self.console.write(self.prompt)
prompt = self.prompt
if self.sys_ps1 and prompt.endswith(self.sys_ps1):
prompt = prompt[:-len(self.sys_ps1)]
self.text.tag_add("console", "iomark-1c")
self.console.write(prompt)
self.shell_sidebar.update_sidebar()
self.text.mark_set("insert", "end-1c")
self.set_line_and_column()
self.io.reset_undo()
@ -1326,6 +1400,13 @@ class PyShell(OutputWindow):
return 'disabled'
return super().rmenu_check_paste()
def squeeze_current_text_event(self, event=None):
self.squeezer.squeeze_current_text()
self.shell_sidebar.update_sidebar()
def on_squeezed_expand(self, index, text, tags):
self.shell_sidebar.update_sidebar()
def fix_x11_paste(root):
"Make paste replace selection on x11. See issue #5124."