aboutsummaryrefslogtreecommitdiff
path: root/atom/packages/ex-mode/lib
diff options
context:
space:
mode:
authorBen Beltran <ben@nsovocal.com>2015-07-10 11:12:25 -0500
committerBen Beltran <ben@nsovocal.com>2015-07-10 11:12:25 -0500
commit24c7594d62d8d7fbbcdb64b11ce4adc5d8e6991a (patch)
treeded312222bb108923da1820ba40b04d710d20e5b /atom/packages/ex-mode/lib
parenteb786e82d170e2abc351a432ade616d6ecdeeb6b (diff)
Adds atom
Diffstat (limited to 'atom/packages/ex-mode/lib')
-rw-r--r--atom/packages/ex-mode/lib/command-error.coffee5
-rw-r--r--atom/packages/ex-mode/lib/command.coffee169
-rw-r--r--atom/packages/ex-mode/lib/ex-command-mode-input-element.coffee64
-rw-r--r--atom/packages/ex-mode/lib/ex-mode.coffee36
-rw-r--r--atom/packages/ex-mode/lib/ex-state.coffee59
-rw-r--r--atom/packages/ex-mode/lib/ex-view-model.coffee35
-rw-r--r--atom/packages/ex-mode/lib/ex.coffee231
-rw-r--r--atom/packages/ex-mode/lib/find.coffee26
-rw-r--r--atom/packages/ex-mode/lib/global-ex-state.coffee5
-rw-r--r--atom/packages/ex-mode/lib/view-model.coffee26
10 files changed, 656 insertions, 0 deletions
diff --git a/atom/packages/ex-mode/lib/command-error.coffee b/atom/packages/ex-mode/lib/command-error.coffee
new file mode 100644
index 0000000..c9861e9
--- /dev/null
+++ b/atom/packages/ex-mode/lib/command-error.coffee
@@ -0,0 +1,5 @@
+class CommandError
+ constructor: (@message) ->
+ @name = 'Command Error'
+
+module.exports = CommandError
diff --git a/atom/packages/ex-mode/lib/command.coffee b/atom/packages/ex-mode/lib/command.coffee
new file mode 100644
index 0000000..115b273
--- /dev/null
+++ b/atom/packages/ex-mode/lib/command.coffee
@@ -0,0 +1,169 @@
+ExViewModel = require './ex-view-model'
+Ex = require './ex'
+Find = require './find'
+CommandError = require './command-error'
+
+class Command
+ constructor: (@editor, @exState) ->
+ @viewModel = new ExViewModel(@)
+
+ parseAddr: (str, curPos) ->
+ if str is '.'
+ addr = curPos.row
+ else if str is '$'
+ # Lines are 0-indexed in Atom, but 1-indexed in vim.
+ addr = @editor.getBuffer().lines.length - 1
+ else if str[0] in ["+", "-"]
+ addr = curPos.row + @parseOffset(str)
+ else if not isNaN(str)
+ addr = parseInt(str) - 1
+ else if str[0] is "'" # Parse Mark...
+ unless @vimState?
+ throw new CommandError("Couldn't get access to vim-mode.")
+ mark = @vimState.marks[str[1]]
+ unless mark?
+ throw new CommandError("Mark #{str} not set.")
+ addr = mark.bufferMarker.range.end.row
+ else if str[0] is "/"
+ addr = Find.findNextInBuffer(@editor.buffer, curPos, str[1...-1])
+ unless addr?
+ throw new CommandError("Pattern not found: #{str[1...-1]}")
+ else if str[0] is "?"
+ addr = Find.findPreviousInBuffer(@editor.buffer, curPos, str[1...-1])
+ unless addr?
+ throw new CommandError("Pattern not found: #{str[1...-1]}")
+
+ return addr
+
+ parseOffset: (str) ->
+ if str.length is 0
+ return 0
+ if str.length is 1
+ o = 1
+ else
+ o = parseInt(str[1..])
+ if str[0] is '+'
+ return o
+ else
+ return -o
+
+ execute: (input) ->
+ @vimState = @exState.globalExState.vim?.getEditorState(@editor)
+ # Command line parsing (mostly) following the rules at
+ # http://pubs.opengroup.org/onlinepubs/9699919799/utilities
+ # /ex.html#tag_20_40_13_03
+
+ # Steps 1/2: Leading blanks and colons are ignored.
+ cl = input.characters
+ cl = cl.replace(/^(:|\s)*/, '')
+ return unless cl.length > 0
+
+ # Step 3: If the first character is a ", ignore the rest of the line
+ if cl[0] is '"'
+ return
+
+ # Step 4: Address parsing
+ lastLine = @editor.getBuffer().lines.length - 1
+ if cl[0] is '%'
+ range = [0, lastLine]
+ cl = cl[1..]
+ else
+ addrPattern = ///^
+ (?: # First address
+ (
+ \.| # Current line
+ \$| # Last line
+ \d+| # n-th line
+ '[\[\]<>'`"^.(){}a-zA-Z]| # Marks
+ /.*?[^\\]/| # Regex
+ \?.*?[^\\]\?| # Backwards search
+ [+-]\d* # Current line +/- a number of lines
+ )((?:\s*[+-]\d*)*) # Line offset
+ )?
+ (?:, # Second address
+ ( # Same as first address
+ \.|
+ \$|
+ \d+|
+ '[\[\]<>'`"^.(){}a-zA-Z]|
+ /.*?[^\\]/|
+ \?.*?[^\\]\?|
+ [+-]\d*
+ )((?:\s*[+-]\d*)*)
+ )?
+ ///
+
+ [match, addr1, off1, addr2, off2] = cl.match(addrPattern)
+
+ curPos = @editor.getCursorBufferPosition()
+
+ if addr1?
+ address1 = @parseAddr(addr1, curPos)
+ else
+ # If no addr1 is given (,+3), assume it is '.'
+ address1 = curPos.row
+ if off1?
+ address1 += @parseOffset(off1)
+
+ address1 = 0 if address1 is -1
+
+ if address1 < 0 or address1 > lastLine
+ throw new CommandError('Invalid range')
+
+ if addr2?
+ address2 = @parseAddr(addr2, curPos)
+ if off2?
+ address2 += @parseOffset(off2)
+
+ if address2 < 0 or address2 > lastLine
+ throw new CommandError('Invalid range')
+
+ if address2 < address1
+ throw new CommandError('Backwards range given')
+
+ range = [address1, if address2? then address2 else address1]
+ cl = cl[match?.length..]
+
+ # Step 5: Leading blanks are ignored
+ cl = cl.trimLeft()
+
+ # Step 6a: If no command is specified, go to the last specified address
+ if cl.length is 0
+ @editor.setCursorBufferPosition([range[1], 0])
+ return
+
+ # Ignore steps 6b and 6c since they only make sense for print commands and
+ # print doesn't make sense
+
+ # Ignore step 7a since flags are only useful for print
+
+ # Step 7b: :k<valid mark> is equal to :mark <valid mark> - only a-zA-Z is
+ # in vim-mode for now
+ if cl.length is 2 and cl[0] is 'k' and /[a-z]/i.test(cl[1])
+ command = 'mark'
+ args = cl[1]
+ else if not /[a-z]/i.test(cl[0])
+ command = cl[0]
+ args = cl[1..]
+ else
+ [m, command, args] = cl.match(/^(\w+)(.*)/)
+
+ # If the command matches an existing one exactly, execute that one
+ if (func = Ex.singleton()[command])?
+ func(range, args)
+ else
+ # Step 8: Match command against existing commands
+ matching = (name for name, val of Ex.singleton() when \
+ name.indexOf(command) is 0)
+
+ matching.sort()
+
+ command = matching[0]
+
+ func = Ex.singleton()[command]
+ if func?
+ func(range, args)
+ else
+ throw new CommandError("Not an editor command: #{input.characters}")
+
+module.exports = Command
diff --git a/atom/packages/ex-mode/lib/ex-command-mode-input-element.coffee b/atom/packages/ex-mode/lib/ex-command-mode-input-element.coffee
new file mode 100644
index 0000000..91e0eb2
--- /dev/null
+++ b/atom/packages/ex-mode/lib/ex-command-mode-input-element.coffee
@@ -0,0 +1,64 @@
+class ExCommandModeInputElement extends HTMLDivElement
+ createdCallback: ->
+ @className = "command-mode-input"
+
+ @editorContainer = document.createElement("div")
+ @editorContainer.className = "editor-container"
+
+ @appendChild(@editorContainer)
+
+ initialize: (@viewModel, opts = {}) ->
+ if opts.class?
+ @editorContainer.classList.add(opts.class)
+
+ if opts.hidden
+ @editorContainer.style.height = "0px"
+
+ @editorElement = document.createElement "atom-text-editor"
+ @editorElement.classList.add('editor')
+ @editorElement.getModel().setMini(true)
+ @editorElement.setAttribute('mini', '')
+ @editorContainer.appendChild(@editorElement)
+
+ @singleChar = opts.singleChar
+ @defaultText = opts.defaultText ? ''
+
+ @panel = atom.workspace.addBottomPanel(item: this, priority: 100)
+
+ @focus()
+ @handleEvents()
+
+ this
+
+ handleEvents: ->
+ if @singleChar?
+ @editorElement.getModel().getBuffer().onDidChange (e) =>
+ @confirm() if e.newText
+ else
+ atom.commands.add(@editorElement, 'editor:newline', @confirm.bind(this))
+
+ atom.commands.add(@editorElement, 'core:confirm', @confirm.bind(this))
+ atom.commands.add(@editorElement, 'core:cancel', @cancel.bind(this))
+ atom.commands.add(@editorElement, 'blur', @cancel.bind(this))
+
+ confirm: ->
+ @value = @editorElement.getModel().getText() or @defaultText
+ @viewModel.confirm(this)
+ @removePanel()
+
+ focus: ->
+ @editorElement.focus()
+
+ cancel: (e) ->
+ @viewModel.cancel(this)
+ @removePanel()
+
+ removePanel: ->
+ atom.workspace.getActivePane().activate()
+ @panel.destroy()
+
+module.exports =
+document.registerElement("ex-command-mode-input"
+ extends: "div",
+ prototype: ExCommandModeInputElement.prototype
+)
diff --git a/atom/packages/ex-mode/lib/ex-mode.coffee b/atom/packages/ex-mode/lib/ex-mode.coffee
new file mode 100644
index 0000000..1a4979d
--- /dev/null
+++ b/atom/packages/ex-mode/lib/ex-mode.coffee
@@ -0,0 +1,36 @@
+GlobalExState = require './global-ex-state'
+ExState = require './ex-state'
+Ex = require './ex'
+{Disposable, CompositeDisposable} = require 'event-kit'
+
+module.exports = ExMode =
+ activate: (state) ->
+ @globalExState = new GlobalExState
+ @disposables = new CompositeDisposable
+ @exStates = new WeakMap
+
+ @disposables.add atom.workspace.observeTextEditors (editor) =>
+ return if editor.mini
+
+ element = atom.views.getView(editor)
+
+ if not @exStates.get(editor)
+ exState = new ExState(
+ element,
+ @globalExState
+ )
+
+ @exStates.set(editor, exState)
+
+ @disposables.add new Disposable =>
+ exState.destroy()
+
+ deactivate: ->
+ @disposables.dispose()
+
+ provideEx: ->
+ registerCommand: Ex.registerCommand.bind(Ex)
+
+ consumeVim: (vim) ->
+ @vim = vim
+ @globalExState.setVim(vim)
diff --git a/atom/packages/ex-mode/lib/ex-state.coffee b/atom/packages/ex-mode/lib/ex-state.coffee
new file mode 100644
index 0000000..b676535
--- /dev/null
+++ b/atom/packages/ex-mode/lib/ex-state.coffee
@@ -0,0 +1,59 @@
+{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
+
+Command = require './command'
+CommandError = require './command-error'
+
+class ExState
+ constructor: (@editorElement, @globalExState) ->
+ @emitter = new Emitter
+ @subscriptions = new CompositeDisposable
+ @editor = @editorElement.getModel()
+ @opStack = []
+ @history = []
+
+ @registerOperationCommands
+ open: (e) => new Command(@editor, @)
+
+ destroy: ->
+ @subscriptions.dispose()
+
+ getExHistoryItem: (index) ->
+ @globalExState.commandHistory[index]
+
+ pushExHistory: (command) ->
+ @globalExState.commandHistory.unshift command
+
+ registerOperationCommands: (commands) ->
+ for commandName, fn of commands
+ do (fn) =>
+ pushFn = (e) => @pushOperations(fn(e))
+ @subscriptions.add(
+ atom.commands.add(@editorElement, "ex-mode:#{commandName}", pushFn)
+ )
+
+ onDidFailToExecute: (fn) ->
+ @emitter.on('failed-to-execute', fn)
+
+ pushOperations: (operations) ->
+ @opStack.push operations
+
+ @processOpStack() if @opStack.length == 2
+
+ clearOpStack: ->
+ @opStack = []
+
+ processOpStack: ->
+ [command, input] = @opStack
+ if input.characters.length > 0
+ @history.unshift command
+ try
+ command.execute(input)
+ catch e
+ if (e instanceof CommandError)
+ atom.notifications.addError("Command error: #{e.message}")
+ @emitter.emit('failed-to-execute')
+ else
+ throw e
+ @clearOpStack()
+
+module.exports = ExState
diff --git a/atom/packages/ex-mode/lib/ex-view-model.coffee b/atom/packages/ex-mode/lib/ex-view-model.coffee
new file mode 100644
index 0000000..0c99be9
--- /dev/null
+++ b/atom/packages/ex-mode/lib/ex-view-model.coffee
@@ -0,0 +1,35 @@
+{ViewModel, Input} = require './view-model'
+
+module.exports =
+class ExViewModel extends ViewModel
+ constructor: (@exCommand) ->
+ super(@exCommand, class: 'command')
+ @historyIndex = -1
+
+ atom.commands.add(@view.editorElement, 'core:move-up', @increaseHistoryEx)
+ atom.commands.add(@view.editorElement, 'core:move-down', @decreaseHistoryEx)
+
+ restoreHistory: (index) ->
+ @view.editorElement.getModel().setText(@history(index).value)
+
+ history: (index) ->
+ @exState.getExHistoryItem(index)
+
+ increaseHistoryEx: =>
+ if @history(@historyIndex + 1)?
+ @historyIndex += 1
+ @restoreHistory(@historyIndex)
+
+ decreaseHistoryEx: =>
+ if @historyIndex <= 0
+ # get us back to a clean slate
+ @historyIndex = -1
+ @view.editorElement.getModel().setText('')
+ else
+ @historyIndex -= 1
+ @restoreHistory(@historyIndex)
+
+ confirm: (view) =>
+ @value = @view.value
+ @exState.pushExHistory(@)
+ super(view)
diff --git a/atom/packages/ex-mode/lib/ex.coffee b/atom/packages/ex-mode/lib/ex.coffee
new file mode 100644
index 0000000..c159534
--- /dev/null
+++ b/atom/packages/ex-mode/lib/ex.coffee
@@ -0,0 +1,231 @@
+path = require 'path'
+CommandError = require './command-error'
+
+trySave = (func) ->
+ deferred = Promise.defer()
+
+ try
+ func()
+ deferred.resolve()
+ catch error
+ if error.message.endsWith('is a directory')
+ atom.notifications.addWarning("Unable to save file: #{error.message}")
+ else if error.path?
+ if error.code is 'EACCES'
+ atom.notifications
+ .addWarning("Unable to save file: Permission denied '#{error.path}'")
+ else if error.code in ['EPERM', 'EBUSY', 'UNKNOWN', 'EEXIST']
+ atom.notifications.addWarning("Unable to save file '#{error.path}'",
+ detail: error.message)
+ else if error.code is 'EROFS'
+ atom.notifications.addWarning(
+ "Unable to save file: Read-only file system '#{error.path}'")
+ else if (errorMatch =
+ /ENOTDIR, not a directory '([^']+)'/.exec(error.message))
+ fileName = errorMatch[1]
+ atom.notifications.addWarning("Unable to save file: A directory in the "+
+ "path '#{fileName}' could not be written to")
+ else
+ throw error
+
+ deferred.promise
+
+getFullPath = (filePath) ->
+ return filePath if path.isAbsolute(filePath)
+ return path.join(atom.project.getPath(), filePath)
+
+replaceGroups = (groups, replString) ->
+ arr = replString.split('')
+ offset = 0
+ cdiff = 0
+
+ while (m = replString.match(/(?:[^\\]|^)\\(\d)/))?
+ group = groups[m[1]] or ''
+ i = replString.indexOf(m[0])
+ l = m[0].length
+ replString = replString.slice(i + l)
+ arr[i + offset...i + offset + l] = (if l is 2 then '' else m[0][0]) +
+ group
+ arr = arr.join('').split ''
+ offset += i + l - group.length
+
+ return arr.join('').replace(/\\\\(\d)/, '\\$1')
+
+class Ex
+ @singleton: =>
+ @ex ||= new Ex
+
+ @registerCommand: (name, func) =>
+ @singleton()[name] = func
+
+ quit: ->
+ atom.workspace.getActivePane().destroyActiveItem()
+
+ q: => @quit()
+
+ tabedit: (range, args) ->
+ args = args.trim()
+ filePaths = args.split(' ')
+ pane = atom.workspace.getActivePane()
+ if filePaths? and filePaths.length > 0
+ for file in filePaths
+ do -> atom.workspace.openURIInPane file, pane
+ else
+ atom.workspace.openURIInPane('', pane)
+
+ tabe: (args...) => @tabedit(args...)
+
+ tabnew: (args...) => @tabedit(args...)
+
+ tabclose: => @quit()
+
+ tabc: => @tabclose()
+
+ tabnext: ->
+ pane = atom.workspace.getActivePane()
+ pane.activateNextItem()
+
+ tabn: => @tabnext()
+
+ tabprevious: ->
+ pane = atom.workspace.getActivePane()
+ pane.activatePreviousItem()
+
+ tabp: => @tabprevious()
+
+ edit: (range, filePath) ->
+ filePath = filePath.trim()
+ if filePath.indexOf(' ') isnt -1
+ throw new CommandError('Only one file name allowed')
+ buffer = atom.workspace.getActiveTextEditor().buffer
+ filePath = buffer.getPath() if filePath is ''
+ buffer.setPath(getFullPath(filePath))
+ buffer.load()
+
+ e: (args...) => @edit(args...)
+
+ enew: ->
+ buffer = atom.workspace.getActiveTextEditor().buffer
+ buffer.setPath(undefined)
+ buffer.load()
+
+ write: (range, filePath) ->
+ filePath = filePath.trim()
+ deferred = Promise.defer()
+
+ pane = atom.workspace.getActivePane()
+ editor = atom.workspace.getActiveTextEditor()
+ if atom.workspace.getActiveTextEditor().getPath() isnt undefined
+ if filePath.length > 0
+ editorPath = editor.getPath()
+ fullPath = getFullPath(filePath)
+ trySave(-> editor.saveAs(fullPath))
+ .then ->
+ deferred.resolve()
+ editor.buffer.setPath(editorPath)
+ else
+ trySave(-> editor.save())
+ .then deferred.resolve
+ else
+ if filePath.length > 0
+ fullPath = getFullPath(filePath)
+ trySave(-> editor.saveAs(fullPath))
+ .then deferred.resolve
+ else
+ fullPath = atom.showSaveDialogSync()
+ if fullPath?
+ trySave(-> editor.saveAs(fullPath))
+ .then deferred.resolve
+
+ deferred.promise
+
+ w: (args...) =>
+ @write(args...)
+
+ wq: (args...) =>
+ @write(args...).then => @quit()
+
+ x: (args...) => @wq(args...)
+
+ wa: ->
+ atom.workspace.saveAll()
+
+ split: (range, args) ->
+ args = args.trim()
+ filePaths = args.split(' ')
+ filePaths = undefined if filePaths.length is 1 and filePaths[0] is ''
+ pane = atom.workspace.getActivePane()
+ if filePaths? and filePaths.length > 0
+ newPane = pane.splitUp()
+ for file in filePaths
+ do ->
+ atom.workspace.openURIInPane file, newPane
+ else
+ pane.splitUp(copyActiveItem: true)
+
+ sp: (args...) => @split(args...)
+
+ substitute: (range, args) ->
+ args = args.trimLeft()
+ delim = args[0]
+ if /[a-z]/i.test(delim)
+ throw new CommandError(
+ "Regular expressions can't be delimited by letters")
+ delimRE = new RegExp("[^\\\\]#{delim}")
+ spl = []
+ args_ = args[1..]
+ while (i = args_.search(delimRE)) isnt -1
+ spl.push args_[..i]
+ args_ = args_[i + 2..]
+ if args_.length is 0 and spl.length is 3
+ throw new CommandError('Trailing characters')
+ else if args_.length isnt 0
+ spl.push args_
+ if spl.length > 3
+ throw new CommandError('Trailing characters')
+ spl[1] ?= ''
+ spl[2] ?= ''
+
+ try
+ pattern = new RegExp(spl[0], spl[2])
+ catch e
+ if e.message.indexOf('Invalid flags supplied to RegExp constructor') is 0
+ # vim only says 'Trailing characters', but let's be more descriptive
+ throw new CommandError("Invalid flags: #{e.message[45..]}")
+ else if e.message.indexOf('Invalid regular expression: ') is 0
+ throw new CommandError("Invalid RegEx: #{e.message[27..]}")
+ else
+ throw e
+
+ buffer = atom.workspace.getActiveTextEditor().buffer
+ cp = buffer.history.createCheckpoint()
+ for line in [range[0]..range[1]]
+ buffer.scanInRange(pattern,
+ [[line, 0], [line, buffer.lines[line].length]],
+ ({match, matchText, range, stop, replace}) ->
+ replace(replaceGroups(match[..], spl[1]))
+ )
+ buffer.history.groupChangesSinceCheckpoint(cp)
+
+ s: (args...) => @substitute(args...)
+
+ vsplit: (range, args) ->
+ args = args.trim()
+ filePaths = args.split(' ')
+ filePaths = undefined if filePaths.length is 1 and filePaths[0] is ''
+ pane = atom.workspace.getActivePane()
+ if filePaths? and filePaths.length > 0
+ newPane = pane.splitLeft()
+ for file in filePaths
+ do ->
+ atom.workspace.openURIInPane file, newPane
+ else
+ pane.splitLeft(copyActiveItem: true)
+
+ vsp: (args...) => @vsplit(args...)
+
+ delete: (range) ->
+ range = [[range[0], 0], [range[1] + 1, 0]]
+ atom.workspace.getActiveTextEditor().buffer.setTextInRange(range, '')
+
+module.exports = Ex
diff --git a/atom/packages/ex-mode/lib/find.coffee b/atom/packages/ex-mode/lib/find.coffee
new file mode 100644
index 0000000..53dffc5
--- /dev/null
+++ b/atom/packages/ex-mode/lib/find.coffee
@@ -0,0 +1,26 @@
+module.exports = {
+ findInBuffer : (buffer, pattern) ->
+ found = []
+ buffer.scan(new RegExp(pattern, 'g'), (obj) -> found.push obj.range)
+ return found
+
+ findNextInBuffer : (buffer, curPos, pattern) ->
+ found = @findInBuffer(buffer, pattern)
+ more = (i for i in found when i.compare([curPos, curPos]) is 1)
+ if more.length > 0
+ return more[0].start.row
+ else if found.length > 0
+ return found[0].start.row
+ else
+ return null
+
+ findPreviousInBuffer : (buffer, curPos, pattern) ->
+ found = @findInBuffer(buffer, pattern)
+ less = (i for i in found when i.compare([curPos, curPos]) is -1)
+ if less.length > 0
+ return less[less.length - 1].start.row
+ else if found.length > 0
+ return found[found.length - 1].start.row
+ else
+ return null
+}
diff --git a/atom/packages/ex-mode/lib/global-ex-state.coffee b/atom/packages/ex-mode/lib/global-ex-state.coffee
new file mode 100644
index 0000000..be51b85
--- /dev/null
+++ b/atom/packages/ex-mode/lib/global-ex-state.coffee
@@ -0,0 +1,5 @@
+class GlobalExState
+ commandHistory: []
+ setVim: (@vim) ->
+
+module.exports = GlobalExState
diff --git a/atom/packages/ex-mode/lib/view-model.coffee b/atom/packages/ex-mode/lib/view-model.coffee
new file mode 100644
index 0000000..af96e77
--- /dev/null
+++ b/atom/packages/ex-mode/lib/view-model.coffee
@@ -0,0 +1,26 @@
+ExCommandModeInputElement = require './ex-command-mode-input-element'
+
+class ViewModel
+ constructor: (@command, opts={}) ->
+ {@editor, @exState} = @command
+
+ @view = new ExCommandModeInputElement().initialize(@, opts)
+ @editor.commandModeInputView = @view
+ @exState.onDidFailToExecute => @view.remove()
+ @done = false
+
+ confirm: (view) ->
+ @exState.pushOperations(new Input(@view.value))
+ @done = true
+
+ cancel: (view) ->
+ unless @done
+ @exState.pushOperations(new Input(''))
+ @done = true
+
+class Input
+ constructor: (@characters) ->
+
+module.exports = {
+ ViewModel, Input
+}