<commit_before>
# Todo:
# Fix word movement, particularly for "a word".
# Konami code?
# Use find as a mode.

# This prevents printable characters from being passed through to underlying page.  It should, however, allow
# through chrome keyboard shortcuts.  It's a keyboard-event backstop for all of the following modes.
class SuppressPrintable extends Mode
  constructor: (options) ->
    handler = (event) =>
      if KeyboardUtils.isPrintable event
        if event.type == "keydown"
          # Completely suppress Backspace and Delete.
          if event.keyCode in [ 8, 46 ]
            @suppressEvent
          else
            DomUtils.suppressPropagation
            @stopBubblingAndFalse
        else
          @suppressEvent
      else
        @stopBubblingAndTrue

    # This is pushed onto the handler stack before calling super().  Therefore, it ends up underneath (or
    # after) all of the other handlers associated with the mode.
    handlerId = handlerStack.push
      _name: "#{@id}/suppress-printable"
      keydown: handler
      keypress: handler
      keyup: handler

    super options
    @handlers.push handlerId

# This watches keyboard events and maintains @countPrefix as number keys and other keys are pressed.
class MaintainCount extends SuppressPrintable
  constructor: (options) ->
    @countPrefix = options.initialCount || ""
    super options

    @push
      _name: "#{@id}/maintain-count"
      keypress: (event) =>
        @alwaysContinueBubbling =>
          unless event.metaKey or event.ctrlKey or event.altKey
            keyChar = String.fromCharCode event.charCode
            @countPrefix =
              if keyChar?.length == 1 and "0" <= keyChar <= "9" and @countPrefix + keyChar != "0"
                if @options.initialCount
                  @countPrefix = ""
                  delete @options.initialCount
                @countPrefix + keyChar
              else
                ""

# Some symbolic names.
forward = "forward"
backward = "backward"
character = "character"

# This implements movement commands with count prefixes (using MaintainCount) for both visual mode and edit
# mode.
class Movement extends MaintainCount
  opposite: { forward: backward, backward: forward }

  copy: (text) ->
    chrome.runtime.sendMessage handler: "copyToClipboard", data: text if text

  paste: (callback) ->
    chrome.runtime.sendMessage handler: "pasteFromClipboard", (response) -> callback response

  # Swap the anchor node/offset and the focus node/offset.
  reverseSelection: ->
    element = document.activeElement
    if element and DomUtils.isEditable(element) and not element. isContentEditable
      # Note(smblott). This implementation is unacceptably inefficient if the selection is large.  We only use
      # it if we have to.  However, the normal method does not work for input elements.
      direction = @getDirection()
      length = @selection.toString().length
      @selection[if direction == forward then "collapseToEnd" else "collapseToStart"]()
      @selection.modify "extend", @opposite[direction], character for [0...length]
    else
      # Normal method.
      direction = @getDirection()
      original = @selection.getRangeAt(0).cloneRange()
      range = original.cloneRange()
      range.collapse direction == backward
      @selection.removeAllRanges()
      @selection.addRange range
      which = if direction == forward then "start" else "end"
      @selection.extend original["#{which}Container"], original["#{which}Offset"]

  # Run a movement command.  The single movement argument can be a string of the form "direction amount", e.g.
  # "forward word", or a list, e.g. [ "forward", "word" ].
  runMovement: (movement) ->
    movement = movement.split(" ") if typeof movement == "string"
    @selection.modify @alterMethod, movement...

  # Try to move one character in "direction".  Return 1, -1 or 0, indicating whether the selection got bigger,
  # or smaller, or is unchanged.
  moveInDirection: (direction) ->
    length = @selection.toString().length
    @selection.modify "extend", direction, character
    @selection.toString().length - length

  # Get the direction of the selection.  The selection is "forward" if the focus is at or after the anchor,
  # and "backward" otherwise.
  # NOTE(smblott). Could be better, see: https://dom.spec.whatwg.org/#interface-range.
  getDirection: ->
    # Try to move the selection forward or backward, check whether it got bigger or smaller (then restore it).
    for direction in [ forward, backward ]
      if success = @moveInDirection direction
        @moveInDirection @opposite[direction]
        return if 0 < success then direction else @opposite[direction]
    backward

  # An approximation of the vim "w" movement.
  moveForwardWord: (direction) ->
    @runMovement movement for movement in [ "forward word", "forward word", "backward word" ]

  collapseSelection: ->
    if 0 < @selection.toString().length
      @selection[if @getDirection() == backward then "collapseToEnd" else "collapseToStart"]()

  movements:
    "l": "forward character"
    "h": "backward character"
    "j": "forward line"
    "k": "backward line"
    "e": "forward word"
    "b": "backward word"<commit_msg>Visual/edit modes: better word-movement/"w".<commit_after>
# Todo:
# Fix word movement, particularly for "a word".
# Konami code?
# Use find as a mode.

