about summary refs log blame commit diff stats
path: root/ranger/api/commands.py
blob: 1997cde5574bad0892decc1db2855873435b23c5 (plain) (tree)
d1a1173d ^










<
-- text editor, particularly text drawing, horizontal wrap, vertical scrolling
Text = {}

local utf8 = require 'utf8'

require 'search'
require 'select'
require 'undo'
require 'text_tests'

-- return values:
--  y coordinate drawn until in px
--  position of start of final screen line drawn
function Text.draw(line, line_width, line_index)
--?   print('text.draw')
  love.graphics.setColor(0,0,0)
  -- wrap long lines
  local x = 25
  local y = line.y
  local pos = 1
  local screen_line_starting_pos = 1
  if line.fragments == nil then
    Text.compute_fragments(line, line_width)
  end
  if line.screen_line_starting_pos == nil then
    Text.populate_screen_line_starting_pos(line_index)
  end
--?   print('--')
  for _, f in ipairs(line.fragments) do
    local frag, frag_text = f.data, f.text
    -- render fragment
    local frag_width = App.width(frag_text)
    local frag_len = utf8.len(frag)
--?     local s=tostring
--?     print('('..s(x)..','..s(y)..') '..frag..'('..s(frag_width)..' vs '..s(line_width)..') '..s(line_index)..' vs '..s(Screen_top1.line)..'; '..s(pos)..' vs '..s(Screen_top1.pos)..'; bottom: '..s(Screen_bottom1.line)..'/'..s(Screen_bottom1.pos))
    if x + frag_width > line_width then
      assert(x > 25)  -- no overfull lines
      -- update y only after drawing the first screen line of screen top
      if Text.lt1(Screen_top1, {line=line_index, pos=pos}) then
        y = y + Line_height
        if y + Line_height > App.screen.height then
--?           print('b', y, App.screen.height, '=>', screen_line_starting_pos)
          return y, screen_line_starting_pos
        end
        screen_line_starting_pos = pos
--?         print('text: new screen line', y, App.screen.height, screen_line_starting_pos)
      end
      x = 25
    end
--?     print('checking to draw', pos, Screen_top1.pos)
    -- don't draw text above screen top
    if Text.le1(Screen_top1, {line=line_index, pos=pos}) then
      if Selection1.line then
        local lo, hi = Text.clip_selection(line_index, pos, pos+frag_len)
        Text.draw_highlight(line, x,y, pos, lo,hi)
      end
--?       print('drawing '..frag)
      App.screen.draw(frag_text, x,y)
    end
    -- render cursor if necessary
    if line_index == Cursor1.line then
      if pos <= Cursor1.pos and pos + frag_len > Cursor1.pos then
        if Search_term then
          if Lines[Cursor1.line].data:sub(Cursor1.pos, Cursor1.pos+utf8.len(Search_term)-1) == Search_term then
            local lo_px = Text.draw_highlight(line, x,y, pos, Cursor1.pos, Cursor1.pos+utf8.len(Search_term))
            love.graphics.setColor(0,0,0)
            love.graphics.print(Search_term, x+lo_px,y)
          end
        else
          Text.draw_cursor(x+Text.x(frag, Cursor1.pos-pos+1), y)
        end
      end
    end
    x = x + frag_width
    pos = pos + frag_len
  end
  if Search_term == nil then
    if line_index == Cursor1.line and Cursor1.pos == pos then
      Text.draw_cursor(x, y)
    end
  end
  return y, screen_line_starting_pos
end
-- manual tests:
--  draw with small line_width of 100

function Text.draw_cursor(x, y)
  -- blink every 0.5s
  if math.floor(Cursor_time*2)%2 == 0 then
    love.graphics.setColor(1,0,0)
    love.graphics.rectangle('fill', x,y, 3,Line_height)
    love.graphics.setColor(0,0,0)
  end
  Cursor_x = x
  Cursor_y = y+Line_height
end

function Text.compute_fragments(line, line_width)
--?   print('compute_fragments', line_width)
  line.fragments = {}
  local x = 25
  -- try to wrap at word boundaries
  for frag in line.data:gmatch('%S*%s*') do
    local frag_text = App.newText(love.graphics.getFont(), frag)
    local frag_width = App.width(frag_text)
