#@+leo-ver=5-thin
#@+node:ekr.20170302123956.1: * @file ../doc/leoAttic.txt
# This is Leo's final resting place for dead code.
# New in Leo 6.7.5. The attic will contain only code retired in the present release.

#@@language python
#@@killbeautify
#@+all
#@+node:ekr.20240617085704.1: ** Permanent attic
#@+node:ekr.20230913144248.1: *3* retire g.SherlockTracer
# I am going to leave this class in the attic indefinitely.
# It might be useful as the base for other classes.
#@+node:ekr.20121128031949.12605: *4* class g.SherlockTracer
class SherlockTracer:
    """
    A stand-alone tracer class with many of Sherlock's features.

    This class should work in any environment containing the re, os and sys modules.

    The arguments in the pattern lists determine which functions get traced
    or which stats get printed. Each pattern starts with "+", "-", "+:" or
    "-:", followed by a regular expression::

    "+x"  Enables tracing (or stats) for all functions/methods whose name
          matches the regular expression x.
    "-x"  Disables tracing for functions/methods.
    "+:x" Enables tracing for all functions in the **file** whose name matches x.
    "-:x" Disables tracing for an entire file.

    Enabling and disabling depends on the order of arguments in the pattern
    list. Consider the arguments for the Rope trace::

    patterns=['+.*','+:.*',
        '-:.*\\lib\\.*','+:.*rope.*','-:.*leoGlobals.py',
        '-:.*worder.py','-:.*prefs.py','-:.*resources.py',])

    This enables tracing for everything, then disables tracing for all
    library modules, except for all rope modules. Finally, it disables the
    tracing for Rope's worder, prefs and resources modules.

    Being able to zero in on the code of interest can be a big help in
    studying other people's code. This is a non-invasive method: no tracing
    code needs to be inserted anywhere.

    Usage:

    g.SherlockTracer(patterns).run()
    """
    @others
#@+node:ekr.20121128031949.12602: *5* sherlock.__init__
def __init__(
    self,
    patterns: list[Any],
    indent: bool = True,
    show_args: bool = True,
    show_return: bool = True,
    verbose: bool = True,
) -> None:
    """SherlockTracer ctor."""
    self.bad_patterns: list[str] = []  # List of bad patterns.
    self.indent = indent  # True: indent calls and returns.
    self.contents_d: dict[str, list] = {}  # Keys are file names, values are file lines.
    self.n = 0  # The frame level on entry to run.
    self.stats: dict[str, dict] = {}  # Keys are full file names, values are dicts.
    self.patterns: list[Any] = None  # A list of regex patterns to match.
    self.pattern_stack: list[str] = []
    self.show_args = show_args  # True: show args for each function call.
    self.show_return = show_return  # True: show returns from each function.
    self.trace_lines = True  # True: trace lines in enabled functions.
    self.verbose = verbose  # True: print filename:func
    self.set_patterns(patterns)
    try:  # Don't assume g.app exists.
        from leo.core.leoQt import QtCore
        if QtCore:
            # pylint: disable=no-member
            QtCore.pyqtRemoveInputHook()
    except Exception:
        pass
#@+node:ekr.20140326100337.16844: *5* sherlock.__call__
def __call__(self, frame: Any, event: Any, arg: Any) -> Any:
    """Exists so that self.dispatch can return self."""
    return self.dispatch(frame, event, arg)
#@+node:ekr.20140326100337.16846: *5* sherlock.bad_pattern
def bad_pattern(self, pattern: Any) -> None:
    """Report a bad Sherlock pattern."""
    if pattern not in self.bad_patterns:
        self.bad_patterns.append(pattern)
        print(f"\nignoring bad pattern: {pattern}\n")
#@+node:ekr.20140326100337.16847: *5* sherlock.check_pattern
def check_pattern(self, pattern: str) -> bool:
    """Give an error and return False for an invalid pattern."""
    try:
        for prefix in ('+:', '-:', '+', '-'):
            if pattern.startswith(prefix):
                re.match(pattern[len(prefix) :], 'xyzzy')
                return True
        self.bad_pattern(pattern)
        return False
    except Exception:
        self.bad_pattern(pattern)
        return False
#@+node:ekr.20121128031949.12609: *5* sherlock.dispatch
def dispatch(self, frame: Any, event: Any, arg: Any) -> Any:
    """The dispatch method."""
    if event == 'call':
        self.do_call(frame, arg)
    elif event == 'return' and self.show_return:
        self.do_return(frame, arg)
    elif event == 'line' and self.trace_lines:
        self.do_line(frame, arg)
    # Queue the SherlockTracer instance again.
    return self
#@+node:ekr.20121128031949.12603: *5* sherlock.do_call & helper
def do_call(self, frame: Any, unused_arg: Any) -> None:
    """Trace through a function call."""
    frame1 = frame
    code = frame.f_code
    file_name = code.co_filename
    locals_ = frame.f_locals
    function_name = code.co_name
    try:
        full_name = self.get_full_name(locals_, function_name)
    except Exception:
        full_name = function_name
    if not self.is_enabled(file_name, full_name, self.patterns):
        # 2020/09/09: Don't touch, for example, __ methods.
        return
    n = 0  # The number of callers of this def.
    while frame:
        frame = frame.f_back
        n += 1
    indent = ' ' * max(0, n - self.n) if self.indent else ''
    path = f"{os.path.basename(file_name):>20}" if self.verbose else ''
    leadin = '+' if self.show_return else ''
    args_list = self.get_args(frame1)
    if self.show_args and args_list:
        args_s = ','.join(args_list)
        args_s2 = f"({args_s})"
        if len(args_s2) > 100:
            print(f"{path}:{indent}{leadin}{full_name}")
            g.printObj(args_list, indent=len(indent) + 22)
        else:
            print(f"{path}:{indent}{leadin}{full_name}{args_s2}")
    else:
        print(f"{path}:{indent}{leadin}{full_name}")
    # Always update stats.
    d = self.stats.get(file_name, {})
    d[full_name] = 1 + d.get(full_name, 0)
    self.stats[file_name] = d
#@+node:ekr.20130111185820.10194: *6* sherlock.get_args
def get_args(self, frame: Any) -> list[str]:
    """Return a list of string "name=val" for each arg in the function call."""
    code = frame.f_code
    locals_ = frame.f_locals
    name = code.co_name
    n = code.co_argcount
    if code.co_flags & 4:
        n = n + 1
    if code.co_flags & 8:
        n = n + 1
    result = []
    for i in range(n):
        name = code.co_varnames[i]
        if name != 'self':
            arg = locals_.get(name, '*undefined*')
            if arg:
                if isinstance(arg, (list, tuple)):
                    val_s = ','.join([self.show(z) for z in arg if self.show(z)])
                    val = f"[{val_s}]"
                elif isinstance(arg, str):
                    val = arg
                else:
                    val = self.show(arg)
                if val:
                    result.append(f"{name}={val}")
    return result
#@+node:ekr.20140402060647.16845: *5* sherlock.do_line (not used)
bad_fns: list[str] = []

def do_line(self, frame: Any, arg: Any) -> None:
    """print each line of enabled functions."""
    if 1:
        return
    code = frame.f_code
    file_name = code.co_filename
    locals_ = frame.f_locals
    name = code.co_name
    full_name = self.get_full_name(locals_, name)
    if not self.is_enabled(file_name, full_name, self.patterns):
        return
    n = frame.f_lineno - 1  # Apparently, the first line is line 1.
    d = self.contents_d
    lines = d.get(file_name)
    if not lines:
        print(file_name)
        try:
            with open(file_name) as f:
                s = f.read()
        except Exception:
            if file_name not in self.bad_fns:
                self.bad_fns.append(file_name)
                print(f"open({file_name}) failed")
            return
        lines = g.splitLines(s)
        d[file_name] = lines
    line = lines[n].rstrip() if n < len(lines) else '<EOF>'
    if 0:
        print(f"{name:3} {line}")
    else:
        print(f"{g.shortFileName(file_name)} {n} {full_name} {line}")
#@+node:ekr.20130109154743.10172: *5* sherlock.do_return & helper
def do_return(self, frame: Any, arg: Any) -> None:  # Arg *is* used below.
    """Trace a return statement."""
    code = frame.f_code
    fn = code.co_filename
    locals_ = frame.f_locals
    name = code.co_name
    self.full_name = self.get_full_name(locals_, name)
    if not self.is_enabled(fn, self.full_name, self.patterns):
        return
    n = 0
    while frame:
        frame = frame.f_back
        n += 1
    path = f"{os.path.basename(fn):>20}" if self.verbose else ''
    if name and name == '__init__':
        try:
            ret1 = locals_ and locals_.get('self', None)
            self.put_ret(ret1, n, path)
        except NameError:
            self.put_ret(f"<{ret1.__class__.__name__}>", n, path)
    else:
        self.put_ret(arg, n, path)
#@+node:ekr.20220605141445.1: *6* sherlock.put_ret
def put_ret(self, arg: Any, n: int, path: str) -> None:
    """Print arg, the value returned by a "return" statement."""
    indent = ' ' * max(0, n - self.n + 1) if self.indent else ''
    try:
        if isinstance(arg, types.GeneratorType):
            ret = '<generator>'
        elif isinstance(arg, (tuple, list)):
            ret_s = ','.join([self.show(z) for z in arg])
            if len(ret_s) > 40:
                g.printObj(arg, indent=len(indent))
                ret = ''
            else:
                ret = f"[{ret_s}]"
        elif arg:
            ret = self.show(arg)
            if len(ret) > 100:
                ret = f"\n    {ret}"
        else:
            ret = '' if arg is None else repr(arg)
        print(f"{path}:{indent}-{self.full_name} -> {ret}")
    except Exception:
        exctype, value = sys.exc_info()[:2]
        try:  # Be extra careful.
            arg_s = f"arg: {arg!r}"
        except Exception:
            arg_s = ''  # arg.__class__.__name__
        print(
            f"{path}:{indent}-{self.full_name} -> "
            f"{exctype.__name__}, {value} {arg_s}"
        )
#@+node:ekr.20121128111829.12185: *5* sherlock.fn_is_enabled
def fn_is_enabled(self, func: Any, patterns: list[str]) -> bool:
    """Return True if tracing for the given function is enabled."""
    if func in self.ignored_functions:
        return False

    def ignore_function() -> None:
        if func not in self.ignored_functions:
            self.ignored_functions.append(func)
            print(f"Ignore function: {func}")
    #
    # New in Leo 6.3. Never trace dangerous functions.
    table = (
        '_deepcopy.*',
        # Unicode primitives.
        'encode\b', 'decode\b',
        # System functions
        '.*__next\b',
        '<frozen>', '<genexpr>', '<listcomp>',
        # '<decorator-gen-.*>',
        'get\b',
        # String primitives.
        'append\b', 'split\b', 'join\b',
        # File primitives...
        'access_check\b', 'expanduser\b', 'exists\b', 'find_spec\b',
        'abspath\b', 'normcase\b', 'normpath\b', 'splitdrive\b',
    )
    g.trace('=====', func)
    for z in table:
        if re.match(z, func):
            ignore_function()
            return False
    #
    # Legacy code.
    try:
        enabled, pattern = False, None
        for pattern in patterns:
            if pattern.startswith('+:'):
                if re.match(pattern[2:], func):
                    enabled = True
            elif pattern.startswith('-:'):
                if re.match(pattern[2:], func):
                    enabled = False
        return enabled
    except Exception:
        self.bad_pattern(pattern)
        return False
#@+node:ekr.20130112093655.10195: *5* sherlock.get_full_name
def get_full_name(self, locals_: Any, name: str) -> str:
    """Return class_name::name if possible."""
    full_name = name
    try:
        user_self = locals_ and locals_.get('self', None)
        if user_self:
            full_name = user_self.__class__.__name__ + '::' + name
    except Exception:
        pass
    return full_name
#@+node:ekr.20121128111829.12183: *5* sherlock.is_enabled
ignored_files: list[str] = []  # List of files.
ignored_functions: list[str] = []  # List of files.

def is_enabled(
    self,
    file_name: str,
    function_name: str,
    patterns: list[str] = None,
) -> bool:
    """Return True if tracing for function_name in the given file is enabled."""
    #
    # New in Leo 6.3. Never trace through some files.
    if not os:
        return False  # Shutting down.
    base_name = os.path.basename(file_name)
    if base_name in self.ignored_files:
        return False

    def ignore_file() -> None:
        if base_name not in self.ignored_files:
            self.ignored_files.append(base_name)

    def ignore_function() -> None:
        if function_name not in self.ignored_functions:
            self.ignored_functions.append(function_name)

    if f"{os.sep}lib{os.sep}" in file_name:
        ignore_file()
        return False
    if base_name.startswith('<') and base_name.endswith('>'):
        ignore_file()
        return False
    #
    # New in Leo 6.3. Never trace dangerous functions.
    table = (
        '_deepcopy.*',
        # Unicode primitives.
        'encode\b', 'decode\b',
        # System functions
        '.*__next\b',
        '<frozen>', '<genexpr>', '<listcomp>',
        # '<decorator-gen-.*>',
        'get\b',
        # String primitives.
        'append\b', 'split\b', 'join\b',
        # File primitives...
        'access_check\b', 'expanduser\b', 'exists\b', 'find_spec\b',
        'abspath\b', 'normcase\b', 'normpath\b', 'splitdrive\b',
    )
    for z in table:
        if re.match(z, function_name):
            ignore_function()
            return False
    #
    # Legacy code.
    enabled = False
    if patterns is None:
        patterns = self.patterns
    for pattern in patterns:
        try:
            if pattern.startswith('+:'):
                if re.match(pattern[2:], file_name):
                    enabled = True
            elif pattern.startswith('-:'):
                if re.match(pattern[2:], file_name):
                    enabled = False
            elif pattern.startswith('+'):
                if re.match(pattern[1:], function_name):
                    enabled = True
            elif pattern.startswith('-'):
                if re.match(pattern[1:], function_name):
                    enabled = False
            else:
                self.bad_pattern(pattern)
        except Exception:
            self.bad_pattern(pattern)
    return enabled
