Community
    • Login

    Script for better counting

    Scheduled Pinned Locked Moved General Discussion
    1 Posts 1 Posters 448 Views
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • Alan KilbornA
      Alan Kilborn
      last edited by PeterJones

      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:

      41ec0e80-9eeb-48c4-832c-1fd609ba8737-image.png

      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.

      1 Reply Last reply Reply Quote 5
      • Alan KilbornA Alan Kilborn referenced this topic on
      • First post
        Last post
      The Community of users of the Notepad++ text editor.
      Powered by NodeBB | Contributors