--?     print('x: '..tostring(x)..'; '..tostring(line_width-x)..'px to go')
--?     print('frag: ^'..frag..'$ is '..tostring(frag_width)..'px wide')
    if x + frag_width > line_width then
      while x + frag_width > line_width do
--?         print(x, frag, frag_width, line_width)
        if x < 0.8*line_width then
--?           print(frag, x, frag_width, line_width)
          -- long word; chop it at some letter
          -- We're not going to reimplement TeX here.
          local b = Text.nearest_pos_less_than(frag, line_width - x)
          assert(b > 0)  -- avoid infinite loop when window is too narrow
--?           print('space for '..tostring(b)..' graphemes')
          local frag1 = string.sub(frag, 1, b)
          local frag1_text = App.newText(love.graphics.getFont(), frag1)
          local frag1_width = App.width(frag1_text)
--?           print(frag, x, frag1_width, line_width)
          assert(x + frag1_width <= line_width)
--?           print('inserting '..frag1..' of width '..tostring(frag1_width)..'px')
          table.insert(line.fragments, {data=frag1, text=frag1_text})
          frag = string.sub(frag, b+1)
          frag_text = App.newText(love.graphics.getFont(), frag)
          frag_width = App.width(frag_text)
        end
        x = 25  -- new line
      end
    end
    if #frag > 0 then
--?       print('inserting '..frag..' of width '..tostring(frag_width)..'px')
      table.insert(line.fragments, {data=frag, text=frag_text})
    end
    x = x + frag_width
  end
end

function Text.textinput(t)
  if love.mouse.isDown('1') then return end
  if App.ctrl_down() or App.alt_down() or App.cmd_down() then return end
  if Selection1.line then
    Text.delete_selection()
  end
  local before = snapshot(Cursor1.line)
--?   print(Screen_top1.line, Screen_top1.pos, Cursor1.line, Cursor1.pos, Screen_bottom1.line, Screen_bottom1.pos)
  Text.insert_at_cursor(t)
  if Cursor_y >= App.screen.height - Line_height then
    Text.populate_screen_line_starting_pos(Cursor1.line)
    Text.snap_cursor_to_bottom_of_screen()
--?     print('=>', Screen_top1.line, Screen_top1.pos, Cursor1.line, Cursor1.pos, Screen_bottom1.line, Screen_bottom1.pos)
  end
  record_undo_event({before=before, after=snapshot(Cursor1.line)})
end

function Text.insert_at_cursor(t)
  local byte_offset
  if Cursor1.pos > 1 then
    byte_offset = utf8.offset(Lines[Cursor1.line].data, Cursor1.pos)
  else
    byte_offset = 1
  end
  if byte_offset == nil then
    print(Cursor1.line, Cursor1.pos, byte_offset, Lines[Cursor1.line].data)
    assert(false)
  end
  Lines[Cursor1.line].data = string.sub(Lines[Cursor1.line].data, 1, byte_offset-1)..t..string.sub(Lines[Cursor1.line].data, byte_offset)
  Lines[Cursor1.line].fragments = nil
  Lines[Cursor1.line].screen_line_starting_pos = nil
  Cursor1.pos = Cursor1.pos+1
end

-- Don't handle any keys here that would trigger love.textinput above.
function Text.keychord_pressed(chord)
--?   print(chord)
  --== shortcuts that mutate text
  if chord == 'return' then
    local before_line = Cursor1.line
    local before = snapshot(before_line)
    Text.insert_return()
    if (Cursor_y + Line_height) > App.screen.height then
      Text.snap_cursor_to_bottom_of_screen()
    end
    save_to_disk(Lines, Filename)
    record_undo_event({before=before, after=snapshot(before_line, Cursor1.line)})
  elseif chord == 'tab' then
    local before = snapshot(Cursor1.line)
--?     print(Screen_top1.line, Screen_top1.pos, Cursor1.line, Cursor1.pos, Screen_bottom1.line, Screen_bottom1.pos)
    Text.insert_at_cursor('\t')
    if Cursor_y >= App.screen.height - Line_height then
      Text.populate_screen_line_starting_pos(Cursor1.line)
      Text.snap_cursor_to_bottom_of_screen()