#@+node:ekr.20121128111829.12182: *5* sherlock.print_stats
def print_stats(self, patterns: list[str] = None) -> None:
    """Print all accumulated statisitics."""
    print('\nSherlock statistics...')
    if not patterns:
        patterns = ['+.*', '+:.*',]
    for fn in sorted(self.stats.keys()):
        d = self.stats.get(fn)
        if self.fn_is_enabled(fn, patterns):
            result = sorted(d.keys())  # type:ignore
        else:
            result = [key for key in sorted(d.keys())  # type:ignore
                if self.is_enabled(fn, key, patterns)]
        if result:
            print('')
            fn = fn.replace('\\', '/')
            parts = fn.split('/')
            print('/'.join(parts[-2:]))
            for key in result:
                print(f"{d.get(key):4} {key}")
#@+node:ekr.20121128031949.12614: *5* sherlock.run
# Modified from pdb.Pdb.set_trace.

def run(self, frame: Any = None) -> None:
    """Trace from the given frame or the caller's frame."""
    print("SherlockTracer.run:patterns:\n%s" % '\n'.join(self.patterns))
    if frame is None:
        frame = sys._getframe().f_back
    # Compute self.n, the number of frames to ignore.
    self.n = 0
    while frame:
        frame = frame.f_back
        self.n += 1
    # Pass self to sys.settrace to give easy access to all methods.
    sys.settrace(self)
#@+node:ekr.20140322090829.16834: *5* sherlock.push & pop
def push(self, patterns: list[str]) -> None:
    """Push the old patterns and set the new."""
    self.pattern_stack.append(self.patterns)  # type:ignore
    self.set_patterns(patterns)
    print(f"SherlockTracer.push: {self.patterns}")

def pop(self) -> None:
    """Restore the pushed patterns."""
    if self.pattern_stack:
        self.patterns = self.pattern_stack.pop()  # type:ignore
        print(f"SherlockTracer.pop: {self.patterns}")
    else:
        print('SherlockTracer.pop: pattern stack underflow')
#@+node:ekr.20140326100337.16845: *5* sherlock.set_patterns
def set_patterns(self, patterns: list[str]) -> None:
    """Set the patterns in effect."""
    self.patterns = [z for z in patterns if self.check_pattern(z)]
#@+node:ekr.20140322090829.16831: *5* sherlock.show
def show(self, item: Any) -> str:
    """return the best representation of item."""
    if not item:
        return repr(item)
    if isinstance(item, dict):
        return 'dict'
    if isinstance(item, str):
        s = repr(item)
        if len(s) <= 20:
            return s
        return s[:17] + '...'
    s = repr(item)
    # A Hack for mypy:
    if s.startswith("<object object"):
        s = "_dummy"
    return s
#@+node:ekr.20121128093229.12616: *5* sherlock.stop
def stop(self) -> None:
    """Stop all tracing."""
    sys.settrace(None)
#@+node:ekr.20240617085410.1: *3* retire two sort commands
@language rest
@
XEmacs provides several commands for sorting text in a buffer.  All
operate on the contents of the region (the text between point and the
mark).  They divide the text of the region into many "sort records",
identify a "sort key" for each record, and then reorder the records
using the order determined by the sort keys.  The records are ordered so
that their keys are in alphabetical order, or, for numerical sorting, in
numerical order.  In alphabetical sorting, all upper-case letters `A'
through `Z' come before lower-case `a', in accordance with the ASCII
character sequence.

   The sort commands differ in how they divide the text into sort
records and in which part of each record they use as the sort key.
Most of the commands make each line a separate sort record, but some
commands use paragraphs or pages as sort records.  Most of the sort
commands use each entire sort record as its own sort key, but some use
only a portion of the record as the sort key.

`M-x sort-lines'
     Divide the region into lines and sort by comparing the entire text
     of a line.  A prefix argument means sort in descending order.

`M-x sort-paragraphs'
     Divide the region into paragraphs and sort by comparing the entire
     text of a paragraph (except for leading blank lines).  A prefix
     argument means sort in descending order.

`M-x sort-pages'
     Divide the region into pages and sort by comparing the entire text
     of a page (except for leading blank lines).  A prefix argument
     means sort in descending order.

`M-x sort-fields'
     Divide the region into lines and sort by comparing the contents of
     one field in each line.  Fields are defined as separated by
     whitespace, so the first run of consecutive non-whitespace
     characters in a line constitutes field 1, the second such run
     constitutes field 2, etc.

     You specify which field to sort by with a numeric argument: 1 to
     sort by field 1, etc.  A negative argument means sort in descending
     order.  Thus, minus 2 means sort by field 2 in reverse-alphabetical
     order.

`M-x sort-numeric-fields'
     Like `M-x sort-fields', except the specified field is converted to
     a number for each line and the numbers are compared.  `10' comes
     before `2' when considered as text, but after it when considered
     as a number.

`M-x sort-columns'
     Like `M-x sort-fields', except that the text within each line used
     for comparison comes from a fixed range of columns.  An explanation
     is given below.

   For example, if the buffer contains:

     On systems where clash detection (locking of files being edited) is
     implemented, XEmacs also checks the first time you modify a buffer
     whether the file has changed on disk since it was last visited or
     saved.  If it has, you are asked to confirm that you want to change
     the buffer.

then if you apply `M-x sort-lines' to the entire buffer you get:

     On systems where clash detection (locking of files being edited) is
     implemented, XEmacs also checks the first time you modify a buffer
     saved.  If it has, you are asked to confirm that you want to change
     the buffer.
     whether the file has changed on disk since it was last visited or

where the upper case `O' comes before all lower case letters.  If you
apply instead `C-u 2 M-x sort-fields' you get:

     saved.  If it has, you are asked to confirm that you want to change
     implemented, XEmacs also checks the first time you modify a buffer
     the buffer.
     On systems where clash detection (locking of files being edited) is
     whether the file has changed on disk since it was last visited or

where the sort keys were `If', `XEmacs', `buffer', `systems', and `the'.

   `M-x sort-columns' requires more explanation.  You specify the
columns by putting point at one of the columns and the mark at the other
column.  Because this means you cannot put point or the mark at the
beginning of the first line to sort, this command uses an unusual
definition of `region': all of the line point is in is considered part
of the region, and so is all of the line the mark is in.

   For example, to sort a table by information found in columns 10 to
15, you could put the mark on column 10 in the first line of the table,
and point on column 15 in the last line of the table, and then use this
command.  Or you could put the mark on column 15 in the first line and
point on column 10 in the last line.

   This can be thought of as sorting the rectangle specified by point
and the mark, except that the text on each line to the left or right of
the rectangle moves along with the text inside the rectangle.  *Note
Rectangles::.
@language python
#@+node:ekr.20150514063305.342: *4* ec.sortFields
@cmd('sort-fields')
def sortFields(self, event: LeoKeyEvent, which: str = None) -> None:
    """
    Divide the selected text into lines and sort by comparing the contents
    of one field in each line. Fields are defined as separated by
    whitespace, so the first run of consecutive non-whitespace characters
    in a line constitutes field 1, the second such run constitutes field 2,
    etc.

    You specify which field to sort by with a numeric argument: 1 to sort
    by field 1, etc. A negative argument means sort in descending order.
    Thus, minus 2 means sort by field 2 in reverse-alphabetical order.
     """
    w = self.editWidget(event)
    if not w or not self._chckSel(event):
        return
    self.beginCommand(w, undoType='sort-fields')
    s = w.getAllText()
    ins = w.getInsertPoint()
    r1, r2, r3, r4 = self.getRectanglePoints(w)
    i, junk = g.getLine(s, r1)
    junk, j = g.getLine(s, r4)
    txt = s[i:j]  # bug reported by pychecker.
    txt = txt.split('\n')
    fields = []
    fn = r'\w+'
    frx = re.compile(fn)
    for line in txt:
        f = frx.findall(line)
        if not which:
            fields.append(f[0])
        else:
            i = int(which)
            if len(f) < i:
                return
            i = i - 1
            fields.append(f[i])
    nz = sorted(zip(fields, txt))
    w.delete(i, j)
    int1 = i
    for z in nz:
        w.insert(f"{int1}.0", f"{z[1]}\n")
        int1 = int1 + 1
    w.setInsertPoint(ins)
    self.endCommand(changed=True, setLabel=True)
#@+node:ekr.20240825054616.1: *3* retire add-editor
@nosearch
#@+node:ekr.20240825123857.1: *4* Multiple body editors
``add-editor``
    Adds a new editor in the body pane and gives it the body editor focus.
``delete-editor``
    Deletes the editor with body editor focus.
``cycle-editor-focus``
    Cycles body editor focus between editors in the body text. The editor that has focus shows the content of the selected outline node; the other body editors continue to show the node contents they last had when they had the body editor focus.
#@+node:ekr.20240825065226.1: *4* dw: deleted
#@+node:ekr.20190118150859.10: *5* dw.addNewEditor
def addNewEditor(self, name: str) -> tuple[QWidget, QTextEditWrapper]:
    """Create a new body editor."""
    c, p = self.leo_c, self.leo_c.p
    body = c.frame.body
    assert isinstance(body, LeoQtBody), repr(body)
    # Step 1: create the editor.
    parent_frame = c.frame.top.leo_body_inner_frame
    widget = qt_text.LeoQTextBrowser(parent_frame, c, self)
    widget.setObjectName('richTextEdit')  # Will be changed later.
    wrapper = qt_text.QTextEditWrapper(widget, name='body', c=c)
    self.packLabel(widget)
    # Step 2: inject ivars, set bindings, etc.
    inner_frame = c.frame.top.leo_body_inner_frame  # Inject ivars *here*.
    body.injectIvars(inner_frame, name, p, wrapper)
    body.updateInjectedIvars(widget, p)
    wrapper.setAllText(p.b)
    wrapper.see(0)
    c.k.completeAllBindingsForWidget(wrapper)
    if isinstance(widget, QtWidgets.QTextEdit):
        colorizer = leoColorizer.make_colorizer(c, widget)
        colorizer.highlighter.setDocument(widget.document())
    else:
        # Scintilla only.
        body.recolorWidget(p, wrapper)
    return parent_frame, wrapper
#@+node:ekr.20110605121601.18212: *5* dw.packLabel
def packLabel(self, w: QWidget, n: int = None) -> None:
    """
    Pack w into the body frame's QVGridLayout.

    The type of w does not affect the following code. In fact, w is a
    QTextBrowser possibly packed inside a LeoLineTextWidget.
    """
    c = self.leo_c
    #
    # Reuse the grid layout in the body frame.
    grid = self.leo_body_frame.layout()
    # Pack the label and the text widget.
    label = QtWidgets.QLineEdit(None)
    label.setObjectName('editorLabel')
    label.setText(c.p.h)
    if n is None:
        n = c.frame.body.numberOfEditors
    n = max(0, n - 1)
    # mypy error: grid is a QGridLayout, not a QLayout.
    grid.addWidget(label, 0, n)  # type:ignore
    grid.addWidget(w, 1, n)  # type:ignore
    grid.setRowStretch(0, 0)  # Don't grow the label vertically.
    grid.setRowStretch(1, 1)  # Give row 1 as much as vertical room as possible.
    # Inject the ivar.
    w.leo_label = label
#@+node:ekr.20070424053629: *4* LeoBody: deleted
#@+node:ekr.20060528100747.1: *5* LeoBody.addEditor
def addEditor(self, event: LeoKeyEvent = None) -> None:
    """Add another editor to the body pane."""
    c, p = self.c, self.c.p
    self.totalNumberOfEditors += 1
    self.numberOfEditors += 1
    if self.numberOfEditors == 2:
        # Inject the ivars into the first editor.
        # Bug fix: Leo 4.4.8 rc1: The name of the last editor need not be '1'
        d = self.editorWrappers
        keys = list(d.keys())
        if len(keys) == 1:
            # Immediately create the label in the old editor.
            w_old = d.get(keys[0])
            self.updateInjectedIvars(w_old, p)
            self.selectLabel(w_old)
        else:
            g.trace('can not happen: unexpected editorWrappers', d)
    name = f"{self.totalNumberOfEditors}"
    pane = self.pb.add(name)
    panes = self.pb.panes()
    minSize = float(1.0 / float(len(panes)))
    # Create the text wrapper.
    f = self.createEditorFrame(pane)
    wrapper = self.createTextWidget(f, name=name, p=p)
    wrapper.delete(0, len(wrapper.getAllText()))
    wrapper.insert(0, p.b)
    wrapper.setInsertPoint(len(p.b))
    wrapper.see(0)
    c.k.completeAllBindingsForWidget(wrapper)
    self.recolorWidget(p, wrapper)
    self.editorWrappers[name] = wrapper
    for pane in panes:
        self.pb.configurepane(pane, size=minSize)
    self.pb.updatelayout()
    c.frame.body.wrapper = wrapper
    # Finish...
    self.updateInjectedIvars(wrapper, p)
    self.selectLabel(wrapper)
    self.selectEditor(wrapper)
    self.updateEditors()
    c.bodyWantsFocus()
