Exemple #1
0
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.sublime_helper = SublimeHelper(self.view)
        self.timesheet_helper = TimesheetHelper(self.sublime_helper)

        self.regions_to_highlight = []
Exemple #2
0
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.sublime_helper = SublimeHelper(self.view)
        self.timesheet_helper = TimesheetHelper(self.sublime_helper)

        # Remember if current view contains timesheet file.
        # This needed to save resources and don't process non-timesheet files.
        self.is_timesheet = None
Exemple #3
0
    def setUp(self):
        # Create view that will be used by tests
        self.view = sublime.active_window().new_file()

        self.sublime_helper = SublimeHelper(self.view)
        self.timesheet_helper = TimesheetHelper(self.sublime_helper)
Exemple #4
0
class DuplicateTimesheetLineCommand(sublime_plugin.TextCommand):
    """
    Duplicate timesheet line under cursor,
    insert it into appropriate place,
    fill time_from and/or time_to fields.
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.sublime_helper = SublimeHelper(self.view)
        self.timesheet_helper = TimesheetHelper(self.sublime_helper)

        self.regions_to_highlight = []

    def run(self, edit: sublime.Edit, copy_issue_and_comment: bool = True):
        """Entry-point, called when command is executed."""

        # Extract timesheet info from line under cursor
        timesheet_info = self.timesheet_helper.extract_timesheet_info(
            self.sublime_helper.get_current_line_content())

        # Do nothing if current line is not a timesheet line
        if not timesheet_info:
            self.view.window().status_message(
                'No valid timesheet line under cursor to duplicate')
            return

        # Fill time_to if needed,
        # remember it to later fill time_from of new timesheet line
        new_time_to = self.fill_time_to(edit)

        new_time_to = new_time_to or self.floor_time(datetime.now())

        # Get last lines
        last_non_empty_line = self.get_last_non_empty_line_region()
        last_timesheet_line = self.get_last_timesheet_line_region()

        last_timesheet_line_info = self.timesheet_helper.extract_timesheet_info(
            self.view.substr(last_timesheet_line))

        # Determine line after which new timesheet line should be inserted,
        # and if extra new line is needed.
        # Extra new line divides timesheet lines of different days,
        # or acts like separator from latest comment
        if self.timesheet_helper.is_today(last_timesheet_line_info):
            insert_after_line = last_timesheet_line
            extra_newline = False
        else:
            insert_after_line = last_non_empty_line
            extra_newline = True

        # Compose new timesheet line content
        new_timesheet_line_content = '\n{},{},{},{},{}'.format(
            new_time_to.strftime('%Y-%m-%d'),
            new_time_to.strftime('%H:%M'),
            ' ' * len('12:00'),
            timesheet_info['issue'][1] if copy_issue_and_comment else '',
            timesheet_info['comment'] if copy_issue_and_comment else '',
        )

        # Insert extra new line if needed
        if extra_newline:
            self.view.insert(edit, insert_after_line.end(), '\n')
            self.regions_to_highlight.append(
                sublime.Region(insert_after_line.end() + len('\n'),
                               insert_after_line.end() + len('\n') +
                               len('\n')))
            insert_after_line = self.view.line(insert_after_line.end() + 1)

        # Insert new line
        self.view.insert(edit, insert_after_line.end(),
                         new_timesheet_line_content)

        added_line_region = sublime.Region(
            insert_after_line.end() + len('\n'),
            insert_after_line.end() + len('\n') +
            len(new_timesheet_line_content),
        )

        self.regions_to_highlight.append(added_line_region)

        # Move cursor to new inserted timesheet line,
        # keep column position same as original cursor
        row, column = self.view.rowcol(self.view.sel()[0].begin())
        self.view.sel().clear()
        self.view.sel().add(insert_after_line.end() + len('\n') + column)

        self.view.show(added_line_region)

        self.highlight_regions()

        self.view.window().status_message('Added new timesheet line')

    def is_visible(self, *args, **kwargs):
        """
        Called when context menu is appeared.
        Return True if current line has ticket info
        and Duplicate Timesheet Line menu item should appear.
        Return False otherwise.
        """
        return self.timesheet_helper.is_valid_timesheet_under_cursor()

    def fill_time_to(self, edit: sublime.Edit) -> Optional[datetime]:
        """
        If latest timesheet line is today and has empty time_to field,
        fill it with current time. Otherwise return time_to value.
        If there is no timesheet line, return None.
        """

        last_timesheet_line_region = self.get_last_timesheet_line_region()
        last_timesheet_line_info = self.timesheet_helper.extract_timesheet_info(
            self.view.substr(last_timesheet_line_region))

        # If latest timesheet line isn't today, do nothing
        if not self.timesheet_helper.is_today(last_timesheet_line_info):
            return

        # If time_to already filled, return it
        if last_timesheet_line_info['to_dt']:
            return last_timesheet_line_info['to_dt']

        # Find start position of time_to field (it's fixed)
        time_to_start = len('2000-01-01,12:00,')

        # Find end position of time_to field
        # which could be any number of space chars
        pos = last_timesheet_line_region.begin() + time_to_start
        while pos < last_timesheet_line_region.end():
            if self.view.substr(pos) == ',':
                break
            pos += 1

        new_time_to = self.floor_time(datetime.now())

        # Replace empty time_to with current time
        self.view.replace(
            edit,
            sublime.Region(last_timesheet_line_region.begin() + time_to_start,
                           pos), new_time_to.strftime('%H:%M'))

        # Add modified piece of line to highlight
        self.regions_to_highlight.append(
            sublime.Region(
                last_timesheet_line_region.begin() + time_to_start,
                last_timesheet_line_region.begin() + time_to_start +
                len('12:00'),
            ))

        return new_time_to

    def highlight_regions(self):
        """
        Highlight new inserted content so user knows what was modified.
        Also remove that highlight after short time so it won't bother user.
        """
        # For each inserted line generate unique key
        regions_key = 'timesheets-{}'.format(unixtime())

        # Highlight regions
        self.view.add_regions(regions_key, self.regions_to_highlight, 'text',
                              'dot', sublime.DRAW_NO_FILL)

        self.regions_to_highlight.clear()

        # Schedule regions clear
        sublime.set_timeout(lambda: self.view.erase_regions(regions_key), 750)

    def get_last_non_empty_line_region(self) -> Optional[sublime.Region]:
        """
        Return line region of latest non-empty string (including comments).
        """
        for line_region in self.sublime_helper.iter_lines_regions_reversed():
            line_content = self.view.substr(line_region)

            if line_content.strip():
                return line_region

    def get_last_timesheet_line_region(self) -> Optional[sublime.Region]:
        """Return line region of latest timesheet line (excluding comments)."""
        for line_region in self.sublime_helper.iter_lines_regions_reversed():
            line_content = self.view.substr(line_region)

            if self.timesheet_helper.extract_timesheet_info(line_content):
                return line_region

    def floor_time(self, dt: datetime) -> datetime:
        """
        Return earliest time to given `dt` with rounding to 10 minutes.
        E.g.:
        12:00 -> 12:00
        12:01 -> 12:00
        12:09 -> 12:00
        """
        return datetime.combine(dt.date(), time(dt.hour, dt.minute // 10 * 10))

    def ceil_time(self, dt: datetime) -> datetime:
        """
        Return closest time in future to given `dt` with rounding to 10 minutes.
        E.g.:
        12:00 -> 12:10
        12:01 -> 12:10
        12:09 -> 12:10
        """
        return self.floor_time(dt) + timedelta(minutes=10)
Exemple #5
0
class GotoTicketCommand(sublime_plugin.TextCommand):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.sublime_helper = SublimeHelper(self.view)
        self.timesheet_helper = TimesheetHelper(self.sublime_helper)

    def run(self, edit):
        """Entry-point, called when command is executed"""
        line_content = self.sublime_helper.get_current_line_content()
        timesheet_info = self.timesheet_helper.extract_timesheet_info(
            line_content)

        if timesheet_info:
            ticket_url = self.generate_ticket_url(timesheet_info)

            if ticket_url:
                self.view.window().status_message(
                    'Opening {}'.format(ticket_url))
                webbrowser.open(ticket_url)
        else:
            self.view.window().status_message(
                "Can't find ticket in current line")

    def is_visible(self, *args) -> bool:
        """
        Called when context menu is appeared.
        Return True if current line has ticket info
        and Goto Ticket menu item should appear.
        Return False otherwise.
        """
        return self.timesheet_helper.is_valid_timesheet_under_cursor()

    def generate_ticket_url(self, timesheet_info: dict) -> Optional[str]:
        """
        Generate and return ticket URL to given `ticket_info`.
        If required setting isn't set, or doesn't have template,
        show error message and return None.
        """
        error_prefix = 'Timesheets plugin'

        bug_tracker, ticket_id = timesheet_info['issue']

        settings = sublime.load_settings('timesheets.sublime-settings')

        option_name = '{}_ticket_url'.format(bug_tracker)
        ticket_url_template = settings.get(option_name)

        if not ticket_url_template:
            sublime.error_message('{}: in order to open ticket in browser '
                                  'please specify valid "{}" option'.format(
                                      error_prefix, option_name))
            return

        ticket_url = ticket_url_template.format(ticket_id)

        # If formatted template and raw template are the same,
        # looks like template missing placeholder ("{}")
        if ticket_url == ticket_url_template:
            sublime.error_message('{}: in order to open ticket in browser '
                                  'please specify valid "{}" option'.format(
                                      error_prefix, option_name))
            return

        return ticket_url
Exemple #6
0
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.sublime_helper = SublimeHelper(self.view)
        self.timesheet_helper = TimesheetHelper(self.sublime_helper)