Beispiel #1
0
def _all_words_in_file_completions(
        textwidget: tkinter.Text) -> List[Completion]:
    match = re.search(r'\w*$', textwidget.get('insert linestart', 'insert'))
    assert match is not None
    before_cursor = match.group(0)
    replace_start = textwidget.index(f'insert - {len(before_cursor)} chars')
    replace_end = textwidget.index('insert')

    counts = dict(
        collections.Counter([
            word for word in re.findall(r'\w+', textwidget.get('1.0', 'end'))
            if before_cursor.casefold() in word.casefold()
        ]))
    if counts.get(before_cursor, 0) == 1:
        del counts[before_cursor]

    return [
        Completion(
            display_text=word,
            replace_start=replace_start,
            replace_end=replace_end,
            replace_text=word,
            filter_text=word,
            documentation=word,
        ) for word in sorted(counts.keys(), key=counts.__getitem__)
    ]
Beispiel #2
0
def find_merge_conflicts(textwidget: tkinter.Text) -> List[List[int]]:
    result = []
    current_state = "outside"

    for lineno in range(1, int(textwidget.index("end - 1 char").split(".")[0]) + 1):
        line = textwidget.get(f"{lineno}.0", f"{lineno}.0 lineend")
        # Line might contain whitespace characters after '<<<<<<< '
        if line.startswith("<<<<<<< "):
            expected_current_state = "outside"
            new_state = "first"
        elif line == "=======":
            expected_current_state = "first"
            new_state = "second"
        elif line.startswith(">>>>>>> "):
            expected_current_state = "second"
            new_state = "outside"
        else:
            int("123")  # needed for coverage to notice that the continue runs
            continue

        if current_state != expected_current_state:
            # Something is funny. Maybe the file contains some things that make
            # it look like git merge conflict, but it really isn't that.
            return []

        current_state = new_state
        if new_state == "first":
            result.append([lineno])
        else:
            result[-1].append(lineno)

    if current_state == "outside":
        return result
    return []
Beispiel #3
0
def insert_char(text: tk.Text, char: str, raw: str = '', go=True):
    try:
        sel = text.get(tk.SEL_FIRST, tk.SEL_LAST)
    except tk.TclError:
        pass
    else:
        insert_text = raw + sel + char
        text.delete(tk.SEL_FIRST, tk.SEL_LAST)
        text.edit_separator()
        text.insert(tk.INSERT, insert_text)
        return 'break'
    index = str(text.index(tk.INSERT)).split('.')
    if text.get(f'{index[0]}.{int(index[1])}') == char:
        if char == raw:
            text.mark_set(tk.INSERT, f'{index[0]}.{int(index[1]) + 1}')
            text.see(tk.INSERT)
            return 'break'
    if raw:
        text.insert(tk.INSERT, raw)
        if (char != raw) or (char == '"') or char == "'":
            text.insert(tk.INSERT, char)
    if go:
        text.mark_set(tk.INSERT, f'{index[0]}.{int(index[1]) + 1}')
        text.see(tk.INSERT)
    return 'break'
Beispiel #4
0
def change_batch(widget: tkinter.Text) -> Iterator[None]:
    """A context manager to optimize doing many changes to a text widget.

    When :func:`track_changes` has been called, every change to a text widget
    generates a new ``<<ContentChanged>>`` event, and lots of
    ``<<ContentChanged>>`` events can cause Porcupine to run slowly. To avoid
    that, you can use this context manager during the changes, like this::

        with textwidget.change_batch(some_text_widget_with_change_tracking):
            for thing in big_list_of_things_to_do:
                textwidget.delete(...)
                textwidget.insert(...)

    This context manager also affects some other things, and so it can be
    useful even with text widgets that don't use :func:`track_changes`:

    * Undoing the whole batch is done with one Ctrl+Z press.
    * When the ``with`` statement ends, the cursor is moved back to where it
      was when the ``with`` statement started.

    See :source:`porcupine/plugins/indent_block.py` for a complete example.
    """
    cursor_pos = widget.index("insert")
    autoseparators_value = widget["autoseparators"]

    try:
        widget.config(autoseparators=False)
        widget.edit_separator()
        try:
            tracker = _change_trackers[widget]
        except KeyError:
            yield
        else:
            tracker.begin_batch()
            try:
                yield
            finally:
                tracker.finish_batch()
        widget.edit_separator()

    finally:
        widget.config(autoseparators=autoseparators_value)
        widget.mark_set("insert", cursor_pos)