#@+node:ekr.20060528132829: *5* LeoBody.assignPositionToEditor
def assignPositionToEditor(self, p: Position) -> None:
    """Called *only* from tree.select to select the present body editor."""
    c = self.c
    w = c.frame.body.widget
    self.updateInjectedIvars(w, p)
    self.selectLabel(w)
#@+node:ekr.20070422093128: *5* LeoBody.computeLabel
def computeLabel(self, w: Wrapper) -> str:
    s = w.leo_label_s
    if hasattr(w, 'leo_chapter') and w.leo_chapter:
        s = f"{w.leo_chapter.name}: {s}"
    return s
#@+node:ekr.20200415041750.1: *5* LeoBody.cycleEditorFocus
@body_cmd('editor-cycle-focus')
@body_cmd('cycle-editor-focus')
def cycleEditorFocus(self, event: LeoKeyEvent = None) -> None:
    """Cycle keyboard focus between the body text editors."""
    c = self.c
    d = self.editorWrappers
    w = c.frame.body.wrapper
    values = list(d.values())
    if len(values) > 1:
        i = values.index(w) + 1
        if i == len(values):
            i = 0
        w2 = values[i]
        assert w != w2
        self.selectEditor(w2)
        c.frame.body.wrapper = w2
#@+node:ekr.20070424080640: *5* LeoBody.deactivateActiveEditor
# Not used in Qt.

def deactivateActiveEditor(self, w: Wrapper) -> None:
    """Inactivate the previously active editor."""
    d = self.editorWrappers
    # Don't capture ivars here! assignPositionToEditor keeps them up-to-date. (??)
    for key in d:
        w2 = d.get(key)
        if w2 != w and w2.leo_active:
            w2.leo_active = False
            self.unselectLabel(w2)
            return
#@+node:ekr.20060528113806: *5* LeoBody.deleteEditor (overridden)
def deleteEditor(self, event: LeoKeyEvent = None) -> None:
    """Delete the presently selected body text editor."""
    c = self.c
    w = c.frame.body.wrapper
    d = self.editorWrappers
    if len(list(d.keys())) == 1:
        return
    name = w.leo_name
    del d[name]
    self.pb.delete(name)
    panes = self.pb.panes()
    minSize = float(1.0 / float(len(panes)))
    for pane in panes:
        self.pb.configurepane(pane, size=minSize)
    # Select another editor.
    w = list(d.values())[0]
    # c.frame.body.wrapper = w # Don't do this now?
    self.numberOfEditors -= 1
    self.selectEditor(w)
#@+node:ekr.20070425180705: *5* LeoBody.findEditorForChapter
def findEditorForChapter(self, chapter: str, p: Position) -> Any:
    """Return an editor to be assigned to chapter."""
    c = self.c
    return c.frame.body.wrapper
#@+node:ekr.20060530210057: *5* LeoBody.select/unselectLabel
def unselectLabel(self, w: Wrapper) -> None:
    self.createChapterIvar(w)

def selectLabel(self, w: Wrapper) -> None:
    pass
#@+node:ekr.20061017083312: *5* LeoBody.selectEditor & helpers
selectEditorLockout = False

def selectEditor(self, w: Wrapper) -> None:
    """Select the editor given by w and node w.leo_p."""
    #  Called whenever wrapper must be selected.
    c = self.c
    if self.selectEditorLockout:
        return
    if w and w == self.c.frame.body.widget:
        if w.leo_p and w.leo_p != c.p:
            c.selectPosition(w.leo_p)
            c.bodyWantsFocus()
        return
    try:
        self.selectEditorLockout = True
        self.selectEditorHelper(w)
    finally:
        self.selectEditorLockout = False
#@+node:ekr.20070423102603: *6* LeoBody.selectEditorHelper
def selectEditorHelper(self, wrapper: Wrapper) -> None:
    """Select the editor whose widget is given."""
    c = self.c
    if not (hasattr(wrapper, 'leo_p') and wrapper.leo_p):
        g.trace('no wrapper.leo_p')
        return
    self.deactivateActiveEditor(wrapper)
    # The actual switch.
    c.frame.body.wrapper = wrapper
    wrapper.leo_active = True
    self.switchToChapter(wrapper)
    self.selectLabel(wrapper)
    if not self.ensurePositionExists(wrapper):
        g.trace('***** no position editor!')
        return
    p = wrapper.leo_p
    c.redraw(p)
    c.recolor()
    c.bodyWantsFocus()
#@+node:ekr.20060528131618: *5* LeoBody.updateEditors
# Called from addEditor and assignPositionToEditor

def updateEditors(self) -> None:
    c, p = self.c, self.c.p
    d = self.editorWrappers
    if len(list(d.keys())) < 2:
        return  # There is only the main widget.
    for key in d:
        wrapper = d.get(key)
        v = wrapper.leo_v
        if v and v == p.v and wrapper != c.frame.body.wrapper:
            wrapper.delete(0, len(wrapper.getAllText()))
            wrapper.insert(0, p.b)
            wrapper.setInsertPoint(len(p.b))
            self.recolorWidget(p, wrapper)
    c.bodyWantsFocus()
#@+node:ekr.20070424092855: *5* LeoBody.updateInjectedIvars
# Called from addEditor and assignPositionToEditor.

def updateInjectedIvars(self, w: Wrapper, p: Position) -> None:
    """Inject updated ivars in w, a gui widget."""
    if not w:
        return
    c = self.c
    cc = c.chapterController
    # Was in ctor.
    use_chapters = c.config.getBool('use-chapters')
    if cc and use_chapters:
        w.leo_chapter = cc.getSelectedChapter()
    else:
        w.leo_chapter = None
    w.leo_p = p.copy()
    w.leo_v = w.leo_p.v
    w.leo_label_s = p.h
#@+node:ekr.20240825055124.1: *4* LeoQtBody: deleted
# From LeoQtBody ctor.
    self.canvasRenderer: QtWidgets.QGraphicsView = None
    self.canvasRendererLabel: QtWidgets.QLineEdit = None
    self.canvasRendererVisible = False
    self.textRenderer: QtWidgets.QFrame = None
    self.textRendererLabel: QtWidgets.QLineEdit = None
    self.textRendererVisible = False
    self.textRendererWrapper: Wrapper = None
#@+node:ekr.20110605121601.18194: *5* LeoQtBody.entries
#@+node:ekr.20110605121601.18195: *6* LeoQtBody.add_editor_command
# An override of leoFrame.addEditor.

@body_cmd('editor-add')
@body_cmd('add-editor')
def add_editor_command(self, event: LeoKeyEvent = None) -> None:
    """Add another editor to the body pane."""
    c, p = self.c, self.c.p
    d = self.editorWrappers
    dw = c.frame.top
    wrapper = c.frame.body.wrapper  # A QTextEditWrapper
    widget = wrapper.widget
    self.totalNumberOfEditors += 1
    self.numberOfEditors += 1
    if self.totalNumberOfEditors == 2:
        d['1'] = wrapper
        # Pack the original body editor.
        # Fix #1021: Pack differently depending on whether the gutter exists.
        if self.use_gutter:
            dw.packLabel(widget.parent(), n=1)
            widget.leo_label = widget.parent().leo_label
        else:
            dw.packLabel(widget, n=1)
    name = f"{self.totalNumberOfEditors}"
    f, wrapper = dw.addNewEditor(name)
    assert g.isTextWrapper(wrapper), wrapper
    assert g.isTextWidget(widget), widget
    assert isinstance(f, QtWidgets.QFrame), f
    d[name] = wrapper
    if self.numberOfEditors == 2:
        # Inject the ivars into the first editor.
        # The name of the last editor need not be '1'
        keys = list(d.keys())
        old_name = keys[0]
        old_wrapper = d.get(old_name)
        old_w = old_wrapper.widget
        self.injectIvars(f, old_name, p, old_wrapper)
        self.updateInjectedIvars(old_w, p)
        # Immediately create the label in the old editor.
        self.selectLabel(old_wrapper)
    # Switch editors.
    c.frame.body.wrapper = wrapper
    self.selectLabel(wrapper)
    self.selectEditor(wrapper)
    self.updateEditors()
    c.bodyWantsFocus()
#@+node:ekr.20110605121601.18197: *6* LeoQtBody.assignPositionToEditor
def assignPositionToEditor(self, p: Position) -> None:
    """Called *only* from tree.select to select the present body editor."""
    c = self.c
    wrapper = c.frame.body.wrapper
    w = wrapper and wrapper.widget
    if w:  # Careful: w may not exist during unit testing.
        self.updateInjectedIvars(w, p)
        self.selectLabel(wrapper)
#@+node:ekr.20110605121601.18198: *6* LeoQtBody.cycleEditorFocus
# Use the base class method.
#@+node:ekr.20110605121601.18199: *6* LeoQtBody.delete_editor_command
@body_cmd('delete-editor')
@body_cmd('editor-delete')
def delete_editor_command(self, event: LeoKeyEvent = None) -> None:
    """Delete the presently selected body text editor."""
    c, d = self.c, self.editorWrappers
    wrapper = c.frame.body.wrapper
    w = wrapper.widget
    assert g.isTextWrapper(wrapper), wrapper
    assert g.isTextWidget(w), w
    # Fix bug 228: make *sure* the old text is saved.
    c.p.b = wrapper.getAllText()
    name = getattr(w, 'leo_name', None)
    if len(list(d.keys())) <= 1 or name == '1':
        g.warning('can not delete main editor')
        return
    #
    # Actually delete the widget.
    del d[name]
    f = c.frame.top.leo_body_frame
    layout = f.layout()
    for z in (w, w.leo_label):
        if z:
            self.unpackWidget(layout, z)
    #
    # Select another editor.
    new_wrapper = list(d.values())[0]
    self.numberOfEditors -= 1
    if self.numberOfEditors == 1:
        w = new_wrapper.widget
        label = getattr(w, 'leo_label', None)
        if label:
            self.unpackWidget(layout, label)
    w.leo_label = None
    self.selectEditor(new_wrapper)
#@+node:ekr.20110605121601.18200: *6* LeoQtBody.findEditorForChapter
def findEditorForChapter(self, chapter: Any, p: Position) -> None:
    """Return an editor to be assigned to chapter."""
    c, d = self.c, self.editorWrappers
    values = list(d.values())
    # First, try to match both the chapter and position.
    if p:
        for w in values:
            if (
                hasattr(w, 'leo_chapter') and w.leo_chapter == chapter and
                hasattr(w, 'leo_p') and w.leo_p and w.leo_p == p
            ):
                return w
    # Next, try to match just the chapter.
    for w in values:
        if hasattr(w, 'leo_chapter') and w.leo_chapter == chapter:
            return w
    # As a last resort, return the present editor widget.
    return c.frame.body.wrapper
#@+node:ekr.20110605121601.18201: *6* LeoQtBody.select/unselectLabel
def unselectLabel(self, wrapper: Wrapper) -> None:
    pass

def selectLabel(self, wrapper: Wrapper) -> None:
    c = self.c
    w = wrapper.widget
    label = getattr(w, 'leo_label', None)
    if label:
        label.setEnabled(True)
        label.setText(c.p.h)
        label.setEnabled(False)
#@+node:ekr.20110605121601.18202: *6* LeoQtBody.selectEditor & helpers
selectEditorLockout = False

def selectEditor(self, wrapper: Wrapper) -> None:
    """Select editor w and node w.leo_p."""
    trace = 'select' in g.app.debug and not g.unitTesting
    tag = 'qt_body.selectEditor'
    c = self.c
    if not wrapper:
        return
    if self.selectEditorLockout:
        return
    w = wrapper.widget
    assert g.isTextWrapper(wrapper), wrapper
    assert g.isTextWidget(w), w
    if trace:
        print(f"{tag:>30}: {wrapper} {c.p.h}")
    if wrapper and wrapper == c.frame.body.wrapper:
        self.deactivateEditors(wrapper)
        if hasattr(w, 'leo_p') and w.leo_p and w.leo_p != c.p:
            c.selectPosition(w.leo_p)
            c.bodyWantsFocus()
        return
    try:
        self.selectEditorLockout = True
        self.selectEditorHelper(wrapper)
    finally:
        self.selectEditorLockout = False
