<commit_before>"""Find and replace text."""
# FIXME: finding 'as' or 'asa' from 'asasasasa' is broken
from __future__ import annotations

import re
import sys
import tkinter
import weakref
from functools import partial
from tkinter import ttk
from typing import Any, Iterator, List, Tuple, cast

if sys.version_info >= (3, 8):
    from typing import Literal
else:
    from typing_extensions import Literal

from porcupine import get_tab_manager, images, menubar, tabs

finders: weakref.WeakKeyDictionary[tabs.FileTab, Finder] = weakref.WeakKeyDictionary()


class Finder(ttk.Frame):
    """A widget for finding and replacing text.

    Use the pack geometry manager with this widget.
    """

    def __init__(self, parent: tkinter.Misc, textwidget: tkinter.Text, **kwargs: Any) -> None:
        super().__init__(parent, **kwargs)
        self._textwidget = textwidget

        # grid layout:
        #         column 0        column 1     column 2        column 3
        #     ,---------------------------------------------------------------.
        # row0|     Find:     | text entry    |       | [x] Full words only   |
        #     |---------------|---------------|-------|-----------------------|
        # row1| Replace with: | text entry    |       | [x] Ignore case       |
        #     |---------------------------------------------------------------|
        # row2| button frame, this thing contains a bunch of buttons          |
        #     |---------------------------------------------------------------|
        # row3| status label with useful-ish text                             |
        #     |---------------------------------------------------------------|
        # row4| separator                                                     |
        #     `---------------------------------------------------------------'
        #
        # note that column 2 is used just for spacing, the separator helps
        # distinguish this from e.g. status bar below this
        self.grid_columnconfigure(2, minsize=30)
        self.grid_columnconfigure(3, weight=1)

        self.full_words_var = tkinter.BooleanVar()
        self.ignore_case_var = tkinter.BooleanVar()
        find_var = tkinter.StringVar()

        self.find_entry = self._add_entry(0, "Find:")
        self.find_entry.config(textvariable=find_var)
        find_var.trace_add('write', self.highlight_all_matches)

        # because cpython gc
        cast(Any, self.find_entry).lol = find_var

        self.replace_entry = self._add_entry(1, "Replace with:")

        self.find_entry.bind('<Shift-Return>', self._go_to_previous_match, add=True)
        self.find_entry.bind('<Return>', self._go_to_next_match, add=True)

        buttonframe = ttk.Frame(self)
        buttonframe.grid(row=2, column=0, columnspan=4, sticky='we')

        self.previous_button = ttk.Button(buttonframe, text="Previous match",
                                          command=self._go_to_previous_match)
        self.next_button = ttk.Button(buttonframe, text="Next match",
                                      command=self._go_to_next_match)
        self.replace_this_button = ttk.Button(
            buttonframe, text="Replace this match", underline=len("Replace "),
            command=self._replace_this)
        self.replace_all_button = ttk.Button(
            buttonframe, text="Replace all", underline=len("Replace "),
            command=self._replace_all)

        self.previous_button.pack(side='left')
        self.next_button.pack(side='left')
        self.replace_this_button.pack(side='left')
        self.replace_all_button.pack(side='left')
        self._update_buttons()

        self.full_words_var.trace_add('write', self.highlight_all_matches)
        self.ignore_case_var.trace_add('write', self.highlight_all_matches)

        ttk.Checkbutton(
            self, text="Full words only", underline=0, variable=self.full_words_var
        ).grid(row=0, column=3, sticky='w')
        ttk.Checkbutton(
            self, text="Ignore case", underline=0, variable=self.ignore_case_var
        ).grid(row=1, column=3, sticky='w')

        self.statuslabel = ttk.Label(self)
        self.statuslabel.grid(row=3, column=0, columnspan=4, sticky='we')

        ttk.Separator(self, orient='horizontal').grid(
            row=4, column=0, columnspan=4, sticky='we')

        closebutton = ttk.Label(self, cursor='hand2')
        closebutton.place(relx=1, rely=0, anchor='ne')
        closebutton.bind('<Button-1>', self.hide, add=True)

        closebutton.config(image=images.get('closebutton'))

        # explained in test_find_plugin.py
        textwidget.bind('<<Selection>>', self._update_buttons, add=True)

        textwidget.bind('<<SettingChanged:pygments_style>>', self._config_tags, add=True)
        self._config_tags()

    def _config_tags(self, junk: object = None) -> None:
        # TODO: use more pygments theme instead of hard-coded colors?
        self._textwidget.tag_config(
            'find_highlight', foreground='black', background='yellow')
        self._textwidget.tag_config(
            'find_highlight_selected', foreground='black', background='orange')
        self._textwidget.tag_raise('find_highlight', 'sel')
        self._textwidget.tag_raise('find_highlight_selected', 'find_highlight')

    def _add_entry(self, row: int, text: str) -> ttk.Entry:
        ttk.Label(self, text=text).grid(row=row, column=0, sticky='w')
        entry = ttk.Entry(self, width=35, font='TkFixedFont')
        entry.bind('<Escape>', self.hide, add=True)
        entry.bind('<Alt-t>', self._replace_this, add=True)
        entry.bind('<Alt-a>', self._replace_all, add=True)
        entry.bind('<Alt-f>', partial(self._toggle_var, self.full_words_var), add=True)
        entry.bind('<Alt-i>', partial(self._toggle_var, self.ignore_case_var), add=True)
        entry.grid(row=row, column=1, sticky='we')
        return entry

    def _toggle_var(self, var: tkinter.BooleanVar, junk: object) -> Literal['break']:
        var.set(not var.get())
        return 'break'

    def show(self) -> None:
        try:
            selected_text = self._textwidget.get('sel.first', 'sel.last')
        except tkinter.TclError:
            selected_text = None  # A little weird, but gets the job done.