Beispiel #5
0
def find_urls(text: tkinter.Text, start: str,
              end: str) -> Iterable[Tuple[str, str]]:
    match_ends_and_search_begins = start
    while True:
        match_start = text.search(r"\mhttps?://[a-z]",
                                  match_ends_and_search_begins,
                                  end,
                                  nocase=True,
                                  regexp=True)
        if not match_start:  # empty string means not found
            break

        url = text.get(match_start, f"{match_start} lineend")
        before_url = (None if text.index(match_start) == "1.0" else
                      text.get(f"{match_start} - 1 char"))

        # urls end on space or quote
        url = url.split(" ")[0]
        url = url.split("'")[0]
        url = url.split('"')[0]

        open2close = {"(": ")", "{": "}", "<": ">"}
        close2open = {")": "(", "}": "{", ">": "<"}

        if before_url in open2close and open2close[before_url] in url:
            # url is parenthesized
            url = url.split(open2close[before_url])[0]
        if url[-1] in close2open and close2open[url[-1]] not in url:
            # url isn't like "Bla(bla)" but ends with ")" or similar, assume that's not part of url
            url = url[:-1]

        # urls in middle of text: URL, and URL.
        url = url.rstrip(".,")

        match_ends_and_search_begins = f"{match_start} + {len(url)} chars"
        yield (match_start, match_ends_and_search_begins)
Beispiel #6
0
    def _change_event_from_command(self, widget: tkinter.Text, subcommand: str,
                                   *args_tuple: str) -> str:
        changes: List[Change] = []

        # search for 'pathName delete' in text(3tk)... it's a wall of text,
        # and this thing has to implement every detail of that wall
        if subcommand == "delete":
            # tk has a funny abstraction of an invisible newline character at
            # the end of file, it's always there but nothing else uses it, so
            # let's ignore it
            args = list(args_tuple)
            for index, old_arg in enumerate(args):
                if old_arg == widget.index("end"):
                    args[index] = widget.index("end - 1 char")

            # "If index2 is not specified then the single character at index1
            # is deleted." and later: "If more indices are given, multiple
            # ranges of text will be deleted." but no mention about combining
            # these features, this works like the text widget actually behaves
            if len(args) % 2 == 1:
                args.append(widget.index(f"{args[-1]} + 1 char"))
            assert len(args) % 2 == 0
            pairs = list(zip(args[0::2], args[1::2]))

            # "If index2 does not specify a position later in the text than
            # index1 then no characters are deleted."
            pairs = [(start, end) for (start, end) in pairs
                     if widget.compare(start, "<", end)]

            # "They [index pairs, aka ranges] are sorted [...]."
            # (line, column) tuples sort nicely
            def get_range_beginning_as_tuple(
                    start_and_end: Tuple[str, str]) -> Tuple[int, int]:
                line, column = map(int, start_and_end[0].split("."))
                return (line, column)

            pairs.sort(key=get_range_beginning_as_tuple)

            # "If multiple ranges with the same start index are given, then the
            # longest range is used. If overlapping ranges are given, then they
            # will be merged into spans that do not cause deletion of text
            # outside the given ranges due to text shifted during deletion."
            def merge_index_ranges(start1: str, end1: str, start2: str,
                                   end2: str) -> Tuple[str, str]:
                start = start1 if widget.compare(start1, "<",
                                                 start2) else start2
                end = end1 if widget.compare(end1, ">", end2) else end2
                return (start, end)

            # loop through pairs of pairs
            for i in range(len(pairs) - 2, -1, -1):
                (start1, end1), (start2, end2) = pairs[i:i + 2]
                if widget.compare(end1, ">=", start2):
                    # they overlap
                    new_pair = merge_index_ranges(start1, end1, start2, end2)
                    pairs[i:i + 2] = [new_pair]

            # "[...] and the text is removed from the last range to the first
            # range so deleted text does not cause an undesired index shifting
            # side-effects."
            for start, end in reversed(pairs):
                changes.append(self._create_change(widget, start, end, ""))

        # the man page's inserting section is also kind of a wall of
        # text, but not as bad as the delete
        elif subcommand == "insert":
            text_index, *other_args = args_tuple

            # "If index refers to the end of the text (the character after the
            # last newline) then the new text is inserted just before the last
            # newline instead."
            if text_index == widget.index("end"):
                text_index = widget.index("end - 1 char")

            # we don't care about the tagList arguments to insert, but we need
            # to handle the other arguments nicely anyway: "If multiple
            # chars-tagList argument pairs are present, they produce the same
            # effect as if a separate pathName insert widget command had been
            # issued for each pair, in order. The last tagList argument may be
            # omitted." i'm not sure what "in order" means here, but i tried
            # it, and 'textwidget.insert('1.0', 'asd', [], 'toot', [])' inserts
            # 'asdtoot', not 'tootasd'
            new_text = "".join(other_args[::2])

            changes.append(
                self._create_change(widget, text_index, text_index, new_text))

        # an even smaller wall of text that mostly refers to insert and replace
        elif subcommand == "replace":
            start, end, *other_args = args_tuple
            new_text = "".join(other_args[::2])

            # more invisible newline garbage
            if start == widget.index("end"):
                start = widget.index("end - 1 char")
            if end == widget.index("end"):
                end = widget.index("end - 1 char")

            # didn't find in docs, but tcl throws an error for this
            assert widget.compare(start, "<=", end)

            changes.append(self._create_change(widget, start, end, new_text))

        else:  # pragma: no cover
            raise ValueError(f"unexpected subcommand: {subcommand}")

        # remove changes that don't actually do anything
        changes = [
            change for change in changes if change.start != change.end
            or change.old_text_len != 0 or change.new_text
        ]

        if self._change_batch is None:
            return str(Changes(changes)) if changes else ""
        else:
            self._change_batch.extend(changes)
            return ""  # don't generate event
