Script for better counting
-
I found some bugs in Notepad++'s Count searching feature, in certain circumstances, so I let the Notepad++ developer know by creating an issue:
https://github.com/notepad-plus-plus/notepad-plus-plus/issues/14200
I also created an easy proposed fix for the problems I encountered:
https://github.com/notepad-plus-plus/notepad-plus-plus/pull/14213
After lengthy debate in the comments section of that “pull” link, the developer decided not to accept my fixes, and that nothing is wrong with the way Count works. (I think the actual result was “let’s see if any other users complain”, which is becoming a common reaction.)
Needless to say, I disagree so I’ve created what I think is a better match-counting solution via script. In this version, called “CountImproved”, I fixed the problems that I noted and added some new ways of counting, which Notepad++'s Count can’t do, specifically:
- counting outside of the selected text
- counting inside/outside multiple selections
- counting inside/outside a column-selected block of text
For the script’s user interface, I utilized the Little Dialog Wrapper and arrived at this, which should be self-explanatory for anyone that has used Notepad++'s stock counting interface:
Here’s the script listing for
CountImproved.py
:# -*- coding: utf-8 -*- # references: # https://community.notepad-plus-plus.org/topic/25022/script-for-better-counting <-- this script is here # https://github.com/notepad-plus-plus/notepad-plus-plus/issues/14200 # https://github.com/notepad-plus-plus/notepad-plus-plus/pull/14213 # for info on WinDialog (aka Little Dialog Wrapper), see # https://github.com/Ekopalypse/NppPythonScripts/tree/master/helper/WinDialog # https://community.notepad-plus-plus.org/topic/24389 # for newbie info on PythonScripts, see https://community.notepad-plus-plus.org/topic/23039/faq-desk-how-to-install-and-run-a-script-in-pythonscript from Npp import * from WinDialog import * # "Little Dialog Wrapper" from WinDialog.controls.button import BST from ctypes import WinDLL user32 = WinDLL('user32') def is_checked(button): return button.getCheckState() == BST.CHECKED def search_regex_is_invalid_error_msg(test_regex): try: # test test_regex for validity editor.research(test_regex, lambda _: None) except RuntimeError as r: return str(r) return '' class RangeTupleList(object): def __init__(self): pass def consolidate_adjacent_ranges(self, two_tup_list): # inspired by https://stackoverflow.com/questions/5679638/merging-a-list-of-time-range-tuples-that-have-overlapping-time-ranges # example: # [ (40,40), (40,40), (41,42), (44,45) ] -> [ (40,42), (44,45) ] if len(two_tup_list) == 0: return two_tup_list # nothing to do two_tup_list = sorted(two_tup_list) held_list = list(two_tup_list[0]) for (start, end) in two_tup_list[1:]: if held_list[1] <= start <= held_list[1] + 1: held_list[1] = end else: yield tuple(held_list) held_list[0] = start; held_list[1] = end yield tuple(held_list) def remove_zero_length_tups(self, two_tup_list): rv = [] for (start, end) in two_tup_list: if start != end: rv.append( (start, end) ) return rv def invert(self, two_tup_list, lower_bound, upper_bound): inverted_tup_list = [] if len(two_tup_list) == 0: inverted_tup_list.append( (lower_bound, upper_bound) ) elif two_tup_list[0][0] == lower_bound and two_tup_list[-1][1] == upper_bound: pass # inverted_tup_list should return as empty list else: offset = 0 if two_tup_list[0][0] != lower_bound: inverted_tup_list.append( (0, two_tup_list[0][0] - offset) ) for j in range(len(two_tup_list) - 1): inverted_tup_list.append( (two_tup_list[j][1] + offset, two_tup_list[j + 1][0] - offset) ) if two_tup_list[-1][1] != upper_bound: inverted_tup_list.append( (two_tup_list[-1][1] + offset, upper_bound) ) return inverted_tup_list class CountImprovedDialog(Dialog): def __init__(self, title='Count Improved'): super().__init__(title) dlgw = 250 self.size = (dlgw,147) self.center = True self.status_bar = StatusBar('', size=(dlgw-1,12), position=(0,88)) self.findwhat_label = Label('Find what:', size=(50,10), position=(5,5)) self.findwhat_edit = TextBox('', size=(240,12), position=(5,14)) self.fill_findwhat_from_sel_button = Button('▼ &Fill with selected text ▼', size=(120,14), position=(105,0)) self.fill_findwhat_from_sel_button.onClick = self.fill_findwhat_click self.matchcase_checkbox = CheckBoxButton(title='Match &case', size=(90,14), position=(10,30)) self.matchwholewordonly_checkbox = CheckBoxButton(title='Match whole &word only', size=(90,14), position=(10,48)) self.regex_checkbox = CheckBoxButton(title='Regular e&xpression', size=(90,14), position=(10,66)) self.regex_checkbox.onClick = self.regex_click self.dotmatchesnewline_checkbox = CheckBoxButton(title='. matches &newline', size=(90,14), position=(18,78)) gbx = 130; gby = 30 self.scope_groupbox = GroupBox('Search Scope:', size=(90,77), position=(gbx,gby)) self.scope_fwd_from_caret_radiobutton = RadioButton('Fo&rward from caret', size=(80,14), position=(gbx+10,gby+10)) self.scope_bwd_from_caret_radiobutton = RadioButton('&Backward from caret', size=(80,14), position=(gbx+10,gby+22)) self.scope_in_sel_radiobutton = RadioButton('&Inside selection(s)', size=(80,14), position=(gbx+10,gby+34)) self.scope_out_sel_radiobutton = RadioButton('&Outside selection(s)', size=(80,14), position=(gbx+10,gby+46)) self.scope_entire_doc_radiobutton = RadioButton('&Entire file', size=(80,14), position=(gbx+10,gby+58)) cbw = 150 self.count_button = Button('Coun&t', size=(cbw,14), position=(int(dlgw/2-cbw/2),114)) self.count_button.onClick = self.count_button_click self.show() def initialize(self): self.scope_entire_doc_radiobutton.setCheckState(BST.CHECKED) something_is_selected = not editor.getSelectionEmpty() user32.EnableWindow(self.fill_findwhat_from_sel_button.hwnd, something_is_selected) user32.EnableWindow(self.scope_in_sel_radiobutton.hwnd, something_is_selected) user32.EnableWindow(self.scope_out_sel_radiobutton.hwnd, something_is_selected) user32.EnableWindow(self.dotmatchesnewline_checkbox.hwnd, is_checked(self.regex_checkbox)) def fill_findwhat_click(self): main_sel_nbr = editor.getSelections() - 1 main_sel_text = editor.getTextRange(editor.getSelectionNStart(main_sel_nbr), editor.getSelectionNEnd(main_sel_nbr)) self.findwhat_edit.setText(main_sel_text) def regex_click(self): user32.EnableWindow(self.dotmatchesnewline_checkbox.hwnd, is_checked(self.regex_checkbox)) def count_button_click(self): find_what = self.findwhat_edit.getText() if len(find_what) == 0: self.status_bar.setText('Error: Find-what text must be supplied!') return regex_mode = is_checked(self.regex_checkbox) search_regex = find_what if regex_mode: if search_regex_is_invalid_error_msg(find_what): self.status_bar.setText('Error: Bad regex supplied!') return else: search_regex = r'\Q' + search_regex + r'\E' if is_checked(self.matchwholewordonly_checkbox): search_regex = r'\b' + search_regex + r'\b' search_regex = ('(?-i)' if is_checked(self.matchcase_checkbox) else '(?i)') + search_regex if regex_mode: search_regex = ('(?s)' if is_checked(self.dotmatchesnewline_checkbox) else '(?-s)') + search_regex curr_pos = editor.getCurrentPos() inside_selection = is_checked(self.scope_in_sel_radiobutton) outside_selection = is_checked(self.scope_out_sel_radiobutton) if inside_selection: search_start_pos = editor.getSelectionStart() search_end_pos = editor.getSelectionEnd() o = 'Count: MATCH_COUNT match(es) in selected text' elif outside_selection: search_start_pos = editor.getSelectionStart() search_end_pos = editor.getSelectionEnd() o = 'Count: MATCH_COUNT match(es) in unselected text' elif is_checked(self.scope_entire_doc_radiobutton): search_start_pos = 0 search_end_pos = editor.getLength() o = 'Count: MATCH_COUNT match(es) in entire file' elif is_checked(self.scope_bwd_from_caret_radiobutton): search_start_pos = 0 search_end_pos = curr_pos o = 'Count: MATCH_COUNT match(es) from start-of-file to caret' else: search_start_pos = curr_pos search_end_pos = editor.getLength() o = 'Count: MATCH_COUNT match(es) from caret to end-of-file' self.match_count = 0 def increment_match_count(m): self.match_count += 1 num_sel = editor.getSelections() if inside_selection and num_sel > 1 or outside_selection: sels_tup_list = [] for n in range(num_sel): sels_tup_list.append( (editor.getSelectionNStart(n), editor.getSelectionNEnd(n)) ) rtl = RangeTupleList() sels_tup_list = rtl.remove_zero_length_tups(sels_tup_list) sels_tup_list = list(rtl.consolidate_adjacent_ranges(sels_tup_list)) if outside_selection: sels_tup_list = rtl.invert(sels_tup_list, 0, editor.getLength()) for (search_start_pos, search_end_pos) in sels_tup_list: editor.research(search_regex, increment_match_count, 0, search_start_pos, search_end_pos) else: editor.research(search_regex, increment_match_count, 0, search_start_pos, search_end_pos) self.status_bar.setText(o.replace('MATCH_COUNT', str(self.match_count))) CountImprovedDialog()
Please note that as Little Dialog Wrapper requires PythonScript 3.0.16.0 or later, so does this script.
-