#@+node:ekr.20110605121601.18203: *7* LeoQtBody.selectEditorHelper
def selectEditorHelper(self, wrapper: Wrapper) -> None:
    c = self.c
    w = wrapper.widget
    assert g.isTextWrapper(wrapper), wrapper
    assert g.isTextWidget(w), w
    if not w.leo_p:
        g.trace('no w.leo_p')
        return
    # The actual switch.
    self.deactivateEditors(wrapper)
    self.recolorWidget(w.leo_p, wrapper)  # switches colorizers.
    c.frame.body.wrapper = wrapper
    # 2014/09/04: Must set both wrapper.widget and body.widget.
    c.frame.body.wrapper.widget = w
    c.frame.body.widget = w
    w.leo_active = True
    self.switchToChapter(wrapper)
    self.selectLabel(wrapper)
    if not self.ensurePositionExists(w):
        g.trace('***** no position editor!')
        return
    if not (hasattr(w, 'leo_p') and w.leo_p):
        g.trace('***** no w.leo_p', w)
        return
    p = w.leo_p
    assert p, p
    c.expandAllAncestors(p)
    # Calls assignPositionToEditor.
    # Calls p.v.restoreCursorAndScroll.
    c.selectPosition(p)
    c.redraw()
    c.recolor()
    c.bodyWantsFocus()
#@+node:ekr.20110605121601.18205: *6* LeoQtBody.updateEditors
# Called from addEditor and assignPositionToEditor

def updateEditors(self) -> None:
    c, p = self.c, self.c.p
    body = p.b
    d = self.editorWrappers
    if len(list(d.keys())) < 2:
        return  # There is only the main widget
    w0 = c.frame.body.wrapper
    i, j = w0.getSelectionRange()
    ins = w0.getInsertPoint()
    sb0 = w0.widget.verticalScrollBar()
    pos0 = sb0.sliderPosition()
    for key in d:
        wrapper = d.get(key)
        w = wrapper.widget
        v = hasattr(w, 'leo_p') and w.leo_p.v
        if v and v == p.v and w != w0:
            sb = w.verticalScrollBar()
            pos = sb.sliderPosition()
            wrapper.setAllText(body)
            self.recolorWidget(p, wrapper)
            sb.setSliderPosition(pos)
    c.bodyWantsFocus()
    w0.setSelectionRange(i, j, insert=ins)
    sb0.setSliderPosition(pos0)
#@+node:ekr.20110605121601.18217: *5* LeoQtBody.Renderer panes
#@+node:ekr.20110605121601.18218: *6* LeoQtBody.hideCanvasRenderer
def hideCanvasRenderer(self, event: LeoKeyEvent = None) -> None:
    """Hide canvas pane."""
    c, d = self.c, self.editorWrappers
    wrapper = c.frame.body.wrapper
    w = wrapper.widget
    name = w.leo_name
    assert name
    assert wrapper == d.get(name), 'wrong wrapper'
    assert g.isTextWrapper(wrapper), wrapper
    assert g.isTextWidget(w), w
    if len(list(d.keys())) <= 1:
        return
    #
    # At present, can not delete the first column.
    if name == '1':
        g.warning('can not delete leftmost editor')
        return
    #
    # Actually delete the widget.
    del d[name]
    f = c.frame.top.leo_body_inner_frame
    layout = f.layout()
    for z in (w, w.leo_label):
        if z:
            self.unpackWidget(layout, z)
    #
    # Select another editor.
    w.leo_label = None
    new_wrapper = list(d.values())[0]
    self.numberOfEditors -= 1
    if self.numberOfEditors == 1:
        w = new_wrapper.widget
        if w.leo_label:  # 2011/11/12
            self.unpackWidget(layout, w.leo_label)
            w.leo_label = None  # 2011/11/12
    self.selectEditor(new_wrapper)
#@+node:ekr.20110605121601.18219: *6* LeoQtBody.hideTextRenderer
def hideCanvas(self, event: LeoKeyEvent = None) -> None:
    """Hide canvas pane."""
    c, d = self.c, self.editorWrappers
    wrapper = c.frame.body.wrapper
    w = wrapper.widget
    name = w.leo_name
    assert name
    assert wrapper == d.get(name), 'wrong wrapper'
    assert g.isTextWrapper(wrapper), wrapper
    assert g.isTextWidget(w), w
    if len(list(d.keys())) <= 1:
        return
    # At present, can not delete the first column.
    if name == '1':
        g.warning('can not delete leftmost editor')
        return
    #
    # Actually delete the widget.
    del d[name]
    f = c.frame.top.leo_body_inner_frame
    layout = f.layout()
    for z in (w, w.leo_label):
        if z:
            self.unpackWidget(layout, z)
    #
    # Select another editor.
    w.leo_label = None
    new_wrapper = list(d.values())[0]
    self.numberOfEditors -= 1
    if self.numberOfEditors == 1:
        w = new_wrapper.widget
        if w.leo_label:
            self.unpackWidget(layout, w.leo_label)
            w.leo_label = None
    self.selectEditor(new_wrapper)
#@+node:ekr.20110605121601.18220: *6* LeoQtBody.packRenderer
def packRenderer(self, f: QWidget, name: str, w: QtWidgets.QFrame) -> QtWidgets.QLineEdit:
    n = max(1, self.numberOfEditors)
    assert isinstance(f, QtWidgets.QFrame), f
    layout = f.layout()
    f.setObjectName(f"{name} Frame")
    # Create the text: to do: use stylesheet to set font, height.
    lab = QtWidgets.QLineEdit(f)
    lab.setObjectName(f"{name} Label")
    lab.setText(name)
    # Pack the label and the widget.
    layout.addWidget(lab, 0, max(0, n - 1), AlignmentFlag.AlignVCenter)  # type:ignore
    layout.addWidget(w, 1, max(0, n - 1))  # type:ignore
    layout.setRowStretch(0, 0)
    layout.setRowStretch(1, 1)  # Give row 1 as much as possible.
    return lab
#@+node:ekr.20110605121601.18221: *6* LeoQtBody.showCanvasRenderer
# An override of leoFrame.addEditor.

def showCanvasRenderer(self, event: LeoKeyEvent = None) -> None:
    """Show the canvas area in the body pane, creating it if necessary."""
    c = self.c
    f = c.frame.top.leo_body_inner_frame
    assert isinstance(f, QtWidgets.QFrame), f
    if not self.canvasRenderer:
        name = 'Graphics Renderer'
        self.canvasRenderer = w = QtWidgets.QGraphicsView(f)
        w.setObjectName(name)
    if not self.canvasRendererVisible:
        self.canvasRendererLabel = self.packRenderer(f, name, w)
        self.canvasRendererVisible = True
#@+node:ekr.20110605121601.18222: *6* LeoQtBody.showTextRenderer
# An override of leoFrame.addEditor.

def showTextRenderer(self, event: LeoKeyEvent = None) -> None:
    """Show the canvas area in the body pane, creating it if necessary."""
    c = self.c
    f = c.frame.top.leo_body_inner_frame
    name = 'Text Renderer'
    w = self.textRenderer
    assert isinstance(f, QtWidgets.QFrame), f
    if w:
        self.textRenderer = qt_text.LeoQTextBrowser(f, c, self)
        w = self.textRenderer
        w.setObjectName(name)
        self.textRendererWrapper = qt_text.QTextEditWrapper(w, name='text-renderer', c=c)
    if not self.textRendererVisible:
        self.textRendererLabel = self.packRenderer(f, name, w)
        self.textRendererVisible = True
#@+node:ekr.20110605121601.18206: *5* LeoQtBody.utils
#@+node:ekr.20110605121601.18207: *6* LeoQtBody.computeLabel
def computeLabel(self, w: Wrapper) -> str:
    if hasattr(w, 'leo_label') and w.leo_label:  # 2011/11/12
        s = w.leo_label.text()
    else:
        s = ''
    if hasattr(w, 'leo_chapter') and w.leo_chapter:
        s = f"{w.leo_chapter}: {s}"
    return s
#@+node:ekr.20110605121601.18208: *6* LeoQtBody.createChapterIvar
def createChapterIvar(self, w: Wrapper) -> None:
    c = self.c
    cc = c.chapterController
    if hasattr(w, 'leo_chapter') and w.leo_chapter:
        pass
    elif cc and self.use_chapters:
        w.leo_chapter = cc.getSelectedChapter()
    else:
        w.leo_chapter = None
#@+node:ekr.20110605121601.18209: *6* LeoQtBody.deactivateEditors
def deactivateEditors(self, wrapper: Wrapper) -> None:
    """Deactivate all editors except wrapper's editor."""
    d = self.editorWrappers
    # Don't capture ivars here! assignPositionToEditor keeps them up-to-date. (??)
    for key in d:
        wrapper2 = d.get(key)
        w2 = wrapper2.widget
        if hasattr(w2, 'leo_active'):
            active = w2.leo_active
        else:
            active = True
        if wrapper2 != wrapper and active:
            w2.leo_active = False
            self.unselectLabel(wrapper2)
            self.onFocusOut(w2)
#@+node:ekr.20110605121601.18210: *6* LeoQtBody.ensurePositionExists
def ensurePositionExists(self, w: Wrapper) -> bool:
    """Return True if w.leo_p exists or can be reconstituted."""
    c = self.c
    if c.positionExists(w.leo_p):
        return True
    for p2 in c.all_unique_positions():
        if p2.v and p2.v == w.leo_p.v:
            w.leo_p = p2.copy()
            return True
    # This *can* happen when selecting a deleted node.
    w.leo_p = c.p.copy()
    return False
#@+node:ekr.20110605121601.18211: *6* LeoQtBody.injectIvars
def injectIvars(self, parentFrame: QWidget, name: str, p: Position, wrapper: Wrapper) -> None:

    trace = g.app.debug == 'select' and not g.unitTesting
    tag = 'qt_body.injectIvars'
    w = wrapper.widget
    assert g.isTextWrapper(wrapper), wrapper
    assert g.isTextWidget(w), w
    if trace:
        print(f"{tag:>30}: {wrapper!r} {g.callers(1)}")
    # Inject ivars
    if name == '1':
        w.leo_p = None  # Will be set when the second editor is created.
    else:
        w.leo_p = p and p.copy()
    w.leo_active = True
    w.leo_bodyBar = None
    w.leo_bodyXBar = None
    w.leo_chapter = None
    # w.leo_colorizer injected by JEditColorizer ctor.
    # w.leo_label injected by packLabel.
    w.leo_frame = parentFrame
    w.leo_name = name
    w.leo_wrapper = wrapper
#@+node:ekr.20110605121601.18213: *6* LeoQtBody.recolorWidget (QScintilla only)
def recolorWidget(self, p: Position, wrapper: Wrapper) -> None:
    """Support QScintillaColorizer.colorize."""
    c = self.c
    colorizer = c.frame.body.colorizer
    if p and colorizer and hasattr(colorizer, 'colorize'):
        g.trace('=====', hasattr(colorizer, 'colorize'), p.h, g.callers())
        old_wrapper = c.frame.body.wrapper
        c.frame.body.wrapper = wrapper
        try:
            colorizer.colorize(p)
        finally:
            # Restore.
            c.frame.body.wrapper = old_wrapper
#@+node:ekr.20110605121601.18214: *6* LeoQtBody.switchToChapter
def switchToChapter(self, w: Wrapper) -> None:
    """select w.leo_chapter."""
    c = self.c
    cc = c.chapterController
    if hasattr(w, 'leo_chapter') and w.leo_chapter:
        chapter = w.leo_chapter
        name = chapter and chapter.name
        oldChapter = cc.getSelectedChapter()
        if chapter != oldChapter:
            cc.selectChapterByName(name)
            c.bodyWantsFocus()
#@+node:ekr.20110605121601.18216: *6* LeoQtBody.unpackWidget
def unpackWidget(self, layout: QLayout, w: Wrapper) -> None:

    index = layout.indexOf(w)
    if index == -1:
        return
    item = layout.itemAt(index)
    if item:
        item.setGeometry(QtCore.QRect(0, 0, 0, 0))
        layout.removeItem(item)
#@+node:ekr.20110605121601.18215: *6* LeoQtBody.updateInjectedIvars
def updateInjectedIvars(self, w: Any, p: Position) -> None:

    c = self.c
    cc = c.chapterController
    assert g.isTextWidget(w), w
    if cc and self.use_chapters:
        w.leo_chapter = cc.getSelectedChapter()
    else:
        w.leo_chapter = None
    w.leo_p = p.copy()