Beispiel #7
0
    def setup(self, widget: tkinter.Text) -> None:
        old_cursor_pos = widget.index("insert")  # must be widget specific

        def cursor_pos_changed() -> None:
            nonlocal old_cursor_pos

            new_pos = widget.index("insert")
            if new_pos == widget.index("end"):
                new_pos = widget.index("end - 1 char")

            if new_pos != old_cursor_pos:
                old_cursor_pos = new_pos
                widget.event_generate("<<CursorMoved>>")

        #       /\
        #      /  \  WARNING: serious tkinter magic coming up
        #     / !! \          proceed at your own risk
        #    /______\
        #
        # this irc conversation might give you an idea of how this works:
        #
        #    <Akuli> __Myst__, why do you want to know how it works?
        #    <__Myst__> Akuli: cause it seems cool
        #    <Akuli> there's 0 reason to docment it in the langserver
        #    <Akuli> ok i can explain :)
        #    <Akuli> in tcl, all statements are command calls
        #    <Akuli> set x lol    ;# set variable x to string lol
        #    <Akuli> set is a command, x and lol are strings
        #    <Akuli> adding stuff to widgets is also command calls
        #    <Akuli> .textwidget insert end hello   ;# add hello to the text
        #            widget
        #    <Akuli> my magic renames the textwidget command to
        #            actual_widget_command, and creates a fake text widget
        #            command that tkinter calls instead
        #    <Akuli> then this fake command checks for all possible widget
        #            commands that can move the cursor or change the content
        #    <Akuli> making sense?
        #    <__Myst__> ooh
        #    <__Myst__> so it's like you're proxying actual calls to the text
        #               widget and calculating change events based on that?
        #    <Akuli> yes
        #    <__Myst__> very cool

        # all widget stuff is implemented in python and in tcl as calls to a
        # tcl command named str(widget), and replacing that with a custom
        # command is a very powerful way to do magic; for example, moving the
        # cursor with arrow keys calls the 'mark set' widget command :D
        actual_widget_command = str(widget) + "_actual_widget"
        widget.tk.call("rename", str(widget), actual_widget_command)

        # this part is tcl because i couldn't get a python callback to work
        widget.tk.eval(
            """
        proc %(fake_widget)s {args} {
            # subcommand is e.g. insert, delete, replace, index, search, ...
            # see text(3tk) for all possible subcommands
            set subcommand [lindex $args 0]

            # issue #5: don't let the cursor to go to the very top or bottom of
            # the view
            if {$subcommand == "see"} {
                # cleaned_index is always a "LINE.COLUMN" string
                set cleaned_index [%(actual_widget)s index [lindex $args 1]]

                # from text(3tk): "If index is far out of view, then the
                # command centers index in the window." and we want to center
                # it correctly, so first go to the center, then a few
                # characters around it, and finally back to center because it
                # feels less error-prone that way
                %(actual_widget)s see $cleaned_index
                %(actual_widget)s see "$cleaned_index - 4 lines"
                %(actual_widget)s see "$cleaned_index + 4 lines"
                %(actual_widget)s see $cleaned_index
                return
            }

            # only these subcommands can change the text, but they can also
            # move the cursor by changing the text before the cursor
            if {$subcommand == "delete" || $subcommand == "insert" || $subcommand == "replace"} {
                # Validate and clean up indexes here so that any problems
                # result in Tcl error
                if {$subcommand == "delete"} {
                    for {set i 1} {$i < [llength $args]} {incr i} {
                        lset args $i [%(actual_widget)s index [lindex $args $i]]
                    }
                }
                if {$subcommand == "insert"} {
                    lset args 1 [%(actual_widget)s index [lindex $args 1]]
                }
                if {$subcommand == "replace"} {
                    lset args 1 [%(actual_widget)s index [lindex $args 1]]
                    lset args 2 [%(actual_widget)s index [lindex $args 2]]
                }
                set prepared_event [%(change_event_from_command)s {*}$args]
            } else {
                set prepared_event ""
            }

            # it's important that this comes after the change cb stuff because
            # this way it's possible to get old_length in self._change_cb()...
            # however, it's also important that this is before the mark set
            # stuff because the documented way to access the new index in a
            # <<CursorMoved>> binding is getting it directly from the widget
            set result [%(actual_widget)s {*}$args]

            if {$prepared_event != ""} {
                # must be after calling actual widget command
                event generate %(event_receiver)s <<ContentChanged>> -data $prepared_event
            }

            # only[*] 'textwidget mark set insert new_location' can change the
            # cursor position, because the cursor position is implemented as a
            # mark named "insert" and there are no other commands that move
            # marks
            #
            # [*] i lied, hehe >:D MUHAHAHA ... inserting text before the
            # cursor also changes it
            if {[lrange $args 0 2] == {mark set insert} || $prepared_event != ""} {
                %(cursor_moved_callback)s
            }

            return $result
        }
        """ % {
                "fake_widget":
                str(widget),
                "actual_widget":
                actual_widget_command,
                "change_event_from_command":
                widget.register(
                    partial(self._change_event_from_command, widget)),
                "event_receiver":
                self._event_receiver_widget,
                "cursor_moved_callback":
                widget.register(cursor_pos_changed),
            })
Beispiel #8
0
def _tag_spans_multiple_lines(textwidget: tkinter.Text, tag: str) -> bool:
    first_lineno: str = textwidget.index(f"{tag}.first")[0]
    last_lineno: str = textwidget.index(f"{tag}.last")[0]
    return first_lineno != last_lineno
Beispiel #9
0
def update_line_and_column(text_: tk.Text):
    line, column = text_.index("insert").split(
        ".")  # Row starts at 1 and column starts at 0
    message.set("Line: " + str(line) + " Column: " + str(int(column) + 1))
Beispiel #10
0
def daten_len(daten:tk.Text) -> int:
    return int(daten.index('end-1c').split('.')[0])