diff --git a/README.md b/README.md index 840bc26..c15896a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # ex-mode package -A short description of your package. +ex-mode for Atom's vim-mode -![A screenshot of your package](https://f.cloud.github.com/assets/69169/2290250/c35d867a-a017-11e3-86be-cd7c5bf3ff9b.gif) +## Usage + +Install vim-mode. Type `:` in command mode. Enter `w`. or `write`. diff --git a/keymaps/ex-mode.cson b/keymaps/ex-mode.cson index f618fd7..5abddd0 100644 --- a/keymaps/ex-mode.cson +++ b/keymaps/ex-mode.cson @@ -7,5 +7,5 @@ # For more detailed documentation see # https://atom.io/docs/latest/advanced/keymaps -'atom-workspace': - 'ctrl-alt-o': 'ex-mode:toggle' +'atom-text-editor.vim-mode:not(.insert-mode)': + ':': 'ex-mode:open' diff --git a/lib/command.coffee b/lib/command.coffee new file mode 100644 index 0000000..38d5765 --- /dev/null +++ b/lib/command.coffee @@ -0,0 +1,21 @@ +ExViewModel = require './ex-view-model' +Ex = require './ex' + +class CommandError + constructor: (@message) -> + @name = 'Command Error' + +class Command + constructor: (@editor, @exState) -> + @viewModel = new ExViewModel(@) + + execute: (input) -> + return unless input.characters.length > 0 + + func = (new Ex)[input.characters] + if func? + func() + else + throw new CommandError("#{input.characters}") + +module.exports = {Command, CommandError} diff --git a/lib/ex-command-mode-input-view.coffee b/lib/ex-command-mode-input-view.coffee new file mode 100644 index 0000000..45fefec --- /dev/null +++ b/lib/ex-command-mode-input-view.coffee @@ -0,0 +1,58 @@ +{View, TextEditorView} = require 'atom' + +module.exports = +class ExCommandModeInputView extends View + @content: -> + @div class: 'command-mode-input', => + @div class: 'editor-container', outlet: 'editorContainer', => + @subview 'editor', new TextEditorView(mini: true) + + initialize: (@viewModel, opts = {})-> + if opts.class? + @editorContainer.addClass opts.class + + if opts.hidden + @editorContainer.addClass 'hidden-input' + + @singleChar = opts.singleChar + @defaultText = opts.defaultText ? '' + + @panel = atom.workspace.addBottomPanel(item: this, priority: 100) + + @focus() + @handleEvents() + + handleEvents: -> + if @singleChar? + @editor.find('input').on 'textInput', @autosubmit + @editor.on 'core:confirm', @confirm + @editor.on 'core:cancel', @cancel + @editor.find('input').on 'blur', @cancel + + stopHandlingEvents: -> + if @singleChar? + @editor.find('input').off 'textInput', @autosubmit + @editor.off 'core:confirm', @confirm + @editor.off 'core:cancel', @cancel + @editor.find('input').off 'blur', @cancel + + autosubmit: (event) => + @editor.setText(event.originalEvent.data) + @confirm() + + confirm: => + @value = @editor.getText() or @defaultText + @viewModel.confirm(@) + @remove() + + focus: => + @editorContainer.find('.editor').focus() + + cancel: (e) => + @viewModel.cancel(@) + @remove() + + remove: => + @stopHandlingEvents() + atom.workspace.getActivePane().activate() + @panel.destroy() diff --git a/lib/ex-mode-view.coffee b/lib/ex-mode-view.coffee deleted file mode 100644 index 2d87618..0000000 --- a/lib/ex-mode-view.coffee +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = -class ExModeView - constructor: (serializeState) -> - # Create root element - @element = document.createElement('div') - @element.classList.add('ex-mode') - - # Create message element - message = document.createElement('div') - message.textContent = "The ExMode package is Alive! It's ALIVE!" - message.classList.add('message') - @element.appendChild(message) - - # Returns an object that can be retrieved when package is activated - serialize: -> - - # Tear down any state and detach - destroy: -> - @element.remove() - - getElement: -> - @element diff --git a/lib/ex-mode.coffee b/lib/ex-mode.coffee index 4042e32..264f2ce 100644 --- a/lib/ex-mode.coffee +++ b/lib/ex-mode.coffee @@ -1,33 +1,28 @@ -ExModeView = require './ex-mode-view' -{CompositeDisposable} = require 'atom' +GlobalExState = require './global-ex-state' +ExState = require './ex-state' +{Disposable, CompositeDisposable} = require 'event-kit' module.exports = ExMode = - exModeView: null - modalPanel: null - subscriptions: null - activate: (state) -> - @exModeView = new ExModeView(state.exModeViewState) - @modalPanel = atom.workspace.addModalPanel(item: @exModeView.getElement(), visible: false) + @globalExState = new GlobalExState + @disposables = new CompositeDisposable + @exStates = new WeakMap - # Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable - @subscriptions = new CompositeDisposable + @disposables.add atom.workspace.observeTextEditors (editor) => + return if editor.mini - # Register command that toggles this view - @subscriptions.add atom.commands.add 'atom-workspace', 'ex-mode:toggle': => @toggle() + 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: -> - @modalPanel.destroy() - @subscriptions.dispose() - @exModeView.destroy() - - serialize: -> - exModeViewState: @exModeView.serialize() - - toggle: -> - console.log 'ExMode was toggled!' - - if @modalPanel.isVisible() - @modalPanel.hide() - else - @modalPanel.show() + @disposables.dispose() diff --git a/lib/ex-state.coffee b/lib/ex-state.coffee new file mode 100644 index 0000000..40e2632 --- /dev/null +++ b/lib/ex-state.coffee @@ -0,0 +1,57 @@ +{Emitter, Disposable, CompositeDisposable} = require 'event-kit' + +{Command, CommandError} = require './command' + +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 + try + command.execute(input) + @history.unshift command + catch e + if (e instanceof CommandError) + @emitter.emit('failed-to-execute') + else + throw e + @clearOpStack() + +module.exports = ExState diff --git a/lib/ex-view-model.coffee b/lib/ex-view-model.coffee new file mode 100644 index 0000000..4f9675d --- /dev/null +++ b/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 + + @view.editor.on('core:move-up', @increaseHistoryEx) + @view.editor.on('core:move-down', @decreaseHistoryEx) + + restoreHistory: (index) -> + @view.editor.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.editor.setText('') + else + @historyIndex -= 1 + @restoreHistory(@historyIndex) + + confirm: (view) => + @value = @view.value + @exState.pushExHistory(@) + super(view) diff --git a/lib/ex.coffee b/lib/ex.coffee new file mode 100644 index 0000000..6d81188 --- /dev/null +++ b/lib/ex.coffee @@ -0,0 +1,5 @@ +class Ex + write: -> atom.workspace.getActiveEditor().save() + w: => @write() + +module.exports = Ex diff --git a/lib/global-ex-state.coffee b/lib/global-ex-state.coffee new file mode 100644 index 0000000..5325113 --- /dev/null +++ b/lib/global-ex-state.coffee @@ -0,0 +1,4 @@ +class GlobalExState + commandHistory: [] + +module.exports = GlobalExState diff --git a/lib/view-model.coffee b/lib/view-model.coffee new file mode 100644 index 0000000..7568aac --- /dev/null +++ b/lib/view-model.coffee @@ -0,0 +1,22 @@ +ExCommandModeInputView = require './ex-command-mode-input-view' + +class ViewModel + constructor: (@command, opts={}) -> + {@editor, @exState} = @command + + @view = new ExCommandModeInputView(@, opts) + @editor.commandModeInputView = @view + @exState.onDidFailToExecute => @view.remove() + + confirm: (view) -> + @exState.pushOperations(new Input(@view.value)) + + cancel: (view) -> + @exState.pushOperations(new Input('')) + +class Input + constructor: (@characters) -> + +module.exports = { + ViewModel, Input +} diff --git a/menus/ex-mode.cson b/menus/ex-mode.cson deleted file mode 100644 index 8b6c1d3..0000000 --- a/menus/ex-mode.cson +++ /dev/null @@ -1,22 +0,0 @@ -# See https://atom.io/docs/latest/creating-a-package#menus for more details -'context-menu': - 'atom-text-editor': [ - { - 'label': 'Toggle ex-mode' - 'command': 'ex-mode:toggle' - } - ] -'menu': [ - { - 'label': 'Packages' - 'submenu': [ - 'label': 'ex-mode' - 'submenu': [ - { - 'label': 'Toggle' - 'command': 'ex-mode:toggle' - } - ] - ] - } -] diff --git a/package.json b/package.json index a97ce1f..0270587 100644 --- a/package.json +++ b/package.json @@ -2,15 +2,17 @@ "name": "ex-mode", "main": "./lib/ex-mode", "version": "0.0.0", - "description": "A short description of your package", + "description": "Ex for Atom's vim-mode", "activationCommands": { - "atom-workspace": "ex-mode:toggle" + "atom-workspace": "ex-mode:open" }, - "repository": "https://github.com/atom/ex-mode", + "repository": "https://github.com/lloeki/ex-mode", "license": "MIT", "engines": { "atom": ">=0.174.0 <2.0.0" }, "dependencies": { + "underscore-plus": "1.x", + "event-kit": "^0.7.2" } }