--?       print('=>', Screen_top1.line, Screen_top1.pos, Cursor1.line, Cursor1.pos, Screen_bottom1.line, Screen_bottom1.pos)
    end
    save_to_disk(Lines, Filename)
    record_undo_event({before=before, after=snapshot(Cursor1.line)})
  elseif chord == 'backspace' then
    if Selection1.line then
      Text.delete_selection()
      save_to_disk(Lines, Filename)
      return
    end
    local before
    if Cursor1.pos > 1 then
      before = snapshot(Cursor1.line)
      local byte_start = utf8.offset(Lines[Cursor1.line].data, Cursor1.pos-1)
      local byte_end = utf8.offset(Lines[Cursor1.line].data, Cursor1.pos)
      if byte_start then
        if byte_end then
          Lines[Cursor1.line].data = string.sub(Lines[Cursor1.line].data, 1, byte_start-1)..string.sub(Lines[Cursor1.line].data, byte_end)
        else
          Lines[Cursor1.line].data = string.sub(Lines[Cursor1.line].data, 1, byte_start-1)
        end
        Lines[Cursor1.line].fragments = nil
        Cursor1.pos = Cursor1.pos-1
      end
    elseif Cursor1.line > 1 then
      before = snapshot(Cursor1.line-1, Cursor1.line)
      if Lines[Cursor1.line-1].mode == 'drawing' then
        table.remove(Lines, Cursor1.line-1)
      else
        -- join lines
        Cursor1.pos = utf8.len(Lines[Cursor1.line-1].data)+1
        Lines[Cursor1.line-1].data = Lines[Cursor1.line-1].data..Lines[Cursor1.line].data
        Lines[Cursor1.line-1].fragments = nil
        table.remove(Lines, Cursor1.line)
      end
      Cursor1.line = Cursor1.line-1
    end
    if Text.lt1(Cursor1, Screen_top1) then
      local top2 = Text.to2(Screen_top1)
      top2 = Text.previous_screen_line(top2)
      Screen_top1 = Text.to1(top2)
      Text.redraw_all()  -- if we're scrolling, reclaim all fragments to avoid memory leaks
    end
    assert(Text.le1(Screen_top1, Cursor1))
    save_to_disk(Lines, Filename)
    record_undo_event({before=before, after=snapshot(Cursor1.line)})
  elseif chord == 'delete' then
    if Selection1.line then
      Text.delete_selection()
      save_to_disk(Lines, Filename)
      return
    end
    local before
    if Cursor1.pos <= utf8.len(Lines[Cursor1.line].data) then
      before = snapshot(Cursor1.line)
    else
      before = snapshot(Cursor1.line, Cursor1.line+1)
    end
    if Cursor1.pos <= utf8.len(Lines[Cursor1.line].data) then
      local byte_start = utf8.offset(Lines[Cursor1.line].data, pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
# This file is part of ranger, the console file manager.
# License: GNU GPL version 3, see the file "AUTHORS" for details.

# TODO: Add an optional "!" to all commands and set a flag if it's there

from __future__ import (absolute_import, division, print_function)

import os
import re
# COMPAT pylint: disable=unused-import
from collections import deque  # NOQA
from ranger.api import LinemodeBase, hook_init, hook_ready, register_linemode  # NOQA
# pylint: enable=unused-import

import ranger
from ranger.core.shared import FileManagerAware
from ranger.ext.lazy_property import lazy_property

_SETTINGS_RE = re.compile(r'^\s*([^\s]+?)=(.*)$')


def _alias_line(full_command, line):
    return full_command + ''.join(re.split(r'(\s+)', line)[1:])


class CommandContainer(FileManagerAware):

    def __init__(self):
        self.commands = {}

    def __getitem__(self, key):
        return self.commands[key]

    def alias(self, name, full_command):
        cmd_name = full_command.split()[0]
        try:
            cmd = self.get_command(cmd_name)
        except KeyError:
            self.fm.notify('alias failed: No such command: {0}'.format(cmd_name), bad=True)
            return None

        class CommandAlias(cmd):   # pylint: disable=too-few-public-methods
            def __init__(self, line, *args, **kwargs):
                super(CommandAlias, self).__init__(
                    _alias_line(self.full_command, line), *args, **kwargs)

        cmd_alias = type(name, (CommandAlias, ), dict(full_command=full_command))
        if issubclass(cmd_alias, FunctionCommand):
            cmd_alias.based_function = name
            cmd_alias.object_name = name
            cmd_alias.function_name = name
        self.commands[name] = cmd_alias

    def load_commands_from_module(self, module):
        for var in vars(module).values():
            try:
                if issubclass(var, Command) and var != Command \
                        and var != FunctionCommand:
                    self.commands[var.get_name()] = var
            except TypeError:
                pass

    def load_commands_from_object(self, obj, filtr):
        for attribute_name in dir(obj):
            if attribute_name[0] == '_' or attribute_name not in filtr:
                continue
            attribute = getattr(obj, attribute_name)
            if hasattr(attribute, '__call__'):
                cmd = type(attribute_name, (FunctionCommand, ), dict(__doc__=attribute.__doc__))
                cmd.based_function = attribute
                cmd.function_name = attribute.__name__
                cmd.object_name = obj.__class__.__name__
                self.commands[attribute_name] = cmd

    def get_command(self, name, abbrev=True):
        if abbrev:
            lst = [cls for cmd, cls in self.commands.items()
                   if cls.allow_abbrev and cmd.startswith(name) or cmd == name]
            if not lst:
                raise KeyError
            if len(lst) == 1:
                return lst[0]
            if self.commands[name] in lst:
                return self.commands[name]
            raise ValueError("Ambiguous command")
        else:
            try:
                return self.commands[name]
            except KeyError:
                return None

    def command_generator(self, start):
        return sorted(cmd + ' ' for cmd in self.commands if cmd.startswith(start))


class Command(FileManagerAware):
    """Abstract command class"""
    name = None
    allow_abbrev = True
    resolve_macros = True
    escape_macros_for_shell = False
    quantifier = None
    _shifted = 0
    _setting_line = None

    def __init__(self, line, quantifier=None):
        self.init_line(line)
        self.quantifier = quantifier
        self.quickly_executed = False

    def init_line(self, line):
        self.line = line
        self.args = line.split()
        try:
            self.firstpart = line[:line.rindex(' ') + 1]
        except ValueError:
            self.firstpart = ''

    @classmethod
    def get_name(cls):
        classdict = cls.__mro__[0].__dict__
        if 'name' in classdict and classdict['name']:
            return cls.name
        return cls.__name__

    def execute(self):
        """Override this"""

    def tab(self, tabnum):
        """Override this"""

    def quick(self):
        """Override this"""

    def cancel(self):
        """Override this"""

    # Easy ways to get information
    def arg(self, n):
        """Returns the nth space separated word"""
        try:
            return self.args[n]
        except IndexError:
            return ""

    def rest(self, n):
        """Returns everything from and after arg(n)"""
        got_space = True
        word_count = 0
        for i, char in enumerate(self.line):
            if char.isspace():
                if not got_space:
                    got_space = True
                    word_count += 1
            elif got_space:
                got_space = False
                if word_count == n + self._shifted:
                    return self.line[i:]
        return ""

    def start(self, n):
        """Returns everything until (inclusively) arg(n)"""
        return ' '.join(self.args[:n]) + " "  # XXX

    def shift(self):
        del self.args[0]
        self._setting_line = None
        self._shifted += 1

    def parse_setting_line(self):
        """
        Parses the command line argument that is passed to the `:set` command.
        Returns [option, value, name_complete].

        Can parse incomplete lines too, and `name_complete` is a boolean
        indicating whether the option name looks like it's completed or
        unfinished.  This is useful for generating tab completions.

        >>> Command("set foo=bar").parse_setting_line()
        ['foo', 'bar', True]
        >>> Command("set foo").parse_setting_line()
        ['foo', '', False]
        >>> Command("set foo=").parse_setting_line()
        ['foo', '', True]
        >>> Command("set foo ").parse_setting_line()
        ['foo', '', True]
        >>> Command("set myoption myvalue").parse_setting_line()
        ['myoption', 'myvalue', True]
        >>> Command("set").parse_setting_line()
        ['', '', False]
        """
        if self._setting_line is not None:
            return self._setting_line
        match = _SETTINGS_RE.match(self.rest(1))
        if match:
            self.firstpart += match.group(1) + '='
            result = [match.group(1), match.group(2), True]
        else:
            result = [self.arg(1), self.rest(2), ' ' in self.rest(1)]
        self._setting_line = result
        return result

    def parse_setting_line_v2(self):
        """
        Parses the command line argument that is passed to the `:set` command.
        Returns [option, value, name_complete, toggle].

        >>> Command("set foo=bar").parse_setting_line_v2()
        ['foo', 'bar', True, False]
        >>> Command("set foo!").parse_setting_line_v2()
        ['foo', '', True, True]
        """
        option, value, name_complete = self.parse_setting_line()
        if len(option) >= 2 and option[-1] == '!':
            toggle = True
            option = option[:-1]
            name_complete = True
        else:
            toggle = False
        return [option, value, name_complete, toggle]

    def parse_flags(self):
        """Finds and returns flags in the command

        >>> Command("").parse_flags()
        ('', '')
        >>> Command("foo").parse_flags()
        ('', '')
        >>> Command("shell test").parse_flags()
        ('', 'test')
        >>> Command("shell -t ls -l").parse_flags()
        ('t', 'ls -l')
        >>> Command("shell -f -- -q test").parse_flags()
        ('f', '-q test')
        >>> Command("shell -foo -bar rest of the command").parse_flags()
        ('foobar', 'rest of the command')
        """
        flags = ""
        args = self.line.split()
        rest = ""
        if args:
            rest = self.line[len(args[0]):].lstrip()
            for arg in args[1:]:
                if arg == "--":
                    rest = rest[2:].lstrip()
                    break
                elif len(arg) > 1 and arg[0] == "-":
                    rest = rest[len(arg):].lstrip()
                    flags += arg[1:]
                else:
                    break
        return flags, rest

    @lazy_property
    def log(self):
        import logging
        return logging.getLogger('ranger.commands.' + self.__class__.__name__)

    # COMPAT: this is still used in old commands.py configs
    def _tab_only_directories(self):
        from os.path import dirname, basename, expanduser, join

        cwd = self.fm.thisdir.path

        rel_dest = self.rest(1)

        # expand the tilde into the user directory
        if rel_dest.startswith('~'):
            rel_dest = expanduser(rel_dest)

        # define some shortcuts
        abs_dest = join(cwd, rel_dest)
        abs_dirname = dirname(abs_dest)
        rel_basename = basename(rel_dest)
        rel_dirname = dirname(rel_dest)

        try:
            # are we at the end of a directory?
            if rel_dest.endswith('/') or rel_dest == '':
                _, dirnames, _ = next(os.walk(abs_dest))

            # are we in the middle of the filename?
            else:
                _, dirnames, _ = next(os.walk(abs_dirname))
                dirnames = [dn for dn in dirnames
                            if dn.startswith(rel_basename)]
        except (OSError, StopIteration):
            # os.walk found nothing
            pass
        else:
            dirnames.sort()

            # no results, return None
            if not dirnames:
                return

            # one result. since it must be a directory, append a slash.
            if len(dirnames) == 1:
                return self.start(1) + join(rel_dirname, dirnames[0]) + '/'

            # more than one result. append no slash, so the user can
            # manually type in the slash to advance into that directory
            return (self.start(1) + join(rel_dirname, dirname)
                    for dirname in dirnames)

    def _tab_directory_content(self):  # pylint: disable=too-many-locals
        from os.path import dirname, basename, expanduser, join

        cwd = self.fm.thisdir.path

        rel_dest = self.rest(1)

        # expand the tilde into the user directory
        if rel_dest.startswith('~'):
            rel_dest = expanduser(rel_dest)

        # define some shortcuts
        abs_dest = join(cwd, rel_dest)
        abs_dirname = dirname(abs_dest)
        rel_basename = basename(rel_dest)
        rel_dirname = dirname(rel_dest)

        try:
            directory = self.fm.get_directory(abs_dest)

            # are we at the end of a directory?
            if rel_dest.endswith('/') or rel_dest == '':
                if directory.content_loaded:
                    # Take the order from the directory object
                    names = [f.basename for f in directory.files]
                    if self.fm.thisfile.basename in names:
                        i = names.index(self.fm.thisfile.basename)
                        names = names[i:] + names[:i]
                else:
                    # Fall back to old method with "os.walk"
                    _, dirnames, filenames = next(os.walk(abs_dest))
                    names = sorted(dirnames + filenames)

            # are we in the middle of the filename?
            else:
                if directory.content_loaded:
                    # Take the order from the directory object
                    names = [f.basename for f in directory.files
                             if f.basename.startswith(rel_basename)]
                    if self.fm.thisfile.basename in names:
                        i = names.index(self.fm.thisfile.basename)
                        names = names[i:] + names[:i]
                else:
                    # Fall back to old method with "os.walk"
                    _, dirnames, filenames = next(os.walk(abs_dirname))
                    names = sorted([name for name in (dirnames + filenames)
                                    if name.startswith(rel_basename)])
        except (OSError, StopIteration):
            # os.walk found nothing
            pass
        else:
            # no results, return None
            if not names:
                return

            # one result. append a slash if it's a directory
            if len(names) == 1:
                path = join(rel_dirname, names[0])
                slash = '/' if os.path.isdir(path) else ''
                return self.start(1) + path + slash

            # more than one result. append no slash, so the user can
            # manually type in the slash to advance into that directory
            return (self.start(1) + join(rel_dirname, name) for name in names)

    def _tab_through_executables(self):
        from ranger.ext.get_executables import get_executables
        programs = [program for program in get_executables() if
                    program.startswith(self.rest(1))]
        if not programs:
            return
        if len(programs) == 1:
            return self.start(1) + programs[0]
        programs.sort()
        return (self.start(1) + program for program in programs)


class FunctionCommand(Command):
    based_function = None
    object_name = ""
    function_name = "unknown"

    def execute(self):  # pylint: disable=too-many-branches
        if not self.based_function:
            return
        if len(self.args) == 1:
            try:
                return self.based_function(  # pylint: disable=not-callable
                    **{'narg': self.quantifier})
            except TypeError:
                return self.based_function()  # pylint: disable=not-callable

        args, keywords = list(), dict()
        for arg in self.args[1:]:
            equal_sign = arg.find("=")
            value = arg if (equal_sign is -1) else arg[equal_sign + 1:]
            try:
                value = int(value)
            except ValueError:
                if value in ('True', 'False'):
                    value = (value == 'True')
                else:
                    try:
                        value = float(value)
                    except ValueError:
                        pass

            if equal_sign == -1:
                args.append(value)
            else:
                keywords[arg[:equal_sign]] = value

        if self.quantifier is not None:
            keywords['narg'] = self.quantifier

        try:
            if self.quantifier is None:
                return self.based_function(*args, **keywords)  # pylint: disable=not-callable
            else:
                try:
                    return self.based_function(*args, **keywords)  # pylint: disable=not-callable
                except TypeError:
                    del keywords['narg']
                    return self.based_function(*args, **keywords)  # pylint: disable=not-callable
        except TypeError:
            if ranger.args.debug:
                raise
            else:
                self.fm.notify(
                    "Bad arguments for %s.%s: %s, %s" % (
                        self.object_name, self.function_name, repr(args), repr(keywords)),
                    bad=True,
                )


if __name__ == '__main__':
    import doctest
    doctest.testmod()
line.screen_line_starting_pos * Line_height end -- convert mx,my in pixels to schema-1 coordinates function Text.to_pos_on_line(line, mx, my) if line.screen_line_starting_pos == nil then return Text.nearest_cursor_pos(line.data, mx) end if line.fragments == nil then Text.compute_fragments(line, Line_width) end assert(my >= line.y) -- duplicate some logic from Text.draw local y = line.y for screen_line_index,screen_line_starting_pos in ipairs(line.screen_line_starting_pos) do local nexty = y + Line_height if my < nexty then -- On all wrapped screen lines but the final one, clicks past end of -- line position cursor on final character of screen line. -- (The final screen line positions past end of screen line as always.) if mx > Line_width and screen_line_index < #line.screen_line_starting_pos then return line.screen_line_starting_pos[screen_line_index+1] end local s = string.sub(line.data, screen_line_starting_pos) return screen_line_starting_pos + Text.nearest_cursor_pos(s, mx) - 1 end y = nexty end assert(false) end -- manual test: -- line: abc -- def -- gh -- fragments: abc, def, gh -- click inside e -- line_starting_pos = 1 + 3 = 4 -- nearest_cursor_pos('defgh', mx) = 2 -- Cursor1.pos = 4 + 2 - 1 = 5 -- manual test: -- click inside h -- line_starting_pos = 1 + 3 + 3 = 7 -- nearest_cursor_pos('gh', mx) = 2 -- Cursor1.pos = 7 + 2 - 1 = 8 function Text.nearest_cursor_pos(line, x) -- x includes left margin if x == 0 then return 1 end local len = utf8.len(line) local max_x = 25+Text.x(line, len+1) if x > max_x then return len+1 end local left, right = 1, len+1 --? print('-- nearest', x) while true do --? print('nearest', x, '^'..line..'$', left, right) if left == right then return left end local curr = math.floor((left+right)/2) local currxmin = 25+Text.x(line, curr) local currxmax = 25+Text.x(line, curr+1) --? print('nearest', x, left, right, curr, currxmin, currxmax) if currxmin <= x and x < currxmax then if x-currxmin < currxmax-x then return curr else return curr+1 end end if left >= right-1 then return right end if currxmin > x then right = curr else left = curr end end assert(false) end function Text.nearest_pos_less_than(line, x) -- x DOES NOT include left margin if x == 0 then return 1 end local len = utf8.len(line) local max_x = Text.x(line, len+1) if x > max_x then return len+1 end local left, right = 1, len+1 --? print('--') while true do local curr = math.floor((left+right)/2) local currxmin = Text.x(line, curr+1) local currxmax = Text.x(line, curr+2) --? print(x, left, right, curr, currxmin, currxmax) if currxmin <= x and x < currxmax then return curr end if left >= right-1 then return left end if currxmin > x then right = curr else left = curr end end assert(false) end function Text.x(s, pos) local offset = utf8.offset(s, pos) assert(offset) local s_before = s:sub(1, offset-1) local text_before = App.newText(love.graphics.getFont(), s_before) return App.width(text_before) end function Text.to2(pos1) if Lines[pos1.line].mode == 'drawing' then return {line=pos1.line, screen_line=1, screen_pos=1} end local result = {line=pos1.line, screen_line=1} if Lines[pos1.line].screen_line_starting_pos == nil then result.screen_pos = pos1.pos else for i=#Lines[pos1.line].screen_line_starting_pos,1,-1 do local spos = Lines[pos1.line].screen_line_starting_pos[i] if spos <= pos1.pos then result.screen_line = i result.screen_pos = pos1.pos - spos + 1 break end end end assert(result.screen_pos) return result end function Text.to1(pos2) local result = {line=pos2.line, pos=pos2.screen_pos} if pos2.screen_line > 1 then result.pos = Lines[pos2.line].screen_line_starting_pos[pos2.screen_line] + pos2.screen_pos - 1 end return result end function Text.eq1(a, b) return a.line == b.line and a.pos == b.pos end function Text.lt1(a, b) if a.line < b.line then return true end if a.line > b.line then return false end return a.pos < b.pos end function Text.le1(a, b) if a.line < b.line then return true end if a.line > b.line then return false end return a.pos <= b.pos end function Text.previous_screen_line(pos2) if pos2.screen_line > 1 then return {line=pos2.line, screen_line=pos2.screen_line-1, screen_pos=1} elseif pos2.line == 1 then return pos2 elseif Lines[pos2.line-1].mode == 'drawing' then return {line=pos2.line-1, screen_line=1, screen_pos=1} else local l = Lines[pos2.line-1] if l.screen_line_starting_pos == nil then return {line=pos2.line-1, screen_line=1, screen_pos=1} else return {line=pos2.line-1, screen_line=#Lines[pos2.line-1].screen_line_starting_pos, screen_pos=1} end end end function Text.populate_screen_line_starting_pos(line_index) --? print('Text.populate_screen_line_starting_pos') local line = Lines[line_index] if line.screen_line_starting_pos then return end -- duplicate some logic from Text.draw if line.fragments == nil then Text.compute_fragments(line, Line_width) end local x = 25 local pos = 1 for _, f in ipairs(line.fragments) do local frag, frag_text = f.data, f.text --? print(x, frag) -- render fragment local frag_width = App.width(frag_text) if x + frag_width > Line_width then x = 25 if line.screen_line_starting_pos == nil then line.screen_line_starting_pos = {1, pos} else --? print(' ', #line.screen_line_starting_pos, line.data) table.insert(line.screen_line_starting_pos, pos) end end x = x + frag_width local frag_len = utf8.len(frag) pos = pos + frag_len end end function Text.redraw_all() print('clearing fragments') for _,line in ipairs(Lines) do line.y = nil line.fragments = nil line.screen_line_starting_pos = nil end end return Text