thatsIch/sublime-rainmeter

View on GitHub
completion/completion.py

Summary

Maintainability
A
2 hrs
Test Coverage
"""This module handles the whole auto smart completion sublime-rainmeter has to offer."""

# python libs
import re

# st libs
import sublime

# own libs
from .. import logger
from .skin.rainmeter_section import SkinRainmeterSectionAutoComplete
from .skin.metadata_section import SkinMetadataSectionAutoComplete
from .section import SkinSectionAutoCompleter


class ContextSensAutoCompletion(object):
    """
    This represents the internal implementation for the contextual auto completion.

    It uses smart environmental information like section, key, values etc
    to provide smarter auto completion suggestions.
    """

    # only show our completion list because nothing else makes sense in this context
    flags = sublime.INHIBIT_EXPLICIT_COMPLETIONS | sublime.INHIBIT_WORD_COMPLETIONS

    section = None
    skin_rainmeter_section = None
    skin_metadata_section = None

    scope = "source.rainmeter"

    # comments are specified by ';'
    comment_exp = re.compile(r'^\s*;.*')

    # enable searching for [] in multiline environment
    bracket_expression = re.compile(r'^\s*(\[.+\])\s*$', re.MULTILINE)
    section_expression = re.compile(r'^\s*\[(.+)\]\s*$', re.I)
    key_expression = re.compile(r'^\s*(.+)\s*=?\s*(.*?)\s*$', re.MULTILINE)
    key_value_expression = re.compile(r'^\s*(.+?)\s*=\s*(.*?)\s*$', re.MULTILINE)

    def __init__(self):
        """Initialize the different completer components."""
        self.section = SkinSectionAutoCompleter()
        self.skin_rainmeter_section = SkinRainmeterSectionAutoComplete()
        self.skin_metadata_section = SkinMetadataSectionAutoComplete()

    def get_lines_of_section_on_cursor(self, view, location):
        """Determine content of current section."""
        size = view.size()
        start_content = view.substr(sublime.Region(0, location))
        end_content = view.substr(sublime.Region(location, size))

        start_index = self.get_start_index_of_section(start_content)
        end_index = self.get_end_index_of_section(end_content, location, size)

        section = view.substr(sublime.Region(start_index, end_index))
        lines = section.splitlines()

        return start_index, lines

    def get_start_index_of_section(self, start_content):
        """
        Return the index of the section.

        If no section is found the first index (0) is returned
        """
        matches = list(self.bracket_expression.finditer(start_content))

        if len(matches) > 0:
            last_match = matches[-1]
            return last_match.start()

        # no previous section found, hardly legal but who cares
        else:
            return 0

    def get_end_index_of_section(self, end_content, offset, end_index):
        """
        Return the index of the next section.

        If no next section is found the last index is returned given through the param end_index
        """
        matches = list(self.bracket_expression.finditer(end_content))
        if len(matches) > 0:
            first_match = matches[0]
            return first_match.start() + offset

        # no next section found
        else:
            return end_index

    def get_key_value(self, line_content):
        """
        Extract the key and/or value in a line if existing.

        This is used if a specific completion is only shown
        on special conditions like only show X on measures.

        If nothing is given return (None, None)
        """
        key_value_match = self.key_value_expression.search(line_content)
        if key_value_match:
            key_match = key_value_match.group(1)
            value_match = key_value_match.group(2)
            logger.info(
                "key/value found in '" + line_content +
                "' with ('" + key_match + "', '" + value_match + "')"
            )

            return key_match, value_match

        key_only_match = self.key_expression.search(line_content)
        if key_only_match:
            logger.info("potential key found in '" + line_content + "'")
            return key_only_match.group(1), None

        return None, None

    def get_key_values(self, lines):
        """Extract all key values in the given lines."""
        key_values = []

        for line in lines:
            key, value = self.get_key_value(line)
            if key:
                key_values.append((key, value))

        return key_values

    def __autocomplete_value(self, line_content, section):
        key_match, value_match = self.get_key_value(line_content)

        if value_match == "":
            logger.info("auto-completing value: cursor after equal trigger in '" + line_content + "'")
            # value trigger
            return self.skin_rainmeter_section.get_value_context_completion(
                section,
                key_match
            )

    def __autocomplete_key(self, line_content, prefix, section, key_values):
        # only do key completion if we are in the key are
        # that means in front of the equal or no equal at all
        logger.info("before equal trigger in '" + line_content + "'")
        key_result = self.skin_rainmeter_section.get_key_context_completion(
            prefix,
            line_content,
            section,
            key_values
        )

        return key_result

    @classmethod
    def __autocomplete_default(cls):
        return None

    def on_query_completions(self, view, prefix, locations):
        """
        Execute if a auto completion is requested.

        can be either via typing or manual invoked with ctrl+space.

        This provides general variable extractions which are then
        passed to the domain specific worker completions.
        """
        for location in locations:
            # ignore non scope
            if not view.match_selector(location, self.scope):
                return None

            # ignore on comment lines
            cursor_line = view.line(location)
            line_content = view.substr(cursor_line)
            if self.comment_exp.search(line_content):
                logger.info("found comment")
                return None

            # find last occurance of the [] to determine the ini sections
            section_start_index, section_lines = self.get_lines_of_section_on_cursor(view, location)

            # determine auto completions of sections on double newline
            line_start_coord = cursor_line.begin()
            row, _ = view.rowcol(line_start_coord)
            section_start = view.rowcol(section_start_index)
            section_start_row, _ = section_start

            if row > 2:
                line_above_empty = section_lines[row - section_start_row - 1] == ''
                line_aabove_empty = section_lines[row - section_start_row - 2] == ''

                if line_above_empty and line_aabove_empty:
                    size = view.size()
                    content = view.substr(sublime.Region(0, size))
                    sections = self.bracket_expression.findall(content)

                    return self.section.get_key_context_completion(prefix, line_content, sections)

            # filter empty lines
            section_lines = [section_line for section_line in section_lines if section_line]
            # filter comments
            section_lines = [line for line in section_lines if not self.comment_exp.search(line)]

            if not section_lines:
                logger.info("section is empty")
                size = view.size()
                content = view.substr(sublime.Region(0, size))
                sections = self.bracket_expression.findall(content)

                return self.section.get_key_context_completion(prefix, line_content, sections)

            # extract section
            first_line = section_lines[0]
            match = self.section_expression.search(first_line)

            section = match.group(1)
            key_values = self.get_key_values(section_lines)

            return self.__autocomplete_value(line_content, section) or \
                self.__autocomplete_key(line_content, prefix, section, key_values) or \
                self.__autocomplete_default()