<commit_msg>fix mypy<commit_after>"""Find and replace text."""
# FIXME: finding 'as' or 'asa' from 'asasasasa' is broken
from __future__ import annotations

import re
import sys
import tkinter
import weakref
from functools import partial
from tkinter import ttk
from typing import Any, Iterator, List, Optional, Tuple, cast

if sys.version_info >= (3, 8):
    from typing import Literal
else:
    from typing_extensions import Literal

from porcupine import get_tab_manager, images, menubar, tabs

finders: weakref.WeakKeyDictionary[tabs.FileTab, Finder] = weakref.WeakKeyDictionary()


class Finder(ttk.Frame):
    """A widget for finding and replacing text.

    Use the pack geometry manager with this widget.
    """

    def __init__(self, parent: tkinter.Misc, textwidget: tkinter.Text, **kwargs: Any) -> None:
        super().__init__(parent, **kwargs)
        self._textwidget = textwidget

        # grid layout:
        #         column 0        column 1     column 2        column 3
        #     ,---------------------------------------------------------------.
        # row0|     Find:     | text entry    |       | [x] Full words only   |
        #     |---------------|---------------|-------|-----------------------|
        # row1| Replace with: | text entry    |       | [x] Ignore case       |
        #     |---------------------------------------------------------------|
        # row2| button frame, this thing contains a bunch of buttons          |
        #     |---------------------------------------------------------------|
        # row3| status label with useful-ish text                             |
        #     |---------------------------------------------------------------|
        # row4| separator                                                     |
        #     `---------------------------------------------------------------'
        #
        # note that column 2 is used just for spacing, the separator helps
        # distinguish this from e.g. status bar below this
        self.grid_columnconfigure(2, minsize=30)
        self.grid_columnconfigure(3, weight=1)

        self.full_words_var = tkinter.BooleanVar()
        self.ignore_case_var = tkinter.BooleanVar()
        find_var = tkinter.StringVar()

        self.find_entry = self._add_entry(0, "Find:")
        self.find_entry.config(textvariable=find_var)
        find_var.trace_add('write', self.highlight_all_matches)

        # because cpython gc
        cast(Any, self.find_entry).lol = find_var

        self.replace_entry = self._add_entry(1, "Replace with:")

        self.find_entry.bind('<Shift-Return>', self._go_to_previous_match, add=True)
        self.find_entry.bind('<Return>', self._go_to_next_match, add=True)

        buttonframe = ttk.Frame(self)
        buttonframe.grid(row=2, column=0, columnspan=4, sticky='we')

        self.previous_button = ttk.Button(buttonframe, text="Previous match",
                                          command=self._go_to_previous_match)
        self.next_button = ttk.Button(buttonframe, text="Next match",
                                      command=self._go_to_next_match)
        self.replace_this_button = ttk.Button(
            buttonframe, text="Replace this match", underline=len("Replace "),
            command=self._replace_this)
        self.replace_all_button = ttk.Button(
            buttonframe, text="Replace all", underline=len("Replace "),
            command=self._replace_all)

        self.previous_button.pack(side='left')
        self.next_button.pack(side='left')
        self.replace_this_button.pack(side='left')
        self.replace_all_button.pack(side='left')
        self._update_buttons()

        self.full_words_var.trace_add('write', self.highlight_all_matches)
        self.ignore_case_var.trace_add('write', self.highlight_all_matches)

        ttk.Checkbutton(
            self, text="Full words only", underline=0, variable=self.full_words_var
        ).grid(row=0, column=3, sticky='w')
        ttk.Checkbutton(
            self, text="Ignore case", underline=0, variable=self.ignore_case_var
        ).grid(row=1, column=3, sticky='w')

        self.statuslabel = ttk.Label(self)
        self.statuslabel.grid(row=3, column=0, columnspan=4, sticky='we')

        ttk.Separator(self, orient='horizontal').grid(
            row=4, column=0, columnspan=4, sticky='we')

        closebutton = ttk.Label(self, cursor='hand2')
        closebutton.place(relx=1, rely=0, anchor='ne')
        closebutton.bind('<Button-1>', self.hide, add=True)

        closebutton.config(image=images.get('closebutton'))

        # explained in test_find_plugin.py
        textwidget.bind('<<Selection>>', self._update_buttons, add=True)

        textwidget.bind('<<SettingChanged:pygments_style>>', self._config_tags, add=True)
        self._config_tags()

    def _config_tags(self, junk: object = None) -> None:
        # TODO: use more pygments theme instead of hard-coded colors?
        self._textwidget.tag_config(
            'find_highlight', foreground='black', background='yellow')
        self._textwidget.tag_config(
            'find_highlight_selected', foreground='black', background='orange')
        self._textwidget.tag_raise('find_highlight', 'sel')
        self._textwidget.tag_raise('find_highlight_selected', 'find_highlight')

    def _add_entry(self, row: int, text: str) -> ttk.Entry:
        ttk.Label(self, text=text).grid(row=row, column=0, sticky='w')
        entry = ttk.Entry(self, width=35, font='TkFixedFont')
        entry.bind('<Escape>', self.hide, add=True)
        entry.bind('<Alt-t>', self._replace_this, add=True)
        entry.bind('<Alt-a>', self._replace_all, add=True)
        entry.bind('<Alt-f>', partial(self._toggle_var, self.full_words_var), add=True)
        entry.bind('<Alt-i>', partial(self._toggle_var, self.ignore_case_var), add=True)
        entry.grid(row=row, column=1, sticky='we')
        return entry

    def _toggle_var(self, var: tkinter.BooleanVar, junk: object) -> Literal['break']:
        var.set(not var.get())
        return 'break'

    def show(self) -> None:
        try:
            selected_text: Optional[str] = self._textwidget.get('sel.first', 'sel.last')
        except tkinter.TclError:
            selected_text = None  # A little weird, but gets the job done.

{'fraction_non_alphanumeric': 0.1733269901680939, 'fraction_numerical': 0.006660323501427212, 'mean_word_length': 2.1097140039447733, 'pattern_counts': {'":': 0, '<': 27, '<?xml version=': 0, '>': 39, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 10, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4300828', 'n_tokens_mistral': 3666, 'n_tokens_neox': 3539, 'n_words': 755}