<commit_before>{$} = require './space-pen-extensions'
_ = require 'underscore-plus'
fs = require 'fs-plus'
path = require 'path'
CSON = require 'season'
BindingSet = require './binding-set'
{Emitter} = require 'emissary'

Modifiers = ['alt', 'control', 'ctrl', 'shift', 'meta']

# Internal: Associates keymaps with actions.
#
# Keymaps are defined in a CSON format. A typical keymap looks something like this:
#
# ```cson
# 'body':
#  'ctrl-l': 'package:do-something'
#'.someClass':
#  'enter': 'package:confirm'
# ```
#
# As a key, you define the DOM element you want to work on, using CSS notation. For that
# key, you define one or more key:value pairs, associating keystrokes with a command to execute.
module.exports =
class Keymap
  Emitter.includeInto(this)

  bindingSets: null
  nextBindingSetIndex: 0
  bindingSetsByFirstKeystroke: null
  queuedKeystroke: null

  constructor: ({@resourcePath, @configDirPath})->
    @bindingSets = []
    @bindingSetsByFirstKeystroke = {}

  loadBundledKeymaps: ->
    @loadDirectory(path.join(@resourcePath, 'keymaps'))
    @emit('bundled-keymaps-loaded')

  loadUserKeymap: ->
    userKeymapPath = CSON.resolve(path.join(@configDirPath, 'keymap'))
    @load(userKeymapPath) if userKeymapPath

  loadDirectory: (directoryPath) ->
    @load(filePath) for filePath in fs.listSync(directoryPath, ['.cson', '.json'])

  load: (path) ->
    @add(path, CSON.readFileSync(path))

  add: (args...) ->
    name = args.shift() if args.length > 1
    keymap = args.shift()
    for selector, bindings of keymap
      @bindKeys(name, selector, bindings)

  remove: (name) ->
    for bindingSet in @bindingSets.filter((bindingSet) -> bindingSet.name is name)
      _.remove(@bindingSets, bindingSet)
      for keystroke of bindingSet.commandsByKeystroke
        firstKeystroke = keystroke.split(' ')[0]
        _.remove(@bindingSetsByFirstKeystroke[firstKeystroke], bindingSet)

  bindKeys: (args...) ->
    name = args.shift() if args.length > 2
    [selector, bindings] = args
    bindingSet = new BindingSet(selector, bindings, @nextBindingSetIndex++, name)
    @bindingSets.unshift(bindingSet)
    for keystroke of bindingSet.commandsByKeystroke
      keystroke = keystroke.split(' ')[0] # only index by first keystroke
      @bindingSetsByFirstKeystroke[keystroke] ?= []
      @bindingSetsByFirstKeystroke[keystroke].push(bindingSet)

  unbindKeys: (selector, bindings) ->
    bindingSet = _.detect @bindingSets, (bindingSet) ->
      bindingSet.selector is selector and bindingSet.bindings is bindings

    if bindingSet
      _.remove(@bindingSets, bindingSet)

  handleKeyEvent: (event) ->
    element = event.target
    element = rootView[0] if element == document.body
    keystroke = @keystrokeStringForEvent(event, @queuedKeystroke)
    mappings = @mappingsForKeystroke(keystroke)
    mappings = @mappingsMatchingElement(mappings, element)

    if mappings.length == 0 and @queuedKeystroke
      @queuedKeystroke = null
      return false
    else
      @queuedKeystroke = null

    for mapping in mappings
      partialMatch = mapping.keystroke isnt keystroke
      if partialMatch
        @queuedKeystroke = keystroke
        shouldBubble = false
      else
        if mapping.command is 'native!'
          shouldBubble = true
        else if @triggerCommandEvent(element, mapping.command)
          shouldBubble = false

      break if shouldBubble?

    shouldBubble ? true

  # Public: Returns an array of objects that represent every keystroke to
  # command mapping. Each object contains the following keys `source`,
  # `selector`, `command`, `keystroke`, `index`, `specificity`.
  allMappings: ->
    mappings = []

    for bindingSet in @bindingSets
      for keystroke, command of bindingSet.getCommandsByKeystroke()
        mappings.push @buildMapping(bindingSet, command, keystroke)

    mappings

  mappingsForKeystroke: (keystroke) ->
    mappings = @allMappings().filter (mapping) ->
      multiKeystroke = /\s/.test keystroke
      if multiKeystroke
        keystroke == mapping.keystroke
      else
        keystroke.split(' ')[0] == mapping.keystroke.split(' ')[0]

  mappingsMatchingElement: (mappings, element) ->
    mappings = mappings.filter ({selector}) -> $(element).closest(selector).length > 0
    mappings.sort (a, b) ->
      if b.specificity == a.specificity
        b.index - a.index
      else
        b.specificity - a.specificity

  buildMapping: (bindingSet, command, keystroke) ->
    selector = bindingSet.selector
    specificity = bindingSet.specificity
    index = bindingSet.index
    source = bindingSet.name
    {command, keystroke, selector, specificity, source}

  triggerCommandEvent: (element, commandName) ->
    commandEvent = $.Event(commandName)
    commandEvent.abortKeyBinding = -> commandEvent.stopImmediatePropagation()
    $(element).trigger(commandEvent)
    not commandEvent.isImmediatePropagationStopped()

  keystrokeStringForEvent: (event, previousKeystroke) ->
    if event.originalEvent.keyIdentifier.indexOf('U+') == 0
      hexCharCode = event.originalEvent.keyIdentifier[2..]
      charCode = parseInt(hexCharCode, 16)
      charCode = event.which if !@isAscii(charCode) and @isAscii(event.which)
      key = @keyFromCharCode(charCode)<commit_msg>Reorder args<commit_after>{$} = require './space-pen-extensions'
