text: import {Node} from "prosemirror-model"
  
  import {Selection} from "./selection"
  import {Transaction} from "./transaction"
  
  function bind(f, self) {
    return !self || !f ? f : f.bind(self)
  }
  
  class FieldDesc {
    constructor(name, desc, self) {
      this.name = name
      this.init = bind(desc.init, self)
      this.apply = bind(desc.apply, self)
    }
  }
  
  const baseFields = [
    new FieldDesc("doc", {
      init(config) { return config.doc || config.schema.topNodeType.createAndFill() },
      apply(tr) { return tr.doc }
    }),
  
    new FieldDesc("selection", {
      init(config, instance) { return config.selection || Selection.atStart(instance.doc) },
      apply(tr) { return tr.selection }
    }),
  
    new FieldDesc("storedMarks", {
      init(config) { return config.storedMarks || null },
      apply(tr, _marks, _old, state) { return state.selection.$cursor ? tr.storedMarks : null }
    }),
  
    new FieldDesc("scrollToSelection", {
      init() { return 0 },
      apply(tr, prev) { return tr.scrolledIntoView ? prev + 1 : prev }
    })
  ]
  
  // Object wrapping the part of a state object that stays the same
  // across transactions. Stored in the state's `config` property.
  class Configuration {
    constructor(schema, plugins) {
      this.schema = schema
      this.fields = baseFields.concat()
      this.plugins = []
      this.pluginsByKey = Object.create(null)
      if (plugins) plugins.forEach(plugin => {
        if (this.pluginsByKey[plugin.key])
          throw new RangeError("Adding different instances of a keyed plugin (" + plugin.key + ")")
        this.plugins.push(plugin)
        this.pluginsByKey[plugin.key] = plugin
        if (plugin.spec.state)
          this.fields.push(new FieldDesc(plugin.key, plugin.spec.state, plugin))
      })
    }
  }
  
  // ::- The state of a ProseMirror editor is represented by an object
  // of this type. A state is a persistent data structure—it isn't
  // updated, but rather a new state value is computed from an old one
  // using the [`apply`](#state.EditorState.apply) method.
  //
  // A state holds a number of built-in fields, and plugins can
  // [define](#state.PluginSpec.state) additional fields.
  export class EditorState {
    constructor(config) {
      this.config = config
    }
  
    // doc:: Node
    // The current document.
  
    // selection:: Selection
    // The selection.
  
    // storedMarks:: ?[Mark]
    // A set of marks to apply to the next input. Will be null when
    // no explicit marks have been set.
  
    // :: Schema
    // The schema of the state's document.
    get schema() {
      return this.config.schema
    }
  
    // :: [Plugin]
    // The plugins that are active in this state.
    get plugins() {
      return this.config.plugins
    }
  
    // :: (Transaction) → EditorState
    // Apply the given transaction to produce a new state.
    apply(tr) {
      return this.applyTransaction(tr).state
    }
  
    // : (Transaction) → ?Transaction
    filterTransaction(tr, ignore = -1) {
      for (let i = 0; i < this.config.plugins.length; i++) if (i != ignore) {
        let plugin = this.config.plugins[i]
        if (plugin.spec.filterTransaction && !plugin.spec.filterTransaction.call(plugin, tr, this))
          return false
      }
      return true
    }
  
    // :: (Transaction) → {state: EditorState, transactions: [Transaction]}
    // Verbose variant of [`apply`](#state.EditorState.apply) that
    // returns the precise transactions that were applied (which might
    // be influenced by the [transaction
    // hooks](#state.PluginSpec.filterTransaction) of
    // plugins) along with the new state.
    applyTransaction(tr) {
      if (!this.filterTransaction(tr)) return {state: this, transactions: []}
  
      let trs = [tr], newState = this.applyInner(tr), seen = null
      // This loop repeatedly gives plugins a chance to respond to
      // transactions as new transactions are added, making sure to only
      // pass the transactions the plugin did not see before.
      outer: for (;;) {
        let haveNew = false
        for (let i = 0; i < this.config.plugins.length; i++) {
          let plugin = this.config.plugins[i]
          if (plugin.spec.appendTransaction) {
            let n = seen ? seen[i].n : 0, oldState = seen ? seen[i].state : this
            let tr = n < trs.length &&
                plugin.spec.appendTransaction.call(plugin, n ? trs.slice(n) : trs, oldState, newState)
            if (tr && newState.filterTransaction(tr, i)) {
              tr.setMeta("appendedTransaction", tr)
              if (!seen) {
                seen = []
                for (let j = 0; j < this.config.plugins.length; j++)
                  seen.push(j < i ? {state: newState, n: trs.length} : {state: this, n: 0})
              }
              trs.push(tr)
              newState = newState.applyInner(tr)
              haveNew = true
            }
            if (seen) seen[i] = {state: newState, n: trs.length}
          }
        }
        if (!haveNew) return {state: newState, transactions: trs}
      }
    }
  
    // : (Transaction) → EditorState
    applyInner(tr) {
      if (!tr.before.eq(this.doc)) throw new RangeError("Applying a mismatched transaction")
      let newInstance = new EditorState(this.config), fields = this.config.fields
      for (let i = 0; i < fields.length; i++) {
        let field = fields[i]
        newInstance[field.name] = field.apply(tr, this[field.name], this, newInstance)
      }
      for (let i = 0; i < applyListeners.length; i++) applyListeners[i](this, tr, newInstance)
      return newInstance
    }
  
    // :: Transaction
    // Start a [transaction](#state.Transaction) from this state.
    get tr() { return new Transaction(this) }
  
    // :: (Object) → EditorState
    // Create a new state.
    //
    //   config::- Configuration options. Must contain `schema` or `doc` (or both).
    //
    //      schema:: ?Schema
    //      The schema to use.
    //
    //      doc:: ?Node
    //      The starting document.
    //
    //      selection:: ?Selection
    //      A valid selection in the document.
    //
    //      storedMarks:: ?[Mark]
    //      The initial set of [stored marks](#state.EditorState.storedMarks).
    //
    //      plugins:: ?[Plugin]
    //      The plugins that should be active in this state.
    static create(config) {
      let $config = new Configuration(config.schema || config.doc.type.schema, config.plugins)
      let instance = new EditorState($config)
      for (let i = 0; i < $config.fields.length; i++)
        instance[$config.fields[i].name] = $config.fields[i].init(config, instance)
      return instance
    }
  
    // :: (Object) → EditorState
    // Create a new state based on this one, but with an adjusted set of
    // active plugins. State fields that exist in both sets of plugins
    // are kept unchanged. Those that no longer exist are dropped, and
    // those that are new are initialized using their
    // [`init`](#state.StateField.init) method, passing in the new
    // configuration object..
    //
    //   config::- configuration options
    //
    //     schema:: ?Schema
    //     New schema to use.
    //
    //     plugins:: ?[Plugin]
    //     New set of active plugins.
    reconfigure(config) {
      let $config = new Configuration(config.schema || this.schema, config.plugins)
      let fields = $config.fields, instance = new EditorState($config)
      for (let i = 0; i < fields.length; i++) {
        let name = fields[i].name
        instance[name] = this.hasOwnProperty(name) ? this[name] : fields[i].init(config, instance)
      }
      return instance
    }
  
    // :: (?union<Object<Plugin>, string, number>) → Object
    // Serialize this state to JSON. If you want to serialize the state
    // of plugins, pass an object mapping property names to use in the
    // resulting JSON object to plugin objects. The argument may also be
    // a string or number, in which case it is ignored, to support the
    // way `JSON.stringify` calls `toString` methods.
    toJSON(pluginFields) {
      let result = {doc: this.doc.toJSON(), selection: this.selection.toJSON()}
      if (this.storedMarks) result.storedMarks = this.storedMarks.map(m => m.toJSON())
      if (pluginFields && typeof pluginFields == 'object') for (let prop in pluginFields) {
        if (prop == "doc" || prop == "selection")
          throw new RangeError("The JSON fields `doc` and `selection` are reserved")
        let plugin = pluginFields[prop], state = plugin.spec.state
        if (state && state.toJSON) result[prop] = state.toJSON.call(plugin, this[plugin.key])
      }
      return result
    }
  
    // :: (Object, Object, ?Object<Plugin>) → EditorState
    // Deserialize a JSON representation of a state. `config` should
    // have at least a `schema` field, and should contain array of
    // plugins to initialize the state with. `pluginFields` can be used
    // to deserialize the state of plugins, by associating plugin
    // instances with the property names they use in the JSON object.
    //
    //   config::- configuration options
    //
    //     schema:: Schema
    //     The schema to use.
    //
    //     plugins:: ?[Plugin]
    //     The set of active plugins.
    static fromJSON(config, json, pluginFields) {
      if (!json) throw new RangeError("Invalid input for EditorState.fromJSON")
      if (!config.schema) throw new RangeError("Required config field 'schema' missing")
      let $config = new Configuration(config.schema, config.plugins)
      let instance = new EditorState($config)
      $config.fields.forEach(field => {
        if (field.name == "doc") {
          instance.doc = Node.fromJSON(config.schema, json.doc)
        } else if (field.name == "selection") {
          instance.selection = Selection.fromJSON(instance.doc, json.selection)
        } else if (field.name == "storedMarks") {
          if (json.storedMarks) instance.storedMarks = json.storedMarks.map(config.schema.markFromJSON)
        } else {
          if (pluginFields) for (let prop in pluginFields) {
            let plugin = pluginFields[prop], state = plugin.spec.state
            if (plugin.key == field.name && state && state.fromJSON &&
                Object.prototype.hasOwnProperty.call(json, prop)) {
              // This field belongs to a plugin mapped to a JSON field, read it from there.
              instance[field.name] = state.fromJSON.call(plugin, config, json[prop], instance)
              return
            }
          }
          instance[field.name] = field.init(config, instance)
        }
      })
      return instance
    }
  
    // Kludge to allow the view to track mappings between different
    // instances of a state.
    static addApplyListener(f) {
      applyListeners.push(f)
    }
    static removeApplyListener(f) {
      let found = applyListeners.indexOf(f)
      if (found > -1) applyListeners.splice(found, 1)
    }
  }
  
  const applyListeners = []
  