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__) ]
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 []
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'
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)
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)
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
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), })
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
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))
def daten_len(daten:tk.Text) -> int: return int(daten.index('end-1c').split('.')[0])