#@+node:ekr.20240830090655.1: *3* retire reload-outline
#@+node:ekr.20240810080628.1: *4* LeoQtFrame.reloadOutline
@frame_cmd('reload-outline')
def reloadOutline(self, event: LeoKeyEvent = None) -> None:
    """reload-outline: Close the outline and reload it."""
    c = self.c

    # Commit any open edits.
    c.endEditing()

    # Make sure the file has a name.
    if not c.fileName():
        c.save()
    if not c.fileName():
        g.es_print('Please name the outline', color='red')
        return

    # Abort the reload if the user vetos closing this outline.
    if c.changed:
        veto = False
        try:
            c.promptingForClose = True
            veto = c.frame.promptForSave()
        finally:
            c.promptingForClose = False
        if veto:
            g.es_print('Cancelling reload-outline command')
            return
        # Save the file.
        c.save()
        g.app.recentFilesManager.writeRecentFilesFile(c)

    # Remember old_index, the outline's position in the QTabbledWidget.
    dw = c.frame.top
    stacked_widget = dw.parent()
    tab_widget = dw.leo_master
    stacked_layout = None
    for w in stacked_widget.children():
        if isinstance(w, QtWidgets.QStackedLayout):
            stacked_layout = w
            break
    else:
        g.trace('Can not happen: no QStackedLayout')
        return

    # Remember the old values.
    old_index = stacked_layout.indexOf(dw)
    tab_names = [tab_widget.tabText(i) for i in range(tab_widget.count())]

    # Completely close the outline.
    g.doHook("close-frame", c=c)
    frame = c.frame
    if frame in g.app.windowList:
        g.app.destroyWindow(frame)
        g.app.windowList.remove(frame)
    else:
        g.app.forgetOpenFile(fn=c.fileName())  # #69.

    # Open the new outline.
    g.openWithFileName(fileName=c.fileName())

    # Do nothing more if the index has not changed.
    new_index = stacked_layout.indexOf(dw)
    if new_index == old_index:
        return

    # Put dw in the proper place.
    stacked_layout.removeWidget(dw)
    stacked_layout.insertWidget(old_index, dw)

    # Fix all tab names.
    for i, name in enumerate(tab_names):
        tab_widget.setTabText(i, name)

    # Select the proper tab.
    tab_widget.setCurrentIndex(old_index)
#@+node:ekr.20240921134306.1: *3* retire minibuffer-based spell commands
# These have probably never worked.
#@+node:ekr.20150514063305.485: *4* commands...(SpellCommandsClass)
#@+node:ekr.20171205043931.1: *5* add
@cmd('spell-add')
def add(self, event: LeoKeyEvent = None) -> None:
    """
    Simulate pressing the 'add' button in the Spell tab.

    Just open the Spell tab if it has never been opened.
    For minibuffer commands, we must also force the Spell tab to be visible.
    """
    # self.handler is a SpellTabHandler object (inited by openSpellTab)
    if self.handler:
        self.openSpellTab()
        self.handler.add()
    else:
        self.openSpellTab()
#@+node:ekr.20150514063305.486: *5* spell-find
@cmd('spell-find')
def find(self, event: LeoKeyEvent = None) -> None:
    """
    Simulate pressing the 'Find' button in the Spell tab.

    Just open the Spell tab if it has never been opened.
    For minibuffer commands, we must also force the Spell tab to be visible.
    """
    # self.handler is a SpellTabHandler object (inited by openSpellTab)
    if self.handler:
        self.openSpellTab()
        self.handler.find()
    else:
        self.openSpellTab()
#@+node:ekr.20150514063305.487: *5* change
@cmd('spell-change')
def change(self, event: LeoKeyEvent = None) -> None:
    """Simulate pressing the 'Change' button in the Spell tab."""
    if self.handler:
        self.openSpellTab()
        self.handler.change()
    else:
        self.openSpellTab()
#@+node:ekr.20150514063305.488: *5* changeThenFind
@cmd('spell-change-then-find')
def changeThenFind(self, event: LeoKeyEvent = None) -> None:
    """Simulate pressing the 'Change, Find' button in the Spell tab."""
    if self.handler:
        self.openSpellTab()
        f = self.handler.changeThenFind
        f()
    else:
        self.openSpellTab()
#@+node:ekr.20150514063305.489: *5* hide
@cmd('spell-tab-hide')
def hide(self, event: LeoKeyEvent = None) -> None:
    """Hide the Spell tab."""
    if self.handler:
        self.c.frame.log.selectTab('Log')
        self.c.bodyWantsFocus()
#@+node:ekr.20150514063305.490: *5* ignore
@cmd('spell-ignore')
def ignore(self, event: LeoKeyEvent = None) -> None:
    """Simulate pressing the 'Ignore' button in the Spell tab."""
    if self.handler:
        self.openSpellTab()
        self.handler.ignore()
    else:
        self.openSpellTab()
#@+node:ekr.20150514063305.491: *5* focusToSpell
@cmd('focus-to-spell-tab')
def focusToSpell(self, event: LeoKeyEvent = None) -> None:
    """Put focus in the spell tab."""
    self.openSpellTab()  # Makes Spell tab visible.
    # This is not a great idea. There is no indication of focus.
        # if self.handler and self.handler.tab:
            # self.handler.tab.setFocus()
#@+node:ekr.20241029111205.1: *3* retire old testing code
#@+node:ekr.20241029111221.1: *4* at-command test-one
### This code no longer works, possibly because 
### site_customize.py *MUST NOT* add the leo-editor directory to sys.path

g.cls()
if c.isChanged():
    print('Saved!')
    c.save()
<< test-one: test kind >>
<< prefixes >>
<< old commands >>
assert kind in ('check-all', 'check-one', 'beautify', 'test'), repr(kind)
if kind == 'check-all':  # Check all files.
    command = 'python -m leo.scripts.check_leo'
    g.execute_shell_commands(command)
elif kind == 'check-one':
    # path = 'plugins/qt_gui.py'
    path = 'core/leoApp.py'
    command = f"python -m leo.scripts.check_leo {path}"
    g.execute_shell_commands(command)
elif kind == 'beautify':
    << test the beautifier >>
elif kind == 'test':
    commands = (
        f"{importers}.TestJupytext.test_small_file",
    )
    verbose_flag = ''  # -v
    commands_s = f"python -m unittest {verbose_flag} {' '.join(commands)}"
    g.execute_shell_commands(commands_s, trace=True)
else:
    # command = 'run'  # run Nim tests.
    script = 'build-leo.py'
    command = fr"python C:\Repos\leo-editor\leo\scripts\{script}"
    g.execute_shell_commands(command)
#@+node:ekr.20241029111221.2: *5* << test-one: test kind >>
# Last one wins.
kind = 'beautify'
kind = 'test'  # Run unit test.
#@+node:ekr.20241029111221.3: *5* << test the beautifier >>
# Can be run from the command line as follows:
# python -c "import leo.core.leoTokens" --all --report --write leo/core/leoAst.py
args = '--all --report --write'  # --beautified --diff --write'
for command in (
    # f'python -c "import leo.core.leoTokens" {args} leoAst.py',
    f'python -c "import leo.core.leoTokens" {args} leo/core',
    'echo done!',
):
    print(command)
    g.execute_shell_commands(command)
#@+node:ekr.20241029111221.4: *5* << old commands >>
# 'python c:/test/mypy_test.py',
# 'python -m mypy c:/test/mypy_test.py',
# 'python -m ruff check c:/test/mypy_test.py',
# 'python -m pyflakes c:/test/mypy_test.py',

### "leo.scripts.check_leo.TestCheckLeo",
# f"{misc}.test_check_leo.TestCheckLeo",
# f"{core}.test_leoserver.TestLeoServer.test_find_commands",
# f"{core}.test_leoFind.TestFind.test_find_var",
# f"{core}.test_leoTokens.TestTokenBasedOrange",
# f"{core}.test_leoTokens.TestTokens",
# f"{core}.test_leoTokens.TestTokenBasedOrange.test_blank_lines_after_function",
# f"{core}.test_leoAtFile.TestAtFile.test_putBody_unterminated_at_doc_part",
#@+node:ekr.20241029111221.5: *5* << prefixes >>
commands = 'leo.unittests.commands'
core = 'leo.unittests.core'
gui = 'leo.unittests.test_gui'
importers = 'leo.unittests.plugins.test_importers'
misc = 'leo.unittests.misc_tests'
plugins = 'leo.unittests.plugins'
syntax = 'leo.unittests.plugins.test_syntax'
writers = 'leo.unittests.plugins.test_writers'

# f"{core}.test_leoGlobals",
# f"{core}.test_leoTokens",

# f"{misc}.test_design",
# f"{misc}.test_doctests",
# f"{misc}.test_syntax",

# f"{plugins}.test_gui",
# f"{plugins}.test_importers",
# f"{plugins}.test_plugins",
# f"{plugins}.test_writers",
#@+node:ekr.20210905170507.9: *4* TestColorizer.test_colorizer_CWEB
def test_colorizer_CWEB(self):
    text = self.prep(
        r"""\\\
        % This is limbo in cweb mode... It should be in \LaTeX mode, not \c mode.
        % The following should not be colorized: class,if,else.

        @* this is a _cweb_ comment.  Code is written in \c.
        "strings" should not be colorized.
        It should be colored in \LaTeX mode.
        The following are not keywords in latex mode: if, else, etc.
        Noweb section references are _valid_ in cweb comments!
        < < section ref > >
        < < missing ref > >
        @c

        and this is C code. // It is colored in \LaTeX mode by default.
        /* This is a C block comment.  It may also be colored in restricted \LaTeX mode. */

        // Section refs are valid in code too, of course.
        < < section ref > >
        < < missing ref > >

        \LaTeX and \c should not be colored.
        if else, while, do // C keywords.
""")
    self.color('cweb', text)
#@+node:ekr.20210905170507.28: *4* TestColorizer.test_colorizer_rapidq
def test_colorizer_rapidq(self):
    text = self.prep(
    """
        ' New in 4.2.
        ' a comment.

        $APPTYPE,$DEFINE,$ELSE,$ENDIF,$ESCAPECHARS,$IFDEF,$IFNDEF,
        $INCLUDE,$MACRO,$OPTIMIZE,$OPTION,$RESOURCE,$TYPECHECK,$UNDEF,
        ABS,ACOS,ALIAS,AND,AS,ASC,ASIN,ATAN,ATN,BIN$,BIND,BYTE,
        CALL,CALLBACK,CALLFUNC,CASE,CEIL,CHDIR,CHDRIVE,CHR$,CINT,
        CLNG,CLS,CODEPTR,COMMAND$,COMMANDCOUNT,CONSOLE,CONST,CONSTRUCTOR,
        CONVBASE$,COS,CREATE,CSRLIN,CURDIR$,DATA,DATE$,DEC,DECLARE,
        DEFBYTE,DEFDBL,DEFDWORD,DEFINT,DEFLNG,DEFSHORT,DEFSNG,DEFSTR,
        DEFWORD,DELETE$,DIM,DIR$,DIREXISTS,DO,DOEVENTS,DOUBLE,DWORD,
        ELSE,ELSEIF,END,ENVIRON,ENVIRON$,EVENT,EXIT,EXP,EXTENDS,
        EXTRACTRESOURCE,FIELD$,FILEEXISTS,FIX,FLOOR,FOR,FORMAT$,FRAC,
        FUNCTION,FUNCTIONI,GET$,GOSUB,GOTO,HEX$,IF,INC,INITARRAY,
        INKEY$,INP,INPUT,INPUT$,INPUTHANDLE,INSERT$,INSTR,INT,INTEGER,
        INV,IS,ISCONSOLE,KILL,KILLMESSAGE,LBOUND,LCASE$,LEFT$,LEN,
        LFLUSH,LIB,LIBRARYINST,LOCATE,LOG,LONG,LOOP,LPRINT,LTRIM$,
        MEMCMP,MESSAGEBOX,MESSAGEDLG,MID$,MKDIR,MOD,MOUSEX,MOUSEY,
        NEXT,NOT,OFF,ON,OR,OUT,OUTPUTHANDLE,PARAMSTR$,PARAMSTRCOUNT,
        PARAMVAL,PARAMVALCOUNT,PCOPY,PEEK,PLAYWAV,POKE,POS,POSTMESSAGE,
        PRINT,PROPERTY,QUICKSORT,RANDOMIZE,REDIM,RENAME,REPLACE$,
        REPLACESUBSTR$,RESOURCE,RESOURCECOUNT,RESTORE,RESULT,RETURN,
        REVERSE$,RGB,RIGHT$,RINSTR,RMDIR,RND,ROUND,RTRIM$,RUN,
        SCREEN,SELECT,SENDER,SENDMESSAGE,SETCONSOLETITLE,SGN,SHELL,
        SHL,SHORT,SHOWMESSAGE,SHR,SIN,SINGLE,SIZEOF,SLEEP,SOUND,
        SPACE$,SQR,STACK,STATIC,STEP,STR$,STRF$,STRING,STRING$,
        SUB,SUBI,SWAP,TALLY,TAN,THEN,TIME$,TIMER,TO,TYPE,UBOUND,
        UCASE$,UNLOADLIBRARY,UNTIL,VAL,VARIANT,VARPTR,VARPTR$,VARTYPE,
        WEND,WHILE,WITH,WORD,XOR
    """)
    self.color('rapidq', text)
#@+node:ekr.20241129101413.1: *3* retire leo_to_html.xsl
#@+node:ekr.20241129101248.1: *4* leo_to_html.xsl (not used)
@language xml

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>

<!-- The default setting. Not needed unless there is a strip-space element. -->
  <!-- <xsl:preserve-space elements='leo_file nodes t'/> -->