_ = require 'underscore-plus'
fs = require 'fs-plus'
path = require 'path'
CSON = require 'season'
BindingSet = require './binding-set'
{Emitter} = require 'emissary'

Modifiers = ['alt', 'control', 'ctrl', 'shift', 'meta']

# Internal: Associates keymaps with actions.
#
# Keymaps are defined in a CSON format. A typical keymap looks something like this:
#
# ```cson
# 'body':
#  'ctrl-l': 'package:do-something'
#'.someClass':
#  'enter': 'package:confirm'
# ```
#
# As a key, you define the DOM element you want to work on, using CSS notation. For that
# key, you define one or more key:value pairs, associating keystrokes with a command to execute.
module.exports =
class Keymap
  Emitter.includeInto(this)

  bindingSets: null
  nextBindingSetIndex: 0
  bindingSetsByFirstKeystroke: null
  queuedKeystroke: null

  constructor: ({@resourcePath, @configDirPath})->
    @bindingSets = []
    @bindingSetsByFirstKeystroke = {}

  loadBundledKeymaps: ->
    @loadDirectory(path.join(@resourcePath, 'keymaps'))
    @emit('bundled-keymaps-loaded')

  loadUserKeymap: ->
    userKeymapPath = CSON.resolve(path.join(@configDirPath, 'keymap'))
    @load(userKeymapPath) if userKeymapPath

  loadDirectory: (directoryPath) ->
    @load(filePath) for filePath in fs.listSync(directoryPath, ['.cson', '.json'])

  load: (path) ->
    @add(path, CSON.readFileSync(path))

  add: (args...) ->
    name = args.shift() if args.length > 1
    keymap = args.shift()
    for selector, bindings of keymap
      @bindKeys(name, selector, bindings)

  remove: (name) ->
    for bindingSet in @bindingSets.filter((bindingSet) -> bindingSet.name is name)
      _.remove(@bindingSets, bindingSet)
      for keystroke of bindingSet.commandsByKeystroke
        firstKeystroke = keystroke.split(' ')[0]
        _.remove(@bindingSetsByFirstKeystroke[firstKeystroke], bindingSet)

  bindKeys: (args...) ->
    name = args.shift() if args.length > 2
    [selector, bindings] = args
    bindingSet = new BindingSet(selector, bindings, @nextBindingSetIndex++, name)
    @bindingSets.unshift(bindingSet)
    for keystroke of bindingSet.commandsByKeystroke
      keystroke = keystroke.split(' ')[0] # only index by first keystroke
      @bindingSetsByFirstKeystroke[keystroke] ?= []
      @bindingSetsByFirstKeystroke[keystroke].push(bindingSet)

  unbindKeys: (selector, bindings) ->
    bindingSet = _.detect @bindingSets, (bindingSet) ->
      bindingSet.selector is selector and bindingSet.bindings is bindings

    if bindingSet
      _.remove(@bindingSets, bindingSet)

  handleKeyEvent: (event) ->
    element = event.target
    element = rootView[0] if element == document.body
    keystroke = @keystrokeStringForEvent(event, @queuedKeystroke)
    mappings = @mappingsForKeystroke(keystroke)
    mappings = @mappingsMatchingElement(mappings, element)

    if mappings.length == 0 and @queuedKeystroke
      @queuedKeystroke = null
      return false
    else
      @queuedKeystroke = null

    for mapping in mappings
      partialMatch = mapping.keystroke isnt keystroke
      if partialMatch
        @queuedKeystroke = keystroke
        shouldBubble = false
      else
        if mapping.command is 'native!'
          shouldBubble = true
        else if @triggerCommandEvent(element, mapping.command)
          shouldBubble = false

      break if shouldBubble?

    shouldBubble ? true

  # Public: Returns an array of objects that represent every keystroke to
  # command mapping. Each object contains the following keys `source`,
  # `selector`, `command`, `keystroke`, `index`, `specificity`.
  allMappings: ->
    mappings = []

    for bindingSet in @bindingSets
      for keystroke, command of bindingSet.getCommandsByKeystroke()
        mappings.push @buildMapping(bindingSet, command, keystroke)

    mappings

  mappingsForKeystroke: (keystroke) ->
    mappings = @allMappings().filter (mapping) ->
      multiKeystroke = /\s/.test keystroke
      if multiKeystroke
        keystroke == mapping.keystroke
      else
        keystroke.split(' ')[0] == mapping.keystroke.split(' ')[0]

  mappingsMatchingElement: (element, mappings=@allMappings()) ->
    mappings = mappings.filter ({selector}) -> $(element).closest(selector).length > 0
    mappings.sort (a, b) ->
      if b.specificity == a.specificity
        b.index - a.index
      else
        b.specificity - a.specificity

  buildMapping: (bindingSet, command, keystroke) ->
    selector = bindingSet.selector
    specificity = bindingSet.specificity
    index = bindingSet.index
    source = bindingSet.name
    {command, keystroke, selector, specificity, source}

  triggerCommandEvent: (element, commandName) ->
    commandEvent = $.Event(commandName)
    commandEvent.abortKeyBinding = -> commandEvent.stopImmediatePropagation()
    $(element).trigger(commandEvent)
    not commandEvent.isImmediatePropagationStopped()

  keystrokeStringForEvent: (event, previousKeystroke) ->
    if event.originalEvent.keyIdentifier.indexOf('U+') == 0
      hexCharCode = event.originalEvent.keyIdentifier[2..]
      charCode = parseInt(hexCharCode, 16)
      charCode = event.which if !@isAscii(charCode) and @isAscii(event.which)
      key = @keyFromCharCode(charCode)
{'fraction_non_alphanumeric': 0.1137701804368471, 'fraction_numerical': 0.0026590693257359924, 'mean_word_length': 3.655614500442087, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 53, 'https://': 0, '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': '6841467', 'n_tokens_mistral': 3284, 'n_tokens_neox': 3101, 'n_words': 792}