# This prevents printable characters from being passed through to underlying page.  It should, however, allow
# through chrome keyboard shortcuts.  It's a keyboard-event backstop for all of the following modes.
class SuppressPrintable extends Mode
  constructor: (options) ->
    handler = (event) =>
      if KeyboardUtils.isPrintable event
        if event.type == "keydown"
          # Completely suppress Backspace and Delete.
          if event.keyCode in [ 8, 46 ]
            @suppressEvent
          else
            DomUtils.suppressPropagation
            @stopBubblingAndFalse
        else
          @suppressEvent
      else
        @stopBubblingAndTrue

    # This is pushed onto the handler stack before calling super().  Therefore, it ends up underneath (or
    # after) all of the other handlers associated with the mode.
    handlerId = handlerStack.push
      _name: "#{@id}/suppress-printable"
      keydown: handler
      keypress: handler
      keyup: handler

    super options
    @handlers.push handlerId

# This watches keyboard events and maintains @countPrefix as number keys and other keys are pressed.
class MaintainCount extends SuppressPrintable
  constructor: (options) ->
    @countPrefix = options.initialCount || ""
    super options

    @push
      _name: "#{@id}/maintain-count"
      keypress: (event) =>
        @alwaysContinueBubbling =>
          unless event.metaKey or event.ctrlKey or event.altKey
            keyChar = String.fromCharCode event.charCode
            @countPrefix =
              if keyChar?.length == 1 and "0" <= keyChar <= "9" and @countPrefix + keyChar != "0"
                if @options.initialCount
                  @countPrefix = ""
                  delete @options.initialCount
                @countPrefix + keyChar
              else
                ""

# Some symbolic names.
forward = "forward"
backward = "backward"
character = "character"

# This implements movement commands with count prefixes (using MaintainCount) for both visual mode and edit
# mode.
class Movement extends MaintainCount
  opposite: { forward: backward, backward: forward }

  copy: (text) ->
    chrome.runtime.sendMessage handler: "copyToClipboard", data: text if text

  paste: (callback) ->
    chrome.runtime.sendMessage handler: "pasteFromClipboard", (response) -> callback response

  # Return a value which changes whenever the selection changes, regardless of whether the selection is
  # collapsed or not.
  hashSelection: ->
    [ @element?.selectionStart, @selection.toString().length ].join "/"

  # Call a function.  Return true if the selection changed as a side effect, false otherwise.
  selectionChanged: (func) ->
    before = @hashSelection()
    func()
    console.log before, @hashSelection()
    @hashSelection() != before

  # Run a movement.  The single movement argument can be a string of the form "direction amount", e.g.
  # "forward word", or a list, e.g. [ "forward", "word" ].
  runMovement: (movement) ->
    movement = movement.split(" ") if typeof movement == "string"
    console.log movement.join " "
    @selection.modify @alterMethod, movement...

  # Run a sequence of movements, stopping if a movement fails to change the selection.
  runMovements: (movements...) ->
    for movement in movements
      return false unless @selectionChanged => @runMovement movement
    true

  # Swap the anchor node/offset and the focus node/offset.
  reverseSelection: ->
    element = document.activeElement
    if element and DomUtils.isEditable(element) and not element. isContentEditable
      # Note(smblott). This implementation is unacceptably inefficient if the selection is large.  We only use
      # it if we have to.  However, the normal method does not work for input elements.
      direction = @getDirection()
      length = @selection.toString().length
      @selection[if direction == forward then "collapseToEnd" else "collapseToStart"]()
      @selection.modify "extend", @opposite[direction], character for [0...length]
    else
      # Normal method.
      direction = @getDirection()
      original = @selection.getRangeAt(0).cloneRange()
      range = original.cloneRange()
      range.collapse direction == backward
      @selection.removeAllRanges()
      @selection.addRange range
      which = if direction == forward then "start" else "end"
      @selection.extend original["#{which}Container"], original["#{which}Offset"]

  # Try to move one character in "direction".  Return 1, -1 or 0, indicating whether the selection got bigger,
  # or smaller, or is unchanged.
  moveInDirection: (direction) ->
    length = @selection.toString().length
    @selection.modify "extend", direction, character
    @selection.toString().length - length

  # Get the direction of the selection.  The selection is "forward" if the focus is at or after the anchor,
  # and "backward" otherwise.
  # NOTE(smblott). Could be better, see: https://dom.spec.whatwg.org/#interface-range.
  getDirection: ->
    # Try to move the selection forward or backward, check whether it got bigger or smaller (then restore it).
    for direction in [ forward, backward ]
      if success = @moveInDirection direction
        @moveInDirection @opposite[direction]
        return if 0 < success then direction else @opposite[direction]
    backward

  # An approximation of the vim "w" movement; only ever used in the forward direction.  The extra character
  # movements at the end allow us to also get to the end of the very-last word.
  moveForwardWord: () ->
    # First, move to the end of the preceding word...
    if @runMovements "forward character", "backward word", "forward word"
      # And then to the start of the following word...
      @runMovements "forward word", "forward character", "backward character", "backward word"

  collapseSelection: ->
    if 0 < @selection.toString().length
      @selection[if @getDirection() == backward then "collapseToEnd" else "collapseToStart"]()

  movements:
    "l": "forward character"
    "h": "backward character"
    "j": "forward line"
    "k": "backward line"
    "e": "forward word"
    "b": "backward word"
{'fraction_non_alphanumeric': 0.09527114967462039, 'fraction_numerical': 0.0024295010845986984, 'mean_word_length': 3.031479538300105, 'pattern_counts': {'":': 12, '<': 11, '<?xml version=': 0, '>': 35, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '658305', 'n_tokens_mistral': 3095, 'n_tokens_neox': 3030, 'n_words': 1138}