<commit_before># -*- encoding: utf-8
"""
Text viewer component subclassed by Editor.
"""

import os
import re
import sys
import curses
import logging
try:
    import importlib
except ImportError:
    importlib = False

from . import helpers

from .line import Line
from .cursor import Cursor
from .themes import scope_to_pair
import suplemon.linelight  # NOQA

try:
    import pygments.lexers
    from .lexer import Lexer
except ImportError:
    pygments = False


class BaseViewer:
    def __init__(self, app, window):
        """
        Handle Viewer initialization

        :param App app: The main App class of Suplemon
        :param Window window: The ui window to use for the viewer
        """
        self.app = app
        self.window = window
        self.logger = logging.getLogger(__name__)
        self.config = {}
        self.data = ""
        self.lines = [Line()]
        self.file_extension = ""

        # Map special extensions to generic ones for highlighting
        self.extension_map = {
            "scss": "css",
            "less": "css",
            "tmtheme": "xml",
            "ts": "js",
        }
        self.show_line_ends = True

        self.cursor_style = curses.A_UNDERLINE

        self.y_scroll = 0
        self.x_scroll = 0
        self.cursors = [Cursor()]

        # Copy/paste buffer
        self.buffer = []

        # Last search used in 'find'
        self.last_find = ""

        # Runnable methods
        self.operations = {
            "arrow_right": self.arrow_right,              # Arrow Right
            "arrow_left": self.arrow_left,                # Arrow Left
            "arrow_up": self.arrow_up,                    # Arrow Up
            "arrow_down": self.arrow_down,                # Arrow Down
            "jump_left": self.jump_left,                  # Ctrl + Left
            "jump_right": self.jump_right,                # Ctrl + Right
            "jump_up": self.jump_up,                      # Ctrl + Up
            "jump_down": self.jump_down,                  # Ctrl + Down
            "page_up": self.page_up,                      # Page Up
            "page_down": self.page_down,                  # Page Down
            "home": self.home,                            # Home
            "end": self.end,                              # End
            "find": self.find_query,                      # Ctrl + F
            "find_next": self.find_next,                  # Ctrl + D
            "find_all": self.find_all,                    # Ctrl + A
        }

        self.pygments_syntax = None  # Needs to be implemented in derived classes
        self.lexer = None  # Needs to be implemented in derived classes

    def init(self):
        pass

    def get_buffer(self):
        """Returns the current buffer.

        Returns the local buffer or the global buffer depending on config.
        """
        if self.config["use_global_buffer"]:
            return self.app.global_buffer
        else:
            return self.buffer

    def get_size(self):
        """Get editor size (x,y)."""
        y, x = self.window.getmaxyx()
        return (x, y)

    @property
    def scroll_pos(self):
        return self.y_scroll, self.x_scroll

    @scroll_pos.setter
    def scroll_pos(self, pos):
        self.y_scroll = pos[0]
        self.x_scroll = pos[1]

    def get_cursor(self):
        """Return the main cursor."""
        return self.cursors[0]

    def get_first_cursor(self):
        """Get the first (primary) cursor."""
        highest = None
        for cursor in self.cursors:
            if highest is None or cursor.y < highest.y:
                highest = cursor
        return highest

    def get_last_cursor(self):
        """Get the last cursor."""
        lowest = None
        for cursor in self.cursors:
            if lowest is None:
                lowest = cursor
            elif cursor.y > lowest.y:
                lowest = cursor
            elif cursor.y == lowest.y and cursor.x > lowest.x:
                lowest = cursor
        return lowest

    def get_cursors_on_line(self, line_no):
        """Return all cursors on a specific line."""
        cursors = []
        for cursor in self.cursors:
            if cursor.y == line_no:
                cursors.append(cursor)
        return cursors

    def get_line(self, n):
        """Return line at index n.

        :param n: Index of line to get.
        :return: The Line instance.
        """
        return self.lines[n]

    def get_lines_with_cursors(self):
        """Return all line indices that have cursors.

        :return: A list of line numbers that have cursors.
        :rtype: list
        """
        line_nums = []
        for cursor in self.cursors:
            if cursor.y not in line_nums:
                line_nums.append(cursor.y)
        line_nums.sort()
        return line_nums

    def get_data(self):
        """Get editor contents.

        :return: Editor contents.
        :rtype: str
        """
        str_lines = []
        for line in self.lines:
            if isinstance(line, str):
                str_lines.append(line)
            else:
                str_lines.append(line.get_data())
        data = str(self.config["end_of_line"].join(str_lines))
        return data

    def set_data(self, data):
        """Set editor data or contents.

        :param str data: Set the editor contents to data.
        """
        self.data = data
        self.lines = []
        lines = self.data.splitlines()
        # splitlines doesn't return the last line if it's empty so we add a line
        # if there we're no lines at all or if the data ends with a new line
        if not len(lines) or self.data.endswith(("\n", "\r\n", "\r")):
            lines.append(Line())
        for line in lines:
            self.lines.append(Line(line))

    def set_config(self, config):
        """Set the viewer configuration dict.

        :param dict config: Editor config dict with any supported fields. See config.py.
        """
        self.config = config
        self.set_cursor_style(self.config["cursor_style"])

    def set_cursor_style(self, cursor_style):
        """Set cursor style.

        :param str cursor_style: Cursor type, either 'underline' or 'reverse'.
        """
        if cursor_style == "underline":
            self.cursor_style = curses.A_UNDERLINE
        elif cursor_style == "reverse":
            self.cursor_style = curses.A_REVERSE
        else:
            return False
        return True

    def set_cursor(self, cursor):
        self.logger.warning("set_cursor is deprecated, use set_cursor_style instead.")
        return self.set_cursor_style(cursor)

    def set_cursors(self, cursors):
        """Return list of all cursors."""
        self.cursors = [Cursor(c) for c in cursors]

    def set_single_cursor(self, cursor):
        """Discard all cursors and place a new one."""
        self.cursors = [Cursor(cursor)]

    def setup_linelight(self):
        raise NotImplementedError("Needs to be implemented in derived classes")

    def setup_highlight(self):
        raise NotImplementedError("Needs to be implemented in derived classes")

    def set_file_extension(self, ext):
        """Set the file extension."""
        ext = ext.lower()
        if ext and ext != self.file_extension:
            self.file_extension = ext
            self.setup_linelight()
            if self.config["show_highlighting"]:
                self.setup_highlight()

    def add_cursor(self, cursor):
        """Add a new cursor. Accepts a x,y tuple or a Cursor instance."""
        self.cursors.append(Cursor(cursor))

    def pad_lnum(self, n):
        """Pad line number with zeroes."""
        # TODO: move to helpers
        s = str(n)
        while len(s) < self.line_offset()-1:
            s = "0" + s
        return s

    def max_line_length(self):
        """Get maximum line length that fits in the editor."""
        return self.get_size()[0]-self.line_offset()-1

    def line_offset(self):
        """Get the x coordinate of beginning of line."""
        if not self.config["show_line_nums"]:
            return 0
        return len(str(len(self.lines)))+1

    def toggle_line_nums(self):
        """Toggle display of line numbers."""
        self.config["show_line_nums"] = not self.config["show_line_nums"]
        self.render()

    def toggle_line_ends(self):
        """Toggle display of line ends."""
        self.show_line_ends = not self.show_line_ends
        self.render()

    def toggle_highlight(self):
        """Toggle syntax highlighting."""
        return False

    ###########################################################################
    # Curses
    ###########################################################################

    def move_win(self, yx):
        """Move the editor window to position yx."""
        # Must try & catch since mvwin might
        # crash with incorrect coordinates
        try:
            self.window.mvwin(yx[0], yx[1])
        except:
            self.logger.warning("Moving window failed!", exc_info=True)

    def refresh(self):
        """Refresh the editor curses window."""
        self.move_cursors()
        self.render()
        self.window.refresh()

    def resize(self, yx=None):
        """Resize the UI."""
        if not yx:
            yx = self.window.getmaxyx()
        self.window.resize(yx[0], yx[1])
        self.move_cursors()
        self.refresh()

    def render(self):
        """Render the editor curses window."""
        if self.app.block_rendering:
            return

        self.window.erase()
        max_y = self.get_size()[1]
        max_len = self.max_line_length()
        # Iterate through visible lines
        for i in range(max_y):
            x_offset = self.line_offset()
            lnum = i + self.y_scroll
            if lnum >= len(self.lines):  # Make sure we have a line to show
                break
            # Get line for current row
            line = self.lines[lnum]
            if self.config["show_line_nums"]:
                curs_color = curses.color_pair(line.number_color)
                self.window.addstr(i, 0, self.pad_lnum(lnum+1)+" ", curs_color)

            pos = (x_offset, i)
            try:
                self.render_line_contents(line, pos, x_offset, max_len)
            except:
                self.logger.error("Failed rendering line #{0} @{1} DATA:'{2}'!".format(lnum+1, pos, line),
                                  exc_info=True)
        self.render_cursors()

    def render_line_contents(self, line, pos, x_offset, max_len):
        """Render the contents of a line to the screen

        Renders a line to the screen with the appropriate rendering method
        based on settings.
<commit_msg>Replaces pad_lnum by native zfill<commit_after># -*- encoding: utf-8
"""
Text viewer component subclassed by Editor.
"""

import os
import re
import sys
import curses
import logging
try:
    import importlib
except ImportError:
    importlib = False

from . import helpers

from .line import Line
from .cursor import Cursor
from .themes import scope_to_pair
import suplemon.linelight  # NOQA

try:
    import pygments.lexers
    from .lexer import Lexer
except ImportError:
    pygments = False


class BaseViewer:
    def __init__(self, app, window):
        """
        Handle Viewer initialization

        :param App app: The main App class of Suplemon
        :param Window window: The ui window to use for the viewer
        """
        self.app = app
        self.window = window
        self.logger = logging.getLogger(__name__)
        self.config = {}
        self.data = ""
        self.lines = [Line()]
        self.file_extension = ""

        # Map special extensions to generic ones for highlighting
        self.extension_map = {
            "scss": "css",
            "less": "css",
            "tmtheme": "xml",
            "ts": "js",
        }
        self.show_line_ends = True

        self.cursor_style = curses.A_UNDERLINE

        self.y_scroll = 0
        self.x_scroll = 0
        self.cursors = [Cursor()]

        # Copy/paste buffer
        self.buffer = []

        # Last search used in 'find'
        self.last_find = ""

        # Runnable methods
        self.operations = {
            "arrow_right": self.arrow_right,              # Arrow Right
            "arrow_left": self.arrow_left,                # Arrow Left
            "arrow_up": self.arrow_up,                    # Arrow Up
            "arrow_down": self.arrow_down,                # Arrow Down
            "jump_left": self.jump_left,                  # Ctrl + Left
            "jump_right": self.jump_right,                # Ctrl + Right
            "jump_up": self.jump_up,                      # Ctrl + Up
            "jump_down": self.jump_down,                  # Ctrl + Down
            "page_up": self.page_up,                      # Page Up
            "page_down": self.page_down,                  # Page Down
            "home": self.home,                            # Home
            "end": self.end,                              # End
            "find": self.find_query,                      # Ctrl + F
            "find_next": self.find_next,                  # Ctrl + D
            "find_all": self.find_all,                    # Ctrl + A
        }

        self.pygments_syntax = None  # Needs to be implemented in derived classes
        self.lexer = None  # Needs to be implemented in derived classes

    def init(self):
        pass

    def get_buffer(self):
        """Returns the current buffer.

        Returns the local buffer or the global buffer depending on config.
        """
        if self.config["use_global_buffer"]:
            return self.app.global_buffer
        else:
            return self.buffer

    def get_size(self):
        """Get editor size (x,y)."""
        y, x = self.window.getmaxyx()
        return (x, y)

    @property
    def scroll_pos(self):
        return self.y_scroll, self.x_scroll

    @scroll_pos.setter
    def scroll_pos(self, pos):
        self.y_scroll = pos[0]
        self.x_scroll = pos[1]

    def get_cursor(self):
        """Return the main cursor."""
        return self.cursors[0]

    def get_first_cursor(self):
        """Get the first (primary) cursor."""
        highest = None
        for cursor in self.cursors:
            if highest is None or cursor.y < highest.y:
                highest = cursor
        return highest

    def get_last_cursor(self):
        """Get the last cursor."""
        lowest = None
        for cursor in self.cursors:
            if lowest is None:
                lowest = cursor
            elif cursor.y > lowest.y:
                lowest = cursor
            elif cursor.y == lowest.y and cursor.x > lowest.x:
                lowest = cursor
        return lowest

    def get_cursors_on_line(self, line_no):
        """Return all cursors on a specific line."""
        cursors = []
        for cursor in self.cursors:
            if cursor.y == line_no:
                cursors.append(cursor)
        return cursors

    def get_line(self, n):
        """Return line at index n.

        :param n: Index of line to get.
        :return: The Line instance.
        """
        return self.lines[n]

    def get_lines_with_cursors(self):
        """Return all line indices that have cursors.

        :return: A list of line numbers that have cursors.
        :rtype: list
        """
        line_nums = []
        for cursor in self.cursors:
            if cursor.y not in line_nums:
                line_nums.append(cursor.y)
        line_nums.sort()
        return line_nums

    def get_data(self):
        """Get editor contents.

        :return: Editor contents.
        :rtype: str
        """
        str_lines = []
        for line in self.lines:
            if isinstance(line, str):
                str_lines.append(line)
            else:
                str_lines.append(line.get_data())
        data = str(self.config["end_of_line"].join(str_lines))
        return data

    def set_data(self, data):
        """Set editor data or contents.

        :param str data: Set the editor contents to data.
        """
        self.data = data
        self.lines = []
        lines = self.data.splitlines()
        # splitlines doesn't return the last line if it's empty so we add a line
        # if there we're no lines at all or if the data ends with a new line
        if not len(lines) or self.data.endswith(("\n", "\r\n", "\r")):
            lines.append(Line())
        for line in lines:
            self.lines.append(Line(line))

    def set_config(self, config):
        """Set the viewer configuration dict.

        :param dict config: Editor config dict with any supported fields. See config.py.
        """
        self.config = config
        self.set_cursor_style(self.config["cursor_style"])

    def set_cursor_style(self, cursor_style):
        """Set cursor style.

        :param str cursor_style: Cursor type, either 'underline' or 'reverse'.
        """
        if cursor_style == "underline":
            self.cursor_style = curses.A_UNDERLINE
        elif cursor_style == "reverse":
            self.cursor_style = curses.A_REVERSE
        else:
            return False
        return True

    def set_cursor(self, cursor):
        self.logger.warning("set_cursor is deprecated, use set_cursor_style instead.")
        return self.set_cursor_style(cursor)

    def set_cursors(self, cursors):
        """Return list of all cursors."""
        self.cursors = [Cursor(c) for c in cursors]

    def set_single_cursor(self, cursor):
        """Discard all cursors and place a new one."""
        self.cursors = [Cursor(cursor)]

    def setup_linelight(self):
        raise NotImplementedError("Needs to be implemented in derived classes")

    def setup_highlight(self):
        raise NotImplementedError("Needs to be implemented in derived classes")

    def set_file_extension(self, ext):
        """Set the file extension."""
        ext = ext.lower()
        if ext and ext != self.file_extension:
            self.file_extension = ext
            self.setup_linelight()
            if self.config["show_highlighting"]:
                self.setup_highlight()

    def add_cursor(self, cursor):
        """Add a new cursor. Accepts a x,y tuple or a Cursor instance."""
        self.cursors.append(Cursor(cursor))

    def max_line_length(self):
        """Get maximum line length that fits in the editor."""
        return self.get_size()[0]-self.line_offset()-1

    def line_offset(self):
        """Get the x coordinate of beginning of line."""
        if not self.config["show_line_nums"]:
            return 0
        return len(str(len(self.lines)))+1

    def toggle_line_nums(self):
        """Toggle display of line numbers."""
        self.config["show_line_nums"] = not self.config["show_line_nums"]
        self.render()

    def toggle_line_ends(self):
        """Toggle display of line ends."""
        self.show_line_ends = not self.show_line_ends
        self.render()

    def toggle_highlight(self):
        """Toggle syntax highlighting."""
        return False

    ###########################################################################
    # Curses
    ###########################################################################

    def move_win(self, yx):
        """Move the editor window to position yx."""
        # Must try & catch since mvwin might
        # crash with incorrect coordinates
        try:
            self.window.mvwin(yx[0], yx[1])
        except:
            self.logger.warning("Moving window failed!", exc_info=True)

    def refresh(self):
        """Refresh the editor curses window."""
        self.move_cursors()
        self.render()
        self.window.refresh()

    def resize(self, yx=None):
        """Resize the UI."""
        if not yx:
            yx = self.window.getmaxyx()
        self.window.resize(yx[0], yx[1])
        self.move_cursors()
        self.refresh()

    def render(self):
        """Render the editor curses window."""
        if self.app.block_rendering:
            return

        self.window.erase()
        max_y = self.get_size()[1]
        max_len = self.max_line_length()
        # Iterate through visible lines
        for i in range(max_y):
            x_offset = self.line_offset()
            lnum = i + self.y_scroll
            if lnum >= len(self.lines):  # Make sure we have a line to show
                break
            # Get line for current row
            line = self.lines[lnum]
            if self.config["show_line_nums"]:
                curs_color = curses.color_pair(line.number_color)
                padded_num = str(lnum+1).zfill(self.line_offset()-1)
                self.window.addstr(i, 0, padded_num+" ", curs_color)

            pos = (x_offset, i)
            try:
                self.render_line_contents(line, pos, x_offset, max_len)
            except:
                self.logger.error("Failed rendering line #{0} @{1} DATA:'{2}'!".format(lnum+1, pos, line),
                                  exc_info=True)
        self.render_cursors()

    def render_line_contents(self, line, pos, x_offset, max_len):
        """Render the contents of a line to the screen

        Renders a line to the screen with the appropriate rendering method
        based on settings.

{'fraction_non_alphanumeric': 0.11208103818504342, 'fraction_numerical': 0.0021006441975539164, 'mean_word_length': 1.8014907806983131, 'pattern_counts': {'":': 42, '<': 6, '<?xml version=': 0, '>': 9, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 2}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5678852', 'n_tokens_mistral': 6146, 'n_tokens_neox': 5826, 'n_words': 1742}