<xsl:template match ='leo_file'>
<html>
  <head>
    <!--
    <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.9.1/styles/default.min.css">
    <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.9.1/highlight.min.js"></script>
    -->
    <style>
        body{background-color: #505050}
        /* pre { background:#FFE7C6; } */
        /* Must use h1 for nodes: see below. */
        h1 {
          font-size: 12pt;
          font-style: normal;
          font-weight: normal;
        }
        div.outlinepane {
          position: absolute;
          background: #ffffec; /* Leo yellow */
          top: 10px;
          height: 300px;
          width: 700px;
          overflow-x: hidden;
          overflow-y: scroll;
          line-height: 0.8;
          border: solid #bbb 1px;
          
        }
        div.bodypane {
          position: absolute;
          background: #ffffec; /* Leo yellow */
          top: 312px;
          height: 300px;
          width: 700px;
          overflow: scroll;
          border: solid #bbb 1px;
        }
        div.tnode {
            visibility: hidden;
            height: 0;
        }
        div.node {
            position: relative;
            left: 20px;
        }
        div.node[has-children] > h1 {
            <!-- works -->
            <!-- background: red; -->
        }
    </style>

    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
    <script>

      $(document).ready(function(){
        if (true) {
            // Toggle all but top-level nodes.
            // This requires an indication
            $(".node").toggle()
            $(".outlinepane").children(".node").toggle()
        } else {
            // Toggle all second-level nodes.
            // Safer, until we can see which nodes have children.
            $(".outlinepane").children(".node").children(".node").toggle()
        }
        $("h1").click(function(){
          $(this).parent().children("div.node").toggle();
          // The parent div's id is v.x.
          // Find the tnode div whose id is t.x.
          console.clear();
          parent_id=$(this).parent().attr("id");
          if (parent_id) {
            target=$(this).parent().attr("id").substring(2);
              console.log("clicked:"+$(this).text())
              // console.log("parent:"+$(this).parent())
              // console.log("target:"+target)
            $(".tnode").each(function(){
              console.log($(this).attr("id"))
              target2=$(this).attr("id").substring(2);
              if (target === target2) {
                console.log("found:"+target2)
                // $("pre.body-text").text($(this).text());
                $("code").text($(this).text());
              };
            }); // end .each.
          };
        });
      });
    </script>
  </head>
  <body>
    <xsl:apply-templates select='tnodes'/>
    <div class="outlinepane">
      <!-- <h4>Outline Pane</h4> -->
      <xsl:apply-templates select='vnodes'/>
    </div>
    <div class="bodypane">
      <!-- <h4>Body Pane</h4> -->
      <pre class="body-text"><code></code></pre>
    </div>
  </body>
</html>
</xsl:template>

<xsl:template match = 'tnodes'>
<div class="tnodes">
  <xsl:for-each select = 't'>
    <div class="tnode">
      <xsl:attribute name="id"><xsl:value-of select='@tx'/></xsl:attribute>
      <xsl:value-of select='.'/>
    </div>
  </xsl:for-each>
</div>
</xsl:template>

<xsl:template match = 'vnodes'>
  <xsl:for-each select = 'v'>
    <xsl:apply-templates select ='.'/>
  </xsl:for-each>
</xsl:template>

<xsl:template match='v'>
  <div class="node">
    <xsl:attribute name="id"><xsl:value-of select='@t'/></xsl:attribute>
    <xsl:choose>
      <xsl:when test ='./v' >
        <xsl:attribute name="has-children">1</xsl:attribute>
        <h1>+ <xsl:value-of select='vh'/></h1>
        <xsl:apply-templates select = 'v'/>
      </xsl:when>
      <xsl:when test ='vh' >
        <h1>- <xsl:value-of select='vh'/></h1>
      </xsl:when>
      <!--
      <xsl:otherwise>
        <h1>- <xsl:value-of select='vh'/></h1>
      </xsl:otherwise>
      -->
    </xsl:choose>
  </div>
</xsl:template>

</xsl:stylesheet>
#@+node:ekr.20241129101440.1: *4* do-leo.php (not used)
@language php

<?php
$DEBUG = 0;
/*************************************************************************/
/*
 * Support php script for load-leo.html
 *
 * Project purpose:
 *   Allow anyone to go to http://leo-editor.com/load-leo.html and use that
 *   html file to create a read-only version of the original leo file
 *   displayed in a meaningfull way by the web browser.
 *
 * Project operational summary:
 *   -An operator arrives at http://leo-editor.com/load-leo.html
 *   -The operator chooses a file either on their own hard drive, or on
 *    the Internet somewhere
 *   -With either choice, this php script receives, from the web page,
 *    either the name of the file to upload, or the name of a temporary
 *    file that has already been created in the php temp directory by the
 *    javascript in load-leo.html.
 *   -If this script receives the name of a temp file, then the contents
 *    of the target leo file already exist in the web server's file system
 *    in the form of a temporary file which will be deleted as soon as this
 *    script goes out of scope.
 *   -If this script receives the url of a file on the Internet, the script 
 *    knows to use that url as the source for the leo file to display.
 *   -This scripts reads the contents of the source leo file
 *   -This script then injects a reference to
 *    http://www.leoeditor.com/leo_to_html.xsl into the destination file
 *    if such a reference does not already exist in the destination file.
 *   -This script then writes the resulting file to a temporary file in the
 *    /tmp directory. The name format for the temporary file name is:
 *      show-leo-<16-hex-digit-random-name>.leo
 *   -This script then returns the file name to the calling load-leo.html
 *    file (Or an error message).
 *   -show-leo.html then either displays the error message, or changes
 *    document.location to the newly created leo file.
 *   -If show-leo.html changes its page location to the new temporary
 *    file, the apache server renders the page as xml to the browser, using
 *    the html-to_leo.xsl file as a stylesheet during the rendering.
 *
 * Notes:
 *   -This php script has only been tested using Firefox browsers.
 *   -The maximum file size for viewing is 10M.
 *   -leoeditor.com is hosted on a system using an Apache server, whose
 *    settings allow the placement of .htaccess in any root directory.
 *   -The .htaccess file in the root of leoeditor.com has been altered to
 *    request that apache consider all files ending in .leo to be treated
 *    as .xml files.
 *   -The line added to .htaccess is:
 *
 *       AddType application/xml leo
 *
 *   -Each time this script is run, the /tmp directory is scanned and any
 *    files older than 8 hours are removed. This serves as temporary file
 *    garbage collection.
 *   -In addition to scanning for older files, the entire contents of the
 *    /tmp directory are examined for total size. If the size exceeds
 *    500M, files are deleted, oldest first until the directory contents
 *    are less than 400M.
 *   -The maximum size for a single leo file is set by the php and apache
 *    setup. On this particular site, the max size is 64M. However, we
 *    limit the size to 10M.
 *   -Previous versions of this script placed a limit of 1000 users with
 *    concurrent access to this service. This restriction has been removed
 *    so there is no longer any artificial limit as to how many users may
 *    use this service at the same time.
 */
/*************************************************************************/
// Globals
/*************************************************************************/
$AMeg           = 1048576; // 1M
$Error_Message  = "\n" . '<br><p style="font-weight:900;">';
$Error_Message .= "Error uploading file</p><br>\n";
$File_Size      = 0;
$Is_Local       = false;
$Max_File_Size  = $AMeg * 10;
$Leo_Ext        = "leo";
if (!$DEBUG)
{
  // If not testing
  $Max_Storage_Size = $AMeg * 500;
  $Shrink_Size      = $Max_Storage_Size - ($AMeg * 100);
  $Temp_Dir         = getcwd() . "/tmp";
}
else
{
  // If testing
  $Max_Storage_Size = 339834;
  $Shrink_Size      = 21866;
  $Temp_Dir         = getcwd() . "/tmp1";
}
/*************************************************************************/
// Functions start here
/*************************************************************************/

// Return an error message, exit script
function error_exit()
{
  global $Error_Message;
  echo   $Error_Message;
  exit("");
} // error_exit

/*************************************************************************/

// Get the temp file name containing the data, or the remote url containing
// the data
function get_source_name()
{
  global $Is_Local;
  $Is_Local = false;
  foreach($_POST as $key => $value)
  {
    if ($key == "MAX_FILE_SIZE")
    {
      $name = $_FILES['Filedata']['tmp_name'];
      if ($name == "")
      {
        echo "<br>" . "File is too large or file name has an error." . "<br>";
        error_exit();
      }
      $Is_Local = true;
      return $name;
    }
  }
  $name     = $_POST['get_name'];
  if ($name == "")
  {
    echo "<br>" . "File is too large or file name has an error." . "<br>";
    error_exit();
  }
  return $name;
} // get_source_name

/*************************************************************************/

// Age out one file. It can't be a directory. If it is 8 hours old, kill
// it.
function age_file($filename)
{
  $day = time() - (8 * 60 * 60);
  if (file_exists($filename))
  {
    if (is_dir($filename))
      return true;
    $age = filemtime($filename);
      if ($age === false)
        return true;
    if ($age < $day)
      unlink($filename);
      return true;
  }
  return false;
} // age_file

/*************************************************************************/

// Delete files from the directory, oldest first, until the size is below
// the target size.

function size_directory()
{
  global $Max_Storage_Size;
  global $Shrink_Size;
  global $Temp_Dir;
  // sorting routine, sort by file age, oldest will be at the top of the
  // array
  function cmp($a, $b)
  {
    if ($a == $b)
      return 0;
    return ($a < $b) ? -1 : 1;
  }
  $filelist = array();
  $count    = 0;
  $handle = opendir($Temp_Dir);
  if (!$handle)
  {
    echo "<br>" . "File is too large or file name has an error." . "<br>";
    error_exit();
  }
  while (false !== ($entry = readdir($handle)))
  {
    if ($entry == ".")  continue;
    if ($entry == "..") continue;
    $fullname = $Temp_Dir . "/" . $entry;
    $filelist[$entry] = filemtime($fullname);
    $size += filesize($fullname);
    ++$count;
    if ($count > 500)
      break;
  } // while
  $keys = array_keys($filelist);
  if ($size < $Max_Storage_Size)
    return;
  uasort($filelist, cmp);
  $count = 0;
  $keys  = array_keys($filelist);
  foreach ($keys as $file_num => $fname)
  {
    $fullname = $Temp_Dir . "/" . $fname;
    $fsize = filesize($fullname);
    unlink($fullname);
    $size -= $fsize;
    if ($size < 0)
      $size = 0;
    if ($size < $Max_Storage_Size - $Shrink_Size)
      break;
    ++$count;
    if ($count > 10000)
      break;
  } // foreach
}
/*************************************************************************/

// 1. Age out any old temp files. We only age out 10000 at a time ;)
// 2. Check total size of directory and delete files, oldest first, if our
//    size limit has been exceeded.
function age_directory()
{
  global $Max_Storage_Size;
  global $Temp_Dir;
  $size  = 0;
  $count = 0;
  // Age old files
  $handle = opendir($Temp_Dir);
  if (!$handle)
  {
    echo "<br>" . "File is too large or file name has an error." . "<br>";
    error_exit();
  }
  while (false !== ($entry = readdir($handle)))
  {
    if ($entry == ".")  continue;
    if ($entry == "..") continue;
    $data = $Temp_Dir . "/" . $entry;
    if (!age_file($data))
      $size += filesize($data);
    ++$count;
    if ($count > 10000)
      break;
  }
  closedir($handle);
  if ($size >= $Max_Storage_Size)
    return size_directory();
} // age_directory

/*************************************************************************/

// Create a random hex string. Maximum length is 32.
function get_random_hex_string($length)
{
   $res = "";
   $count = 0;
   if ($length < 1)
     return "";
   if ($length > 32)
     $length = 32;
   while (strlen($res) < $length)
   {
     ++$count;
     if ($count > 33)
     {
       echo "<br>" . "File is too large or file name has an error." . "<br>";
       error_exit();
     }
     $temp = dechex(mt_rand());
     $res = $res . $temp;
     if (strlen($res) == $length)
       return $res;
     if (strlen($res) > $length)
       return substr($res, 0, $length);
   }
} // get_random_hex_string

/*************************************************************************/

// Create the output file object
function create_out_file()
{
   global $Temp_Dir;
   $stub = get_random_hex_string(16);
   if ($stub == "")
   {
     echo "<br>" . "Cannot create the temporary file on the web site." . "<br>";
     error_exit();
   }
   $name = "show-leo-" . $stub . ".leo";
   $file_object = fopen($Temp_Dir . "/" . $name, "w");
   if (!$file_object)
   {
     echo "<br>" . "Cannot create the temporary file on the web site." . "<br>";
     error_exit();
   }
   return array($name, $file_object);
} // create_out_file

/*************************************************************************/

// Check that this is a valid leo file name, and that the file size is
// ok.

function validate_file($name)
{
  global $File_Size;
  global $Is_Local;
  global $Leo_Ext;
  global $Max_File_Size;
  $File_Size = 0;
  if ($Is_Local)
  {
    // Can't check the name, because it has been named by javascript
    // using a temp file routine. Size should already be ok, but check
    // anyway to make sure we can stat the file.
    $File_Size = filesize($name);
    if (($File_Size > $Max_File_Size) || $File_Size == 0)
    {
      echo "<br>File is too large<br>\n";
      error_exit();
    }
    return;
  }
  // check name
  $extension = pathinfo($name, $options = PATHINFO_EXTENSION);
  if ($extension != $Leo_Ext)
  {
    echo "<br>The file must end in .leo<br>\n";
    error_exit();
  }
  // check size
  $headers = get_headers($name);
  foreach($headers as $h)
  {

    // split is deprecated. See Leo issue #1551
    // $chunks = split(":", $h);
    $chunks = explode(":", $h);
    $type   = $chunks[0];
    if ($type == "Content-Length")
      $File_Size = $chunks[1];
  }
  if ($File_Size > $Max_File_Size || $File_Size <= 0)
  {
    echo "<br>File is too large<br>\n";
    error_exit();
  }
  //echo "<br>File Size = ". $File_Size . "<br>\n";
}


/*************************************************************************/

// Read the source file into memory. If the file is too large to read all-
// at-once, php will barf, which is what we want. We will also enforce our
// own size limit
function read_source_file($name)
{
  validate_file($name);
  $lines = file($name);
  if (!$lines)
  {
    echo("<br>Can't read this file.<br>");
    error_exit();
  }
  return $lines;
} // read_source_file

/*************************************************************************/
// Script execution starts here
/*************************************************************************/

// Age out any existing old tmp files
age_directory();

// Either get the source script name
if (!$DEBUG)
{
  $script_name = get_source_name();
}

// Or use the following for testing
else
{
  $git_root    = "https://raw.githubusercontent.com/";
  $leo_root    = "leo-editor/leo-editor/master/";
  $script_name = $git_root . $leo_root . "leo/doc/CheatSheet.leo";
}

// Read the file contents into memory. Make sure it exists and is not too
// large.
$lines = read_source_file($script_name);

// Create the output file object
list($out_file_name, $out_file) = create_out_file();

/*************************************************************************/
// Read the first 10 lines of the file to see if there is an old reference
// to ekr_test or a new reference to leo_to_html.xsl. If there is a
// reference to leo_to_html.xsl, do nothing about such reference. If there
// is a reference to ekr_test, replace this reference with a reference to
// leo_to_html.xsl. If there is no reference to leo_to_html.xsl, inject
// a reference on line 2 of the file.
$false_line  = '<?xml-stylesheet type="text/xsl" href="http://leoeditor.';
$false_line .= 'com/leo_to_html.xsl"?>';

$inject_line_two = true;
$change_ekr_test = false;
foreach ($lines as $line_num => $line)
{
  if(strstr($line, "?xml-stylesheet ekr_test?"))
  {
    $change_ekr_test = true;
    $inject_line_two = false;
    if ($line_num == 10)
      break;
  }
} // foreach
foreach ($lines as $line_num => $line)
{
  if(strstr($line, "leo_to_html.xsl"))
  {
    if (!strstr($line, $false_line))
    {
      $change_ekr_test = false;
      $inject_line_two = false;
      if ($line_num == 10)
        break;
    }
  }
} // foreach

/*************************************************************************/
// Write the output file one line at a time. Insert the xsl reference if
// needed
$insert  = '<?xml-stylesheet type="text/xsl"';
$insert .= ' href="/leo_to_html.xsl"?>' . "\n";
foreach ($lines as $line_num => $line)
{
  if ($line_num == 2)
  {
    if ($inject_line_two)
      fwrite($out_file, $insert);
  }
  if ($line == "<?xml-stylesheet ekr_test?>\n")
  {
    if ($change_ekr_test)
      $line = $insert;
  }
  fwrite($out_file, $line);
} // foreach
fclose($out_file);
echo $out_file_name;
// The following lines for testing only
//echo "<br>out_file_name = " . $out_file_name . "<br>\n";
//echo "<br>Good Return<br>\n";
/*************************************************************************/
?>
#@+node:ekr.20250331063802.1: *3* retire documentation previous in LeoPyRef.leo
#@+node:ekr.20050721093241: *4* << about gui classes and gui plugins >>
@nocolor

The following are notes for anyone who is interested in writing
alternate gui's for Leo.

Rule 1: Leo's core is (or should be) free of gui-specific code.

Core code calls **gui wrapper methods** defined by gui-specific classes.

Rule 2: Gui-specific code should be localized to a single file.

Rule 3: Gui-specific code may call gui methods directly.

There are no restrictions about the code in the gui-specific classes.

Rule 4: Gui-specific classes must implement the gui wrapper methods
specified in the gui base classes.

This is the way that gui-specific classes provide gui-specific
services to Leo's core.

The alternative would be to implement all gui-specific commands
directly in the gui-specific code.  But this would be much more work
than needed.  For example, only a few gui-specific wrappers are needed
to implement all commands that deal with body text.  Implementing each
of these commands 'from scratch' would duplicate a lot of code
unnecessarily.

Using the gui wrapper methods is a bit messy for two reasons:

1. It requires defining enough wrappers (both in the base gui classes
   and subclasses) so that all gui-specific services needed by Leo's
   core are available.  Adding a wrapper to a gui base class involves
   adding it to all gui-specific subclasses.  It's easy to forget to
   add a wrapper.  The gui base class defines all wrappers as a
   function that just calls oops().  This prints a warning that the
   wrapper should be defined in a subclass.

2. The original wrappers assumed Tkinter-like indices.  Wrappers that
   were defined later assume Python indices (see Rule 5 below).

Rule 5: Leo's core should use Python indices, not gui-specific
indices.

Leo's core mostly follows this rule: there may be a few exceptions.

A Python index is an int that runs from 0 (beginning of text) to
len(s) (end of text s).  That is, there are exactly len(s) + 1 valid
indices.  In contrast, Tkinter indices run from "1.0" to "x.y" where
text s has x lines and where the length of the last line is y-1.

Two functions in leoGlobals.py support conversions from Python indices to
the row/column indices used by Tkinter.

- g.convertPythonIndexToRowCol converts a Python index to a row/column
  index used by Tkinter.

- g.convertRowColToPythonIndex does the reverse.

Important: the first Tkinter index is '1.0', not '0.0', but the row
returned by g.convertPythonIndexToRowCol is zero based, so the code
that actually creates Tkinter indices from row/col must add 1 to the
row.  Similar remarks apply when going in the reverse direction.
#@+node:ekr.20140831085423.18639: *4* About widgets and wrappers
Here is what you *must know* to understand Leo's core:

1. A **widget** is an actual gui widget.

Leo's core seldom accesses widgets directly.  Instead...

2. A **wrapper class** defines a standard api that hides the details
   of the underlying gui **text** widgets.

Leo's core uses this api almost exclusively. That is, Leo's core code
treats wrappers *as if* they were only text widgets there are!

There is, however, a back door for (hopefully rare!) special cases. All
wrapper classes define an official ``widget`` ivar, so core or plugin code
can gain access to the real (Qt) widget using wrapper.widget. Searching for
wrapper.widget will find all gui-dependent snippets of code in Leo's core.
#@+node:ekr.20240820045246.1: ** To be cleared later
# Clear after 6.8.2.
#@+node:ekr.20241125100825.1: *3* leo/scripts/sphinx_build.py (not used)
@language python

"""
leo/scripts/sphinx_build.py.

Invoke python/scripts/sphinx_build.exe

Note: This straightforward script works on EKR's Windows 11 machine.
      There is *no* guarantee that it will work elsewhere.
      
      EKR's sphinx-build.cmd calls this script as follows:
      python <path to>/leo-editor/leo/Scripts/sphinx_build.py %*
"""

import os
import subprocess
import sys

# Find python/Scripts/sphinx-build.exe.
python_folder = os.path.dirname(sys.executable)
script = os.path.normpath(os.path.join(
    python_folder, 'Scripts', 'sphinx-build.exe'))

if os.path.exists(script):
    # Create a command that executes python/Scripts/sphinx-build.exe.
    args_s = ' '.join(sys.argv[1:])
    script_s = f'"{script}" {args_s}'
    command = fr"python {script_s}"
    # Print and execute the command!
    print(f"sphinx_build.py: {command}\n")
    subprocess.Popen(command, shell=True).communicate()
else:
    print('')
    print(f"Not found: {script!r}")
    print('pip install sphinx')
    print('')
#@+node:ekr.20240821135031.1: *3* dw.change_layout (not used)
def change_layout(self, layout_name: str) -> None:
    """
    Find and change the @string qt-layout-name setting,
    then execute the restart-leo command.
    """
    c = self.leo_c
    h = '@string qt-layout-name'
    root = c.config.settingsRoot()
    if not root:
        top = c.lastTopLevel()
        root = top.insertAfter()
        root.h = '@settings'
        print('Adding @settings node')
        c.redraw()
    p = g.findNodeInTree(c, root, h, exact=False)
    if not p:
        p = root.insertAsLastChild()
        p.h = f"{h} = {layout_name}"
        print(f"Adding {p.h}")
        print(f"Switching to {layout_name}")
        c.save()
        # c.frame.reloadOutline()
        c.restartLeo()
    elif p.h.endswith(f" {layout_name}"):
        g.es_print(f"Already using {layout_name}")
    else:
        p.h = f"{h} = {layout_name}"
        c.save()
        print(f"Switching to {layout_name}")
        # c.frame.reloadOutline()
        c.restartLeo()
#@+node:ekr.20250327110007.1: *3* deleted via PR #4320
https://github.com/leo-editor/leo-editor/pull/4320
#@+node:vitalije.20170716201700.11: *4* _openFile (SqlitePickleShare)
def _openFile(self, fn: str, mode: str = 'r') -> Optional[Any]:
    """ Open this file.  Return a file object.

    Do not print an error message.
    It is not an error for this to fail.
    """
    try:
        return open(fn, mode)
    except Exception:
        return None
#@+node:ekr.20110605121601.17940: *4* qtree.wrapQLineEdit
def wrapQLineEdit(self, w: Wrapper) -> Wrapper:
    """A wretched kludge for MacOs k.masterMenuHandler."""
    c = self.c
    if isinstance(w, QtWidgets.QLineEdit):
        wrapper = self.edit_widget(c.p)
    else:
        wrapper = w
    return wrapper
#@+node:tbrown.20150724090431.10: *4* toggle_sclass
def toggle_sclass(self, w: Wrapper, prop: Any) -> None:
    """Toggle style class or list of classes prop on QWidget w"""
    if not prop:
        return
    props = set(self.sclasses(w))
    if isinstance(prop, str):
        prop = set([prop])
    else:
        prop = set(prop)
    current = props.intersection(prop)
    props.update(prop)
    props = props.difference(current)
    self.set_sclasses(w, props)
#@+node:ekr.20070422094710: *4* LeoBody.createChapterIvar
def createChapterIvar(self, w: Wrapper) -> None:
    c = self.c
    cc = c.chapterController
    if not hasattr(w, 'leo_chapter') or not w.leo_chapter:
        if cc and self.use_chapters:
            w.leo_chapter = cc.getSelectedChapter()
        else:
            w.leo_chapter = None
#@+node:ekr.20250401051638.1: *3* deleted via PR #4322
https://github.com/leo-editor/leo-editor/pull/4322
#@+node:ekr.20250401051709.27: *4* chapter.findEditorInChapter
def findEditorInChapter(self, p: Position) -> Wrapper:
    """return w, an editor displaying position p."""
    chapter, c = self, self.c
    w = c.frame.body.wrapper
    if w:
        w.leo_chapter = chapter
        w.leo_p = p and p.copy()
    return w
#@+node:ekr.20250401051710.23: *4* LeoBody.ensurePositionExists
def ensurePositionExists(self, w: TextAPI) -> bool:
    """Return True if w.leo_p exists or can be reconstituted."""
    c = self.c
    if c.positionExists(w.leo_p):
        return True
    g.trace('***** does not exist', w.leo_p)
    for p2 in c.all_unique_positions():
        if p2.v and p2.v == w.leo_v:
            w.leo_p = p2.copy()
            return True
    # This *can* happen when selecting a deleted node.
    w.leo_p = c.p
    return False
#@+node:ekr.20250331151305.15: *4* LeoBody.switchToChapter
def switchToChapter(self, w: TextAPI) -> None:
    """select w.leo_chapter."""
    c = self.c
    cc = c.chapterController
    chapter = getattr(w, 'leo_chapter', None)
    if chapter:
        chapter = w.leo_chapter
        name = chapter and chapter.name
        oldChapter = cc.getSelectedChapter()
        if chapter != oldChapter:
            cc.selectChapterByName(name)
            c.bodyWantsFocus()
#@+node:ekr.20250405071041.1: *3* deleted via PR #4323
#@+node:ekr.20250401111942.1: *4* at.languageFromAtFileNodeBody (not used)
def languageFromAtFileNodeBody(self, p: Position) -> Optional[str]:
    """
    p is an @<file> node.
    
    Return the language from p.b, looking for *unambiguous @language directives.
    
    Note: p.b will be empty when *reading* any @<file> node.
    """
    s = p.b.strip()
    if not s:
        return None
    languages: list[str] = []
    tag = '@language'
    for line in g.splitLines(s):
        if line.startswith(tag):
            language = line[len(tag) :].strip()
            languages.append(language)
    if len(languages) == 1:
        return languages[0]
    return None
#@+node:ekr.20080923070954.4: *4* at.scanAllDirectives (no longer used)
def scanAllDirectives(self, p: Position) -> dict[str, Value]:
    """
    Scan p and p's ancestors looking for directives,
    setting corresponding AtFile ivars.
    """
    g.deprecated()  # This method is deprecated
    at, c = self, self.c
    d = c.scanAllDirectives(p)

    # #4323: The hard cases. Set the language and delims using only p.h and p.b.
    delims = None
    if p.isAnyAtFileNode():  #4323: Look no further.
        language = at.languageFromAtFileNodeHeadline(p)
        # We *must* calculate delims when writing.
        delims = at.delimsFromAtFileNodeBody(p)
    elif p.h.startswith(('@button', '@command')):
        language = 'python'
    else:
        # Language doesn't matter here.
        # It would be unnecessary/wrong to examine ancestor nodes.
        language = c.target_language or 'python'
    at.language = language

    # Make sure to define delims.
    if delims in (None, (None, None, None)):  # #4256
        delims = g.set_delims_from_language(language)

    << Set comment strings from delims >>

    # Easy cases
    at.encoding = d.get('encoding') or c.config.default_derived_file_encoding
    lineending = d.get('lineending')
    at.explicitLineEnding = bool(lineending)
    at.output_newline = lineending or g.getOutputNewline(c=c)
    at.page_width = d.get('pagewidth') or c.page_width
    at.tab_width = d.get('tabwidth') or c.tab_width
    return {
        "encoding": at.encoding,
        "language": at.language,
        "lineending": at.output_newline,
        "pagewidth": at.page_width,
        "path": d.get('path'),
        "tabwidth": at.tab_width,
    }
#@+node:ekr.20080923070954.13: *5* << Set comment strings from delims >> (at.scanAllDirectives)
delim1, delim2, delim3 = delims
# Use single-line comments if we have a choice.
# delim1,delim2,delim3 now correspond to line,start,end
if delim1:
    at.startSentinelComment = delim1
    at.endSentinelComment = ""  # Must not be None.
elif delim2 and delim3:
    at.startSentinelComment = delim2
    at.endSentinelComment = delim3
else:  # pragma: no cover
    #
    # Emergency!
    #
    # Issue an error only if at.language has been set.
    # This suppresses a message from the markdown importer.
    if not g.unitTesting and at.language:
        g.trace(repr(at.language), g.callers())
        g.es_print(f"unknown language: {at.language}")
        g.es_print('using Python comment delimiters')
    at.startSentinelComment = "#"  # This should never happen!
    at.endSentinelComment = ""
#@+node:ekr.20250405064746.1: *4* c.scanForAtLanguage (never used)
### Use a regex to avoid allocating temp strings.
### at_language_pattern = re.compile(r'^@language\s+([\w]+)', re.MULTILINE)

def scanForAtLanguage(self, p: Position) -> str:
    """Return the language in effect for p."""
    c = self
    language = g.getLanguageFromAncestorAtFileNode(c.p)
    if language:
        assert g.isValidLanguage(language)
        return language
    return c.target_language or 'python'
    ###
        # for p2 in p.self_and_parents():
            # for m in c.at_language_pattern.finditer(p2.b):
                # language = m.group(1)
                # if g.isValidLanguage(language):
                    # return language
                # g.trace('OOP', language)
        # return c.target_language or 'python'
#@+node:ekr.20241012044745.1: *4* gdc.find_language
def find_language(self, c: Cmdr, p: Position) -> str:
    """Return the @language directive in effect at p."""
    if not p:
        return c.target_language
    return c.getLanguage(p)
#@+node:ekr.20210905052021.28: *4* TestAtFile.test_at_scanAllDirectives
def test_at_scanAllDirectives(self):
    self.skipTest("at.scanAllDirectives is deprecated")
    at, c = self.at, self.c
    d = at.scanAllDirectives(c.p)
    # These are the commander defaults, without any settings.
    self.assertEqual(d.get('language'), 'python')
    self.assertEqual(d.get('tabwidth'), -4)
    self.assertEqual(d.get('pagewidth'), 132)
#@+node:ekr.20210905052021.29: *4* TestAtFile.test_at_scanAllDirectives_minimal_
def test_at_scanAllDirectives_minimal_(self):
    self.skipTest("at.scanAllDirectives is deprecated")
    at, c = self.at, self.c
    d = at.scanAllDirectives(c.p)
    d = c.atFileCommands.scanAllDirectives(c.p)
    assert d
#@+node:ekr.20210906075242.17: *4* TestCommands.test_c_scanAllDirectives
def test_c_scanAllDirectives(self):
    self.skipTest("c.scanAllDirectives is deprecated")
    c = self.c
    d = c.scanAllDirectives(c.p)
    # These are the commander defaults, without any settings.
    self.assertEqual(d.get('language'), 'python')
    self.assertEqual(d.get('tabwidth'), -4)
    self.assertEqual(d.get('pagewidth'), 132)
#@+node:ekr.20210906075242.18: *4* TestCommands.test_c_scanAtPathDirectives (legacy)
def test_c_scanAtPathDirectives(self):
    c, p = self.c, self.c.p
    child = p.insertAfter()
    child.h = '@path one'
    grand = child.insertAsLastChild()
    grand.h = '@path two'
    great = grand.insertAsLastChild()
    great.h = 'xyz'
    aList = g.get_directives_dict_list(great)
    path = c.scanAtPathDirectives(aList)
    endpath = g.os_path_normpath('one/two')
    assert path.endswith(endpath), f"expected '{endpath}' got '{path}'"

    # Test 2: Create a commander for an outline outside of g.app.loadDir and its parents.
    from leo.core.leoCommands import Commands
    c = Commands(fileName='~/LeoPyRef.leo', gui=g.app.gui)
    child = p.insertAfter()
    child.h = '@path one'
    grand = child.insertAsLastChild()
    grand.h = '@path two'
    great = grand.insertAsLastChild()
    great.h = 'xyz'
    aList = g.get_directives_dict_list(great)
    path = c.scanAtPathDirectives(aList)
    endpath = g.os_path_normpath('one/two')
    assert path.endswith(endpath), f"expected '{endpath}' got '{path}'"
#@+node:ekr.20210906075242.19: *4* TestCommands.test_c_scanAtPathDirectives_same_name_subdirs (legacy)
def test_c_scanAtPathDirectives_same_name_subdirs(self):
    c = self.c
    # p2 = p.firstChild().firstChild().firstChild()
    p = c.p
    child = p.insertAfter()
    child.h = '@path again'
    grand = child.insertAsLastChild()
    grand.h = '@path again'
    great = grand.insertAsLastChild()
    great.h = 'xyz'
    aList = g.get_directives_dict_list(great)
    path = c.scanAtPathDirectives(aList)
    endpath = g.os_path_normpath('again/again')
    self.assertTrue(path and path.endswith(endpath))
#@+node:ekr.20210905203541.16: *4* TestGlobals.test_g_get_directives_dict
def test_g_get_directives_dict(self):
    c = self.c
    p = c.p
    # Note: @comment must follow @language.
    p.b = self.prep(
    """
        ATlanguage python
        ATcomment a b c
        ATtabwidth -8
        ATpagewidth 72
        ATencoding utf-8
    """).replace('AT', '@')
    d = g.get_directives_dict(p)
    self.assertEqual(d.get('language'), 'python')
    self.assertEqual(d.get('tabwidth'), '-8')
    self.assertEqual(d.get('pagewidth'), '72')
    self.assertEqual(d.get('encoding'), 'utf-8')
    self.assertEqual(d.get('comment'), 'a b c')
    assert not d.get('path'), d.get('path')
#@+node:ekr.20210905203541.33: *4* TestGlobals.test_g_scanAtHeaderDirectives_header
def test_g_scanAtHeaderDirectives_header(self):
    c = self.c
    aList = g.get_directives_dict_list(c.p)
    g.scanAtHeaderDirectives(aList)
#@+node:ekr.20210905203541.35: *4* TestGlobals.test_g_scanAtHeaderDirectives_noheader
def test_g_scanAtHeaderDirectives_noheader(self):
    c = self.c
    aList = g.get_directives_dict_list(c.p)
    g.scanAtHeaderDirectives(aList)
#@+node:ekr.20210905203541.36: *4* TestGlobals.test_g_scanAtLineendingDirectives_cr
def test_g_scanAtLineendingDirectives_cr(self):
    c = self.c
    p = c.p
    p.b = '@lineending cr\n'
    aList = g.get_directives_dict_list(p)
    s = g.scanAtLineendingDirectives(aList)
    self.assertEqual(s, '\r')
#@+node:ekr.20210905203541.37: *4* TestGlobals.test_g_scanAtLineendingDirectives_crlf
def test_g_scanAtLineendingDirectives_crlf(self):
    c = self.c
    p = c.p
    p.b = '@lineending crlf\n'
    aList = g.get_directives_dict_list(p)
    s = g.scanAtLineendingDirectives(aList)
    self.assertEqual(s, '\r\n')
#@+node:ekr.20210905203541.38: *4* TestGlobals.test_g_scanAtLineendingDirectives_lf
def test_g_scanAtLineendingDirectives_lf(self):
    c = self.c
    p = c.p
    p.b = '@lineending lf\n'
    aList = g.get_directives_dict_list(p)
    s = g.scanAtLineendingDirectives(aList)
    self.assertEqual(s, '\n')
#@+node:ekr.20210905203541.39: *4* TestGlobals.test_g_scanAtLineendingDirectives_nl
def test_g_scanAtLineendingDirectives_nl(self):
    c = self.c
    p = c.p
    p.b = '@lineending nl\n'
    aList = g.get_directives_dict_list(p)
    s = g.scanAtLineendingDirectives(aList)
    self.assertEqual(s, '\n')
#@+node:ekr.20210905203541.40: *4* TestGlobals.test_g_scanAtLineendingDirectives_platform
def test_g_scanAtLineendingDirectives_platform(self):
    c = self.c
    p = c.p
    p.b = '@lineending platform\n'
    aList = g.get_directives_dict_list(p)
    s = g.scanAtLineendingDirectives(aList)
    if sys.platform.startswith('win'):
        self.assertEqual(s, '\r\n')  # pragma: no cover
    else:
        self.assertEqual(s, '\n')  # pragma: no cover
#@+node:ekr.20210905203541.42: *4* TestGlobals.test_g_scanAtPagewidthDirectives_40
def test_g_scanAtPagewidthDirectives_40(self):
    c = self.c
    p = c.p
    p.b = '@pagewidth 40\n'
    aList = g.get_directives_dict_list(p)
    n = g.scanAtPagewidthDirectives(aList)
    self.assertEqual(n, 40)
#@+node:ekr.20210905203541.41: *4* TestGlobals.test_g_scanAtPagewidthDirectives_minus_40
def test_g_scanAtPagewidthDirectives_minus_40(self):
    c = self.c
    p = c.p
    p.b = '@pagewidth -40\n'
    aList = g.get_directives_dict_list(p)
    n = g.scanAtPagewidthDirectives(aList)
    # The @pagewidth directive in the parent should control.
    # Depending on how this test is run, the result could be 80 or None.
    assert n in (None, 80), repr(n)
#@+node:ekr.20210905203541.43: *4* TestGlobals.test_g_scanAtTabwidthDirectives_6
def test_g_scanAtTabwidthDirectives_6(self):
    c = self.c
    p = c.p
    p.b = '@tabwidth 6\n'
    aList = g.get_directives_dict_list(p)
    n = g.scanAtTabwidthDirectives(aList)
    self.assertEqual(n, 6)
#@+node:ekr.20210905203541.44: *4* TestGlobals.test_g_scanAtTabwidthDirectives_minus_6
def test_g_scanAtTabwidthDirectives_minus_6(self):
    c = self.c
    p = c.p
    p.b = '@tabwidth -6\n'
    aList = g.get_directives_dict_list(p)
    n = g.scanAtTabwidthDirectives(aList)
    self.assertEqual(n, -6)
#@+node:ekr.20210905203541.45: *4* TestGlobals.test_g_scanAtWrapDirectives_nowrap
def test_g_scanAtWrapDirectives_nowrap(self):
    c = self.c
    p = c.p
    p.b = '@nowrap\n'
    aList = g.get_directives_dict_list(p)
    s = g.scanAtWrapDirectives(aList)
    assert s is False, repr(s)
#@+node:ekr.20210905203541.46: *4* TestGlobals.test_g_scanAtWrapDirectives_wrap_with_wrap_
def test_g_scanAtWrapDirectives_wrap_with_wrap_(self):
    c = self.c
    p = c.p
    p.b = '@wrap\n'
    aList = g.get_directives_dict_list(p)
    s = g.scanAtWrapDirectives(aList)
    assert s is True, repr(s)
#@+node:ekr.20210905203541.47: *4* TestGlobals.test_g_scanAtWrapDirectives_wrap_without_nowrap_
def test_g_scanAtWrapDirectives_wrap_without_nowrap_(self):
    c = self.c
    aList = g.get_directives_dict_list(c.p)
    s = g.scanAtWrapDirectives(aList)
    assert s is None, repr(s)
#@-all
#@@nosearch
#@-leo
