diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 33224b4..0000000 --- a/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -root = true - -[*] -end_of_line = lf -insert_final_newline = true - -[*.{coffee,json}] -indent_style = space -indent_size = 2 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index 2231100..0000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,23 +0,0 @@ -# Ex-Mode Contributing Guidelines - -Current Maintainers: - -- [@jazzpi](https://github.com/jazzpi) - -This project is accepting new maintainers. Interested parties should open a new issue titled `New Maintainer Request`. - -## Pull Requests - -- If the PR *fixes* or should result in the closure of any issues, use the `fixes #` or `closes #` syntax to ensure issue will -close when your PR is merged -- All pull-requests that fix a bug or add a new feature *must* have accompanying tests before they will be merged. If you want -to speed up the merge of your PR, please contribute these tests - - *note*: if you submit a PR but are unsure how to write tests, please begin your PR title with `[needs tests]` -- Please use the [pull request template](PULL_REQUEST_TEMPLATE.md) as a guide for submitting your PR. - -## Issues - -- Be aware of the responsibilities of `ex-mode` vs `vim-mode` -- If you have identified a bug we would welcome any Pull Requests that either: - - Fix the issue - - Create failing tests to confirm the bug diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 1b3c90e..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,15 +0,0 @@ -Fixes # . - -Changes Proposed in this Pull Request: - -- foo -- bar -- baz - -I have written tests for: - -[](Remove the `[]()` to uncomment the appropriate lines) - -[](- New features introduced) -[](- Bugs fixed) -[](- Neither (I'm just enhancing tests!)) diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e8e09d4..0000000 --- a/.travis.yml +++ /dev/null @@ -1,45 +0,0 @@ -## Project specific config ### -language: generic - -env: - global: - - APM_TEST_PACKAGES="vim-mode-plus" - - ATOM_LINT_WITH_BUNDLED_NODE="true" - - matrix: - - ATOM_CHANNEL=stable - - ATOM_CHANNEL=beta - -os: - - linux - -dist: trusty - -### Generic setup follows ### -script: - - curl -s -O https://raw.githubusercontent.com/atom/ci/master/build-package.sh - - chmod u+x build-package.sh - - ./build-package.sh - -notifications: - email: - on_success: never - on_failure: change - -branches: - only: - - master - -git: - depth: 10 - -sudo: false - -addons: - apt: - packages: - - build-essential - - git - - libgnome-keyring-dev - - libsecret-1-dev - - fakeroot diff --git a/CHANGELOG.md b/CHANGELOG.md index 30a0532..c3d858c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,93 +1,3 @@ -## (unpublished) - -* Fix `:enew` not working ([#215](https://github.com/lloeki/ex-mode/pull/215)) - -## 0.18.0 - -* Add Gdefault support ([#191](https://github.com/lloeki/ex-mode/pull/191)) -* Add :sort command ([#190](https://github.com/lloeki/ex-mode/pull/190)) - -## 0.17.0 - -* Add support for Atom 1.19 ([#185](https://github.com/lloeki/ex-mode/pull/185)) -* Added support for canceling ex-mode with Ctrl-C ([#186](https://github.com/lloeki/ex-mode/pull/186)) - -## 0.16.0 - -* Support for Atom 1.18 and 1.19 ([#184](https://github.com/lloeki/ex-mode/pull/184)) - -## 0.15.0 -* `vim-mode-plus` support! - - Add keybinding (@jmarianer) - - Support `:substitute` command (@mkiken) - - Support marks - - Use `vim-mode-plus` in specs - -## 0.14.0 -* Support `:tabonly` (@jmarianer) -* Fix `:x` closing Atom instead of the current pane - -## 0.13.1 -* Limit addresses to the last line -* Fix autocompleting a non existent directory (@mcnicholls) - -## 0.13.0 -* Added basic support for visual marks (e.g. `:'<,'>s/foo/bar`) -* Added `smartcase` option to `:set` -* Fixed using marks as addresses (e.g. `:'a,.delete`) -* Fixed search not working without a closing delimiter (e.g. `:/foo`) - -## 0.12.0 -* Added file and command autocomplete (@stuartquin) -* Added `splitbelow` and `splitright` options to `:set` -* Fixed the editor not updating when saving a new file with `:w` or `:saveas` - -## 0.11.0 - -* Stop using non-standard Promise.defer (fixes issue with `:w`) (@AsaAyers) - -## 0.9.0 - -* Added support for yank commands, ex `:1,10y` (@posgarou) -* Added contributor guidelines, including a pull request template -* Added ability to control splitting with `splitright`, and `splitbelow` (@dragonxwang) - -### Fixes - -* delete commands now add text to clipboard, ex `:1,4d` - -## 0.8.0 -* Don't allow :s delimiters not allowed by vim (@jacwah) -* Backspace over empty `:` now cancels ex-mode (@shamrin) -* Added option to register alias keys in atom init config (@GertjanReynaert) -* Allow `:substitute` to replace empty with an empty string and replacing the last search item (@jazzpi) -* Added `:wall`, `:quitall` and `:wqall` commands (@caiocutrim) -* Added `:saveas` command (@bakert) - -## 0.6.0 -* No project/multiple projects paths (uses first one) -* Support for :set -* Fixes - -## 0.5.0 -* Comply with upcoming Atom API 1.0 -* Added `:d` -* Fixes - -## 0.4.1 - C-C-C-Combo Edition -* Added ex command parser, including ranges -* Added `:wq` -* Added `:s` -* Alert on unknown or invalid command - -## 0.3.0 - Extrovert Edition -* Register new commands from the outside world -* Added `:tabn`, `:tabp`, `:e`, `:enew`, and a few aliases - -## 0.2.0 - NotAOneTrickPony Edition -* Added `:quit`, `:tabedit`, `:wa`, `:split`, `:vsplit` -* Commands can take arguments - ## 0.1.0 - First Release * Every feature added * Every bug fixed diff --git a/README.md b/README.md old mode 100755 new mode 100644 index ea4a068..c15896a --- a/README.md +++ b/README.md @@ -2,67 +2,6 @@ ex-mode for Atom's vim-mode -## Use +## Usage -Install both [vim-mode-plus](https://github.com/t9md/atom-vim-mode-plus) (or -the deprecated `vim-mode`) and ex-mode. Type `:` in command mode. Enter `w` or -`write`. - -## Extend - -Use the service to register commands, from your own package, or straight from `init.coffee`: - -```coffee -# in Atom's init.coffee -atom.packages.onDidActivatePackage (pack) -> - if pack.name == 'ex-mode' - Ex = pack.mainModule.provideEx() - Ex.registerCommand 'z', -> console.log("Zzzzzz...") -``` - -You can also add aliases: - -```coffee -atom.packages.onDidActivatePackage (pack) -> - if pack.name == 'ex-mode' - Ex = pack.mainModule.provideEx() - Ex.registerAlias 'WQ', 'wq' - Ex.registerAlias 'Wq', 'wq' -``` - -## Existing commands - -This is the baseline list of commands supported in `ex-mode`. - -| Command | Operation | -| --------------------------------------- | ---------------------------------- | -| `q/quit/tabc/tabclose` | Close active tab | -| `qall/quitall` | Close all tabs | -| `tabe/tabedit/tabnew` | Open new tab | -| `e/edit/tabe/tabedit/tabnew ` | Edit given file | -| `tabn/tabnext` | Go to next tab | -| `tabp/tabprevious` | Go to previous tab | -| `tabo/tabonly` | Close other tabs | -| `w/write` | Save active tab | -| `w/write/saveas ` | Save as | -| `wall/wa` | Save all tabs | -| `sp/split` | Split window | -| `sp/split ` | Open file in split window | -| `s/substitute` | Substitute regular expression in active line | -| `vsp/vsplit` | Vertical split window | -| `vsp/vsplit ` | Open file in vertical split window | -| `delete` | Cut active line | -| `yank` | Copy active line | -| `set ` | Set options | -| `sort` | Sort all lines in file | -| `sort ` | Sort lines in line range | - -See `lib/ex.coffee` for the implementations of these commands. Contributions are very welcome! - -## Status - -Groundwork is done. More ex commands are easy to add and will be coming as time permits and contributions come in. - -## License - -MIT +Install vim-mode. Type `:` in command mode. Enter `w`. or `write`. diff --git a/keymaps/ex-mode.cson b/keymaps/ex-mode.cson index a071efd..5abddd0 100644 --- a/keymaps/ex-mode.cson +++ b/keymaps/ex-mode.cson @@ -7,10 +7,5 @@ # For more detailed documentation see # https://atom.io/docs/latest/advanced/keymaps -'atom-text-editor.vim-mode-plus:not(.insert-mode)': - ':': 'ex-mode:open' -'atom-text-editor.ex-mode-editor': - 'ctrl-c': 'ex-mode:close' - 'ctrl-[': 'ex-mode:close' 'atom-text-editor.vim-mode:not(.insert-mode)': ':': 'ex-mode:open' diff --git a/lib/autocomplete.coffee b/lib/autocomplete.coffee deleted file mode 100644 index 6775bac..0000000 --- a/lib/autocomplete.coffee +++ /dev/null @@ -1,80 +0,0 @@ -fs = require 'fs' -path = require 'path' -os = require 'os' -Ex = require './ex' - -module.exports = -class AutoComplete - constructor: (commands) -> - @commands = commands - @resetCompletion() - - resetCompletion: () -> - @autoCompleteIndex = 0 - @autoCompleteText = null - @completions = [] - - expandTilde: (filePath) -> - if filePath.charAt(0) == '~' - return os.homedir() + filePath.slice(1) - else - return filePath - - getAutocomplete: (text) -> - if !@autoCompleteText - @autoCompleteText = text - - parts = @autoCompleteText.split(' ') - cmd = parts[0] - - if parts.length > 1 - filePath = parts.slice(1).join(' ') - return @getCompletion(() => @getFilePathCompletion(cmd, filePath)) - else - return @getCompletion(() => @getCommandCompletion(cmd)) - - filterByPrefix: (commands, prefix) -> - commands.sort().filter((f) => f.startsWith(prefix)) - - getCompletion: (completeFunc) -> - if @completions.length == 0 - @completions = completeFunc() - - complete = '' - if @completions.length - complete = @completions[@autoCompleteIndex % @completions.length] - @autoCompleteIndex++ - - # Only one result so lets return this directory - if complete.endsWith('/') && @completions.length == 1 - @resetCompletion() - - return complete - - getCommandCompletion: (command) -> - return @filterByPrefix(@commands, command) - - getFilePathCompletion: (command, filePath) -> - filePath = @expandTilde(filePath) - - if filePath.endsWith(path.sep) - basePath = path.dirname(filePath + '.') - baseName = '' - else - basePath = path.dirname(filePath) - baseName = path.basename(filePath) - - try - basePathStat = fs.statSync(basePath) - if basePathStat.isDirectory() - files = fs.readdirSync(basePath) - return @filterByPrefix(files, baseName).map((f) => - filePath = path.join(basePath, f) - if fs.lstatSync(filePath).isDirectory() - return command + ' ' + filePath + path.sep - else - return command + ' ' + filePath - ) - return [] - catch err - return [] diff --git a/lib/command-error.coffee b/lib/command-error.coffee deleted file mode 100644 index c9861e9..0000000 --- a/lib/command-error.coffee +++ /dev/null @@ -1,5 +0,0 @@ -class CommandError - constructor: (@message) -> - @name = 'Command Error' - -module.exports = CommandError diff --git a/lib/command.coffee b/lib/command.coffee index 15e74fe..38d5765 100644 --- a/lib/command.coffee +++ b/lib/command.coffee @@ -1,201 +1,21 @@ ExViewModel = require './ex-view-model' Ex = require './ex' -Find = require './find' -CommandError = require './command-error' + +class CommandError + constructor: (@message) -> + @name = 'Command Error' class Command constructor: (@editor, @exState) -> - @selections = @exState.getSelections() - @viewModel = new ExViewModel(@, Object.keys(@selections).length > 0) - - parseAddr: (str, cursor) -> - row = cursor.getBufferRow() - if str is '.' - addr = row - else if str is '$' - # Lines are 0-indexed in Atom, but 1-indexed in vim. - # The two ways of getting length let us support Atom 1.19's new buffer - # implementation (https://github.com/atom/atom/pull/14435) and still - # support 1.18 and below - buffer = @editor.getBuffer() - addr = (buffer.getLineCount?() ? buffer.lines.length) - 1 - else if str[0] in ["+", "-"] - addr = 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.mark.marks[str[1]] - unless mark? - throw new CommandError("Mark #{str} not set.") - addr = mark.getEndBufferPosition().row - else if str[0] is "/" - str = str[1...] - if str[str.length-1] is "/" - str = str[...-1] - addr = Find.scanEditor(str, @editor, cursor.getCurrentLineBufferRange().end)[0] - unless addr? - throw new CommandError("Pattern not found: #{str}") - addr = addr.start.row - else if str[0] is "?" - str = str[1...] - if str[str.length-1] is "?" - str = str[...-1] - addr = Find.scanEditor(str, @editor, cursor.getCurrentLineBufferRange().start, true)[0] - unless addr? - throw new CommandError("Pattern not found: #{str[1...-1]}") - addr = addr.start.row - - 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 + @viewModel = new ExViewModel(@) 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 - # see comment in parseAddr about line length - buffer = @editor.getBuffer() - lastLine = (buffer.getLineCount?() ? buffer.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) - - cursor = @editor.getLastCursor() - - # Special case: run command on selection. This can't be handled by simply - # parsing the mark since vim-mode doesn't set it (and it would be fairly - # useless with multiple selections) - if addr1 is "'<" and addr2 is "'>" - runOverSelections = true - else - runOverSelections = false - if addr1? - address1 = @parseAddr(addr1, cursor) - else - # If no addr1 is given (,+3), assume it is '.' - address1 = cursor.getBufferRow() - if off1? - address1 += @parseOffset(off1) - - address1 = 0 if address1 is -1 - address1 = lastLine if address1 > lastLine - - if address1 < 0 - throw new CommandError('Invalid range') - - if addr2? - address2 = @parseAddr(addr2, cursor) - if off2? - address2 += @parseOffset(off2) - - address2 = 0 if address2 is -1 - address2 = lastLine if address2 > lastLine - - if address2 < 0 - 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 is equal to :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 - unless (func = Ex.singleton()[command])? - # 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] + return unless input.characters.length > 0 + func = (new Ex)[input.characters] if func? - if runOverSelections - for id, selection of @selections - bufferRange = selection.getBufferRange() - range = [bufferRange.start.row, bufferRange.end.row] - func({ range, args, @vimState, @exState, @editor }) - else - func({ range, args, @vimState, @exState, @editor }) + func() else - throw new CommandError("Not an editor command: #{input.characters}") + throw new CommandError("#{input.characters}") -module.exports = Command +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.coffee b/lib/ex-mode.coffee index d8fc766..264f2ce 100644 --- a/lib/ex-mode.coffee +++ b/lib/ex-mode.coffee @@ -1,6 +1,5 @@ GlobalExState = require './global-ex-state' ExState = require './ex-state' -Ex = require './ex' {Disposable, CompositeDisposable} = require 'event-kit' module.exports = ExMode = @@ -27,36 +26,3 @@ module.exports = ExMode = deactivate: -> @disposables.dispose() - - provideEx: -> - registerCommand: Ex.registerCommand.bind(Ex) - registerAlias: Ex.registerAlias.bind(Ex) - - consumeVim: (vim) -> - @vim = vim - @globalExState.setVim(vim) - - consumeVimModePlus: (vim) -> - this.consumeVim(vim) - - config: - splitbelow: - title: 'Split below' - description: 'when splitting, split from below' - type: 'boolean' - default: 'false' - splitright: - title: 'Split right' - description: 'when splitting, split from right' - type: 'boolean' - default: 'false' - gdefault: - title: 'Gdefault' - description: 'When on, the ":substitute" flag \'g\' is default on' - type: 'boolean' - default: 'false' - onlyCloseBuffers: - title: 'Only close buffers' - description: 'When on, quitall only closes all buffers, not entire Atom instance' - type: 'boolean' - default: 'false' diff --git a/lib/ex-normal-mode-input-element.coffee b/lib/ex-normal-mode-input-element.coffee deleted file mode 100644 index 915a5be..0000000 --- a/lib/ex-normal-mode-input-element.coffee +++ /dev/null @@ -1,71 +0,0 @@ -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') # Consider this deprecated! - @editorElement.classList.add('ex-mode-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:backspace', @backspace.bind(this)) - - atom.commands.add(@editorElement, 'core:confirm', @confirm.bind(this)) - atom.commands.add(@editorElement, 'core:cancel', @cancel.bind(this)) - atom.commands.add(@editorElement, 'ex-mode:close', @cancel.bind(this)) - atom.commands.add(@editorElement, 'blur', @cancel.bind(this)) - - backspace: -> - # pressing backspace over empty `:` should cancel ex-mode - @cancel() unless @editorElement.getModel().getText().length - - 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/lib/ex-state.coffee b/lib/ex-state.coffee index 89fbeeb..40e2632 100644 --- a/lib/ex-state.coffee +++ b/lib/ex-state.coffee @@ -1,7 +1,6 @@ {Emitter, Disposable, CompositeDisposable} = require 'event-kit' -Command = require './command' -CommandError = require './command-error' +{Command, CommandError} = require './command' class ExState constructor: (@editorElement, @globalExState) -> @@ -34,9 +33,6 @@ class ExState onDidFailToExecute: (fn) -> @emitter.on('failed-to-execute', fn) - onDidProcessOpStack: (fn) -> - @emitter.on('processed-op-stack', fn) - pushOperations: (operations) -> @opStack.push operations @@ -48,25 +44,14 @@ class ExState processOpStack: -> [command, input] = @opStack if input.characters.length > 0 - @history.unshift command try command.execute(input) + @history.unshift command catch e if (e instanceof CommandError) - atom.notifications.addError("Command error: #{e.message}") @emitter.emit('failed-to-execute') else throw e @clearOpStack() - @emitter.emit('processed-op-stack') - - # Returns all non-empty selections - getSelections: -> - filtered = {} - for id, selection of @editor.getSelections() - unless selection.isEmpty() - filtered[id] = selection - - return filtered module.exports = ExState diff --git a/lib/ex-view-model.coffee b/lib/ex-view-model.coffee index 08dd9f6..4f9675d 100644 --- a/lib/ex-view-model.coffee +++ b/lib/ex-view-model.coffee @@ -1,41 +1,20 @@ {ViewModel, Input} = require './view-model' -AutoComplete = require './autocomplete' -Ex = require './ex' module.exports = class ExViewModel extends ViewModel - constructor: (@exCommand, withSelection) -> + constructor: (@exCommand) -> super(@exCommand, class: 'command') @historyIndex = -1 - if withSelection - @view.editorElement.getModel().setText("'<,'>") - - @view.editorElement.addEventListener('keydown', @tabAutocomplete) - atom.commands.add(@view.editorElement, 'core:move-up', @increaseHistoryEx) - atom.commands.add(@view.editorElement, 'core:move-down', @decreaseHistoryEx) - - @autoComplete = new AutoComplete(Ex.getCommands()) + @view.editor.on('core:move-up', @increaseHistoryEx) + @view.editor.on('core:move-down', @decreaseHistoryEx) restoreHistory: (index) -> - @view.editorElement.getModel().setText(@history(index).value) + @view.editor.setText(@history(index).value) history: (index) -> @exState.getExHistoryItem(index) - tabAutocomplete: (event) => - if event.keyCode == 9 - event.stopPropagation() - event.preventDefault() - - completed = @autoComplete.getAutocomplete(@view.editorElement.getModel().getText()) - if completed - @view.editorElement.getModel().setText(completed) - - return false - else - @autoComplete.resetCompletion() - increaseHistoryEx: => if @history(@historyIndex + 1)? @historyIndex += 1 @@ -45,7 +24,7 @@ class ExViewModel extends ViewModel if @historyIndex <= 0 # get us back to a clean slate @historyIndex = -1 - @view.editorElement.getModel().setText('') + @view.editor.setText('') else @historyIndex -= 1 @restoreHistory(@historyIndex) diff --git a/lib/ex.coffee b/lib/ex.coffee index b262023..6d81188 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -1,472 +1,5 @@ -path = require 'path' -CommandError = require './command-error' -fs = require 'fs-plus' -VimOption = require './vim-option' -_ = require 'underscore-plus' -atom - -defer = () -> - deferred = {} - deferred.promise = new Promise((resolve, reject) -> - deferred.resolve = resolve - deferred.reject = reject - ) - return deferred - - -trySave = (func) -> - deferred = defer() - - try - response = func() - - if response instanceof Promise - response.then -> - deferred.resolve() - else - 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 - -saveAs = (filePath, editor) -> - fs.writeFileSync(filePath, editor.getText()) - -getFullPath = (filePath) -> - filePath = fs.normalize(filePath) - - if path.isAbsolute(filePath) - filePath - else if atom.project.getPaths().length == 0 - path.join(fs.normalize('~'), filePath) - else - path.join(atom.project.getPaths()[0], filePath) - -replaceGroups = (groups, string) -> - replaced = '' - escaped = false - while (char = string[0])? - string = string[1..] - if char is '\\' and not escaped - escaped = true - else if /\d/.test(char) and escaped - escaped = false - group = groups[parseInt(char)] - group ?= '' - replaced += group - else - escaped = false - replaced += char - - replaced - -getSearchTerm = (term, modifiers = {'g': true}) -> - - escaped = false - hasc = false - hasC = false - term_ = term - term = '' - for char in term_ - if char is '\\' and not escaped - escaped = true - term += char - else - if char is 'c' and escaped - hasc = true - term = term[...-1] - else if char is 'C' and escaped - hasC = true - term = term[...-1] - else if char isnt '\\' - term += char - escaped = false - - if hasC - modifiers['i'] = false - if (not hasC and not term.match('[A-Z]') and \ - atom.config.get('vim-mode.useSmartcaseForSearch')) or hasc - modifiers['i'] = true - - modFlags = Object.keys(modifiers).filter((key) -> modifiers[key]).join('') - - try - new RegExp(term, modFlags) - catch - new RegExp(_.escapeRegExp(term), modFlags) - class Ex - @singleton: => - @ex ||= new Ex - - @registerCommand: (name, func) => - @singleton()[name] = func - - @registerAlias: (alias, name) => - @singleton()[alias] = (args) => @singleton()[name](args) - - @getCommands: () => - Object.keys(Ex.singleton()).concat(Object.keys(Ex.prototype)).filter((cmd, index, list) -> - list.indexOf(cmd) == index - ) - - quit: -> - atom.workspace.getActivePane().destroyActiveItem() - - quitall: -> - if !atom.config.get('ex-mode.onlyCloseBuffers') - atom.close() - else - atom.workspace.getTextEditors().forEach (editor) -> - editor.destroy() - - q: => @quit() - - qall: => @quitall() - - tabedit: (args) => - if args.args.trim() isnt '' - @edit(args) - else - @tabnew(args) - - tabe: (args) => @tabedit(args) - - tabnew: (args) => - if args.args.trim() is '' - atom.workspace.open() - else - @tabedit(args) - - tabclose: (args) => @quit(args) - - tabc: => @tabclose() - - tabnext: -> - pane = atom.workspace.getActivePane() - pane.activateNextItem() - - tabn: => @tabnext() - - tabprevious: -> - pane = atom.workspace.getActivePane() - pane.activatePreviousItem() - - tabp: => @tabprevious() - - tabonly: -> - tabBar = atom.workspace.getPanes()[0] - tabBarElement = atom.views.getView(tabBar).querySelector(".tab-bar") - tabBarElement.querySelector(".right-clicked") && tabBarElement.querySelector(".right-clicked").classList.remove("right-clicked") - tabBarElement.querySelector(".active").classList.add("right-clicked") - atom.commands.dispatch(tabBarElement, 'tabs:close-other-tabs') - tabBarElement.querySelector(".active").classList.remove("right-clicked") - - tabo: => @tabonly() - - edit: ({ range, args, editor }) -> - filePath = args.trim() - if filePath[0] is '!' - force = true - filePath = filePath[1..].trim() - else - force = false - - if editor.isModified() and not force - throw new CommandError('No write since last change (add ! to override)') - if filePath.indexOf(' ') isnt -1 - throw new CommandError('Only one file name allowed') - - if filePath.length isnt 0 - fullPath = getFullPath(filePath) - if fullPath is editor.getPath() - editor.getBuffer().reload() - else - atom.workspace.open(fullPath) - else - if editor.getPath()? - editor.getBuffer().reload() - else - throw new CommandError('No file name') - - e: (args) => @edit(args) - - enew: -> - atom.workspace.open() - - write: ({ range, args, editor, saveas }) -> - saveas ?= false - filePath = args - if filePath[0] is '!' - force = true - filePath = filePath[1..] - else - force = false - - filePath = filePath.trim() - if filePath.indexOf(' ') isnt -1 - throw new CommandError('Only one file name allowed') - - deferred = defer() - - editor = atom.workspace.getActiveTextEditor() - - # Case 1; path is provided - if filePath.length isnt 0 - fullPath = getFullPath filePath - - # Only write when it does not exist or we have a force flag set. - if force or not fs.existsSync(fullPath) - editor.saveAs(fullPath) - return deferred.promise - - throw new CommandError("File exists (add ! to override)") - - # Case 2; no path provided, call editor save. - editor = atom.workspace.getActiveTextEditor() - - # Does the current buffer exist? - if editor.getPath()? and fs.existsSync(editor.getPath()) - trySave(-> editor.save()).then(deferred.promise) - else - # Cant see what the better API is but Pane.saveActiveItemAs() is the only call - # I could find that states it will ask the user. - trySave(-> atom.workspace.getActivePane().saveActiveItemAs()).then(deferred.promise) - - return deferred.promise - - wall: -> - atom.workspace.saveAll() - - w: (args) => - @write(args) - - wq: (args) => - @write(args).then(=> @quit()) - - wa: => - @wall() - - wqall: => - @wall() - @quitall() - - wqa: => - @wqall() - - xall: => - @wqall() - - xa: => - @wqall() - - saveas: (args) => - args.saveas = true - @write(args) - - xit: (args) => @wq(args) - - x: (args) => @xit(args) - - 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 atom.config.get('ex-mode.splitbelow') - if filePaths? and filePaths.length > 0 - newPane = pane.splitDown() - for file in filePaths - do -> - atom.workspace.openURIInPane file, newPane - else - pane.splitDown(copyActiveItem: true) - else - 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, editor, vimState }) -> - args_ = args.trimLeft() - delim = args_[0] - if /[a-z1-9\\"|]/i.test(delim) - throw new CommandError( - "Regular expressions can't be delimited by alphanumeric characters, '\\', '\"' or '|'") - args_ = args_[1..] - escapeChars = {t: '\t', n: '\n', r: '\r'} - parsed = ['', '', ''] - parsing = 0 - escaped = false - while (char = args_[0])? - args_ = args_[1..] - if char is delim - if not escaped - parsing++ - if parsing > 2 - throw new CommandError('Trailing characters') - else - parsed[parsing] = parsed[parsing][...-1] - else if char is '\\' and not escaped - parsed[parsing] += char - escaped = true - else if parsing == 1 and escaped and escapeChars[char]? - parsed[parsing] += escapeChars[char] - escaped = false - else - escaped = false - parsed[parsing] += char - - [pattern, substition, flags] = parsed - if pattern is '' - if vimState.getSearchHistoryItem? - # vim-mode - pattern = vimState.getSearchHistoryItem() - else if vimState.searchHistory? - # vim-mode-plus - pattern = vimState.searchHistory.get('prev') - - if not pattern? - atom.beep() - throw new CommandError('No previous regular expression') - else - if vimState.pushSearchHistory? - # vim-mode - vimState.pushSearchHistory(pattern) - else if vimState.searchHistory? - # vim-mode-plus - vimState.searchHistory.save(pattern) - - try - flagsObj = {} - flags.split('').forEach((flag) -> flagsObj[flag] = true) - # gdefault option - if atom.config.get('ex-mode.gdefault') - flagsObj.g = !flagsObj.g - patternRE = getSearchTerm(pattern, flagsObj) - catch e - if e.message.indexOf('Invalid flags supplied to RegExp constructor') is 0 - 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 - - editor.transact -> - for line in [range[0]..range[1]] - editor.scanInBufferRange( - patternRE, - [[line, 0], [line + 1, 0]], - ({match, replace}) -> - replace(replaceGroups(match[..], substition)) - ) - - 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 atom.config.get('ex-mode.splitright') - if filePaths? and filePaths.length > 0 - newPane = pane.splitRight() - for file in filePaths - do -> - atom.workspace.openURIInPane file, newPane - else - pane.splitRight(copyActiveItem: true) - else - 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]] - editor = atom.workspace.getActiveTextEditor() - - text = editor.getTextInBufferRange(range) - atom.clipboard.write(text) - - editor.buffer.setTextInRange(range, '') - - yank: ({ range }) -> - range = [[range[0], 0], [range[1] + 1, 0]] - txt = atom.workspace.getActiveTextEditor().getTextInBufferRange(range) - atom.clipboard.write(txt); - - set: ({ range, args }) -> - args = args.trim() - if args == "" - throw new CommandError("No option specified") - options = args.split(' ') - for option in options - do -> - if option.includes("=") - nameValPair = option.split("=") - if (nameValPair.length != 2) - throw new CommandError("Wrong option format. [name]=[value] format is expected") - optionName = nameValPair[0] - optionValue = nameValPair[1] - optionProcessor = VimOption.singleton()[optionName] - if not optionProcessor? - throw new CommandError("No such option: #{optionName}") - optionProcessor(optionValue) - else - optionProcessor = VimOption.singleton()[option] - if not optionProcessor? - throw new CommandError("No such option: #{option}") - optionProcessor() - - sort: ({ range }) => - editor = atom.workspace.getActiveTextEditor() - sortingRange = [[]] - - # If no range is provided, the entire file should be sorted. - isMultiLine = range[1] - range[0] > 1 - if isMultiLine - sortingRange = [[range[0], 0], [range[1] + 1, 0]] - else - sortingRange = [[0, 0], [editor.getLastBufferRow(), 0]] - - # Store every bufferedRow string in an array. - textLines = [] - for lineIndex in [sortingRange[0][0]..sortingRange[1][0] - 1] - textLines.push(editor.lineTextForBufferRow(lineIndex)) - - # Sort the array and join them together with newlines for writing back to the file. - sortedText = _.sortBy(textLines).join('\n') + '\n' - editor.buffer.setTextInRange(sortingRange, sortedText) + write: -> atom.workspace.getActiveEditor().save() + w: => @write() module.exports = Ex diff --git a/lib/find.coffee b/lib/find.coffee deleted file mode 100644 index 60e98b8..0000000 --- a/lib/find.coffee +++ /dev/null @@ -1,85 +0,0 @@ -_ = require 'underscore-plus' - -getSearchTerm = (term, modifiers = {'g': true}) -> - - escaped = false - hasc = false - hasC = false - term_ = term - term = '' - for char in term_ - if char is '\\' and not escaped - escaped = true - term += char - else - if char is 'c' and escaped - hasc = true - term = term[...-1] - else if char is 'C' and escaped - hasC = true - term = term[...-1] - else if char isnt '\\' - term += char - escaped = false - - if hasC - modifiers['i'] = false - if (not hasC and not term.match('[A-Z]') and \ - atom.config.get("vim-mode:useSmartcaseForSearch")) or hasc - modifiers['i'] = true - - modFlags = Object.keys(modifiers).filter((key) -> modifiers[key]).join('') - - try - new RegExp(term, modFlags) - catch - new RegExp(_.escapeRegExp(term), modFlags) - -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 - - # Returns an array of ranges of all occurences of `term` in `editor`. - # The array is sorted so that the first occurences after the cursor come - # first (and the search wraps around). If `reverse` is true, the array is - # reversed so that the first occurence before the cursor comes first. - scanEditor: (term, editor, position, reverse = false) -> - [rangesBefore, rangesAfter] = [[], []] - editor.scan getSearchTerm(term), ({range}) -> - if reverse - isBefore = range.start.compare(position) < 0 - else - isBefore = range.start.compare(position) <= 0 - - if isBefore - rangesBefore.push(range) - else - rangesAfter.push(range) - - if reverse - rangesAfter.concat(rangesBefore).reverse() - else - rangesAfter.concat(rangesBefore) -} diff --git a/lib/global-ex-state.coffee b/lib/global-ex-state.coffee index be51b85..5325113 100644 --- a/lib/global-ex-state.coffee +++ b/lib/global-ex-state.coffee @@ -1,5 +1,4 @@ class GlobalExState commandHistory: [] - setVim: (@vim) -> module.exports = GlobalExState diff --git a/lib/view-model.coffee b/lib/view-model.coffee index 742d751..7568aac 100644 --- a/lib/view-model.coffee +++ b/lib/view-model.coffee @@ -1,22 +1,18 @@ -ExNormalModeInputElement = require './ex-normal-mode-input-element' +ExCommandModeInputView = require './ex-command-mode-input-view' class ViewModel constructor: (@command, opts={}) -> {@editor, @exState} = @command - @view = new ExNormalModeInputElement().initialize(@, opts) - @editor.normalModeInputView = @view + @view = new ExCommandModeInputView(@, 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 + @exState.pushOperations(new Input('')) class Input constructor: (@characters) -> diff --git a/lib/vim-option.coffee b/lib/vim-option.coffee deleted file mode 100644 index 223395f..0000000 --- a/lib/vim-option.coffee +++ /dev/null @@ -1,65 +0,0 @@ -class VimOption - @singleton: => - @option ||= new VimOption - - list: => - atom.config.set("editor.showInvisibles", true) - - nolist: => - atom.config.set("editor.showInvisibles", false) - - number: => - atom.config.set("editor.showLineNumbers", true) - - nu: => - @number() - - nonumber: => - atom.config.set("editor.showLineNumbers", false) - - nonu: => - @nonumber() - - splitright: => - atom.config.set("ex-mode.splitright", true) - - spr: => - @splitright() - - nosplitright: => - atom.config.set("ex-mode.splitright", false) - - nospr: => - @nosplitright() - - splitbelow: => - atom.config.set("ex-mode.splitbelow", true) - - sb: => - @splitbelow() - - nosplitbelow: => - atom.config.set("ex-mode.splitbelow", false) - - nosb: => - @nosplitbelow() - - smartcase: => - atom.config.set("vim-mode.useSmartcaseForSearch", true) - - scs: => - @smartcase() - - nosmartcase: => - atom.config.set("vim-mode.useSmartcaseForSearch", false) - - noscs: => - @nosmartcase() - - gdefault: => - atom.config.set("ex-mode.gdefault", true) - - nogdefault: => - atom.config.set("ex-mode.gdefault", false) - -module.exports = VimOption diff --git a/package.json b/package.json index cda6de9..ff1c05f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ex-mode", "main": "./lib/ex-mode", - "version": "0.18.0", + "version": "0.1.0", "description": "Ex for Atom's vim-mode", "activationCommands": { "atom-workspace": "ex-mode:open" @@ -9,36 +9,10 @@ "repository": "https://github.com/lloeki/ex-mode", "license": "MIT", "engines": { - "atom": ">=0.200.0 <2.0.0" + "atom": ">=0.174.0 <2.0.0" }, "dependencies": { "underscore-plus": "1.x", - "event-kit": "^0.7.2", - "space-pen": "^5.1.1", - "atom-space-pen-views": "^2.0.4", - "fs-plus": "^2.2.8" - }, - "consumedServices": { - "vim-mode": { - "versions": { - "^0.1.0": "consumeVim" - } - }, - "vim-mode-plus": { - "versions": { - "^0.1.0": "consumeVimModePlus" - } - } - }, - "providedServices": { - "ex-mode": { - "description": "Ex commands", - "versions": { - "0.20.0": "provideEx" - } - } - }, - "devDependencies": { - "node-uuid": "^1.4.2" + "event-kit": "^0.7.2" } } diff --git a/spec/autocomplete-spec.coffee b/spec/autocomplete-spec.coffee deleted file mode 100644 index 000e9c2..0000000 --- a/spec/autocomplete-spec.coffee +++ /dev/null @@ -1,100 +0,0 @@ -fs = require 'fs-plus' -path = require 'path' -os = require 'os' -uuid = require 'node-uuid' - -helpers = require './spec-helper' -AutoComplete = require '../lib/autocomplete' - -describe "autocomplete functionality", -> - beforeEach -> - @autoComplete = new AutoComplete(['taba', 'tabb', 'tabc']) - @testDir = path.join(os.tmpdir(), "atom-ex-mode-spec-#{uuid.v4()}") - @nonExistentTestDir = path.join(os.tmpdir(), "atom-ex-mode-spec-#{uuid.v4()}") - @testFile1 = path.join(@testDir, "atom-ex-testfile-a.txt") - @testFile2 = path.join(@testDir, "atom-ex-testfile-b.txt") - - runs => - fs.makeTreeSync(@testDir) - fs.closeSync(fs.openSync(@testFile1, 'w')); - fs.closeSync(fs.openSync(@testFile2, 'w')); - spyOn(@autoComplete, 'resetCompletion').andCallThrough() - spyOn(@autoComplete, 'getFilePathCompletion').andCallThrough() - spyOn(@autoComplete, 'getCommandCompletion').andCallThrough() - - afterEach -> - fs.removeSync(@testDir) - - describe "autocomplete commands", -> - beforeEach -> - @completed = @autoComplete.getAutocomplete('tab') - - it "returns taba", -> - expect(@completed).toEqual('taba') - - it "calls command function", -> - expect(@autoComplete.getCommandCompletion.callCount).toBe(1) - - describe "autocomplete commands, then autoComplete again", -> - beforeEach -> - @completed = @autoComplete.getAutocomplete('tab') - @completed = @autoComplete.getAutocomplete('tab') - - it "returns tabb", -> - expect(@completed).toEqual('tabb') - - it "calls command function", -> - expect(@autoComplete.getCommandCompletion.callCount).toBe(1) - - describe "autocomplete directory", -> - beforeEach -> - filePath = path.join(os.tmpdir(), 'atom-ex-mode-spec-') - @completed = @autoComplete.getAutocomplete('tabe ' + filePath) - - it "returns testDir", -> - expected = 'tabe ' + @testDir + path.sep - expect(@completed).toEqual(expected) - - it "clears autocomplete", -> - expect(@autoComplete.resetCompletion.callCount).toBe(1) - - describe "autocomplete directory, then autocomplete again", -> - beforeEach -> - filePath = path.join(os.tmpdir(), 'atom-ex-mode-spec-') - @completed = @autoComplete.getAutocomplete('tabe ' + filePath) - @completed = @autoComplete.getAutocomplete(@completed) - - it "returns test file 1", -> - expect(@completed).toEqual('tabe ' + @testFile1) - - it "lists files twice", -> - expect(@autoComplete.getFilePathCompletion.callCount).toBe(2) - - describe "autocomplete full directory, then autocomplete again", -> - beforeEach -> - filePath = path.join(@testDir, 'a') - @completed = @autoComplete.getAutocomplete('tabe ' + filePath) - @completed = @autoComplete.getAutocomplete(@completed) - - it "returns test file 2", -> - expect(@completed).toEqual('tabe ' + @testFile2) - - it "lists files once", -> - expect(@autoComplete.getFilePathCompletion.callCount).toBe(1) - - describe "autocomplete non existent directory", -> - beforeEach -> - @completed = @autoComplete.getAutocomplete('tabe ' + @nonExistentTestDir) - - it "returns no completions", -> - expected = ''; - expect(@completed).toEqual(expected) - - describe "autocomplete existing file as directory", -> - beforeEach -> - filePath = @testFile1 + path.sep - @completed = @autoComplete.getAutocomplete('tabe ' + filePath) - - it "returns no completions", -> - expected = ''; - expect(@completed).toEqual(expected) diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee deleted file mode 100644 index 29f211e..0000000 --- a/spec/ex-commands-spec.coffee +++ /dev/null @@ -1,1019 +0,0 @@ -fs = require 'fs-plus' -path = require 'path' -os = require 'os' -uuid = require 'node-uuid' -helpers = require './spec-helper' - -ExClass = require('../lib/ex') -Ex = ExClass.singleton() - -describe "the commands", -> - [editor, editorElement, vimState, exState, dir, dir2] = [] - projectPath = (fileName) -> path.join(dir, fileName) - beforeEach -> - vimMode = atom.packages.loadPackage('vim-mode-plus') - exMode = atom.packages.loadPackage('ex-mode') - waitsForPromise -> - activationPromise = exMode.activate() - helpers.activateExMode() - activationPromise - - runs -> - spyOn(exMode.mainModule.globalExState, 'setVim').andCallThrough() - - waitsForPromise -> - vimMode.activate() - - waitsFor -> - exMode.mainModule.globalExState.setVim.calls.length > 0 - - runs -> - dir = path.join(os.tmpdir(), "atom-ex-mode-spec-#{uuid.v4()}") - dir2 = path.join(os.tmpdir(), "atom-ex-mode-spec-#{uuid.v4()}") - fs.makeTreeSync(dir) - fs.makeTreeSync(dir2) - atom.project.setPaths([dir, dir2]) - - helpers.getEditorElement (element) -> - atom.commands.dispatch(element, "ex-mode:open") - atom.commands.dispatch(element.getModel().normalModeInputView.editorElement, - "core:cancel") - editorElement = element - editor = editorElement.getModel() - vimState = vimMode.mainModule.getEditorState(editor) - exState = exMode.mainModule.exStates.get(editor) - vimState.resetNormalMode() - editor.setText("abc\ndef\nabc\ndef") - - afterEach -> - fs.removeSync(dir) - fs.removeSync(dir2) - - keydown = (key, options={}) -> - options.element ?= editorElement - helpers.keydown(key, options) - - normalModeInputKeydown = (key, opts = {}) -> - editor.normalModeInputView.editorElement.getModel().setText(key) - - submitNormalModeInputText = (text) -> - commandEditor = editor.normalModeInputView.editorElement - commandEditor.getModel().setText(text) - atom.commands.dispatch(commandEditor, "core:confirm") - - openEx = -> - atom.commands.dispatch(editorElement, "ex-mode:open") - - describe "as a motion", -> - beforeEach -> - editor.setCursorBufferPosition([0, 0]) - - it "moves the cursor to a specific line", -> - openEx() - submitNormalModeInputText '2' - - expect(editor.getCursorBufferPosition()).toEqual [1, 0] - - it "moves to the second address", -> - openEx() - submitNormalModeInputText '1,3' - - expect(editor.getCursorBufferPosition()).toEqual [2, 0] - - it "works with offsets", -> - openEx() - submitNormalModeInputText '2+1' - expect(editor.getCursorBufferPosition()).toEqual [2, 0] - - openEx() - submitNormalModeInputText '-2' - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - it "limits to the last line", -> - openEx() - submitNormalModeInputText '10' - expect(editor.getCursorBufferPosition()).toEqual [3, 0] - editor.setCursorBufferPosition([0, 0]) - - openEx() - submitNormalModeInputText '3,10' - expect(editor.getCursorBufferPosition()).toEqual [3, 0] - editor.setCursorBufferPosition([0, 0]) - - openEx() - submitNormalModeInputText '$+1000' - expect(editor.getCursorBufferPosition()).toEqual [3, 0] - editor.setCursorBufferPosition([0, 0]) - - it "goes to the first line with address 0", -> - editor.setCursorBufferPosition([2, 0]) - openEx() - submitNormalModeInputText '0' - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - editor.setCursorBufferPosition([2, 0]) - openEx() - submitNormalModeInputText '0,0' - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - it "doesn't move when the address is the current line", -> - openEx() - submitNormalModeInputText '.' - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - openEx() - submitNormalModeInputText ',' - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - it "moves to the last line", -> - openEx() - submitNormalModeInputText '$' - expect(editor.getCursorBufferPosition()).toEqual [3, 0] - - it "moves to a mark's line", -> - keydown('l') - keydown('m') - normalModeInputKeydown 'a' - keydown('j') - openEx() - submitNormalModeInputText "'a" - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - it "moves to a specified search", -> - openEx() - submitNormalModeInputText '/def' - expect(editor.getCursorBufferPosition()).toEqual [1, 0] - - editor.setCursorBufferPosition([2, 0]) - openEx() - submitNormalModeInputText '?def' - expect(editor.getCursorBufferPosition()).toEqual [1, 0] - - editor.setCursorBufferPosition([3, 0]) - openEx() - submitNormalModeInputText '/ef' - expect(editor.getCursorBufferPosition()).toEqual [1, 0] - - describe ":write", -> - describe "when editing a new file", -> - beforeEach -> - editor.getBuffer().setText('abc\ndef') - - it "opens the save dialog", -> - spyOn(atom, 'showSaveDialogSync') - openEx() - submitNormalModeInputText('write') - expect(atom.showSaveDialogSync).toHaveBeenCalled() - - it "saves when a path is specified in the save dialog", -> - filePath = projectPath('write-from-save-dialog') - spyOn(atom, 'showSaveDialogSync').andReturn(filePath) - openEx() - submitNormalModeInputText('write') - expect(fs.existsSync(filePath)).toBe(true) - expect(fs.readFileSync(filePath, 'utf-8')).toEqual('abc\ndef') - expect(editor.isModified()).toBe(false) - - it "saves when a path is specified in the save dialog", -> - spyOn(atom, 'showSaveDialogSync').andReturn(undefined) - spyOn(fs, 'writeFileSync') - openEx() - submitNormalModeInputText('write') - expect(fs.writeFileSync.calls.length).toBe(0) - - describe "when editing an existing file", -> - filePath = '' - i = 0 - - beforeEach -> - i++ - filePath = projectPath("write-#{i}") - editor.setText('abc\ndef') - editor.saveAs(filePath) - - it "saves the file", -> - editor.setText('abc') - openEx() - submitNormalModeInputText('write') - expect(fs.readFileSync(filePath, 'utf-8')).toEqual('abc') - expect(editor.isModified()).toBe(false) - - describe "with a specified path", -> - newPath = '' - - beforeEach -> - newPath = path.relative(dir, "#{filePath}.new") - editor.getBuffer().setText('abc') - openEx() - - afterEach -> - submitNormalModeInputText("write #{newPath}") - newPath = path.resolve(dir, fs.normalize(newPath)) - expect(fs.existsSync(newPath)).toBe(true) - expect(fs.readFileSync(newPath, 'utf-8')).toEqual('abc') - expect(editor.isModified()).toBe(true) - fs.removeSync(newPath) - - it "saves to the path", -> - - it "expands .", -> - newPath = path.join('.', newPath) - - it "expands ..", -> - newPath = path.join('..', newPath) - - it "expands ~", -> - newPath = path.join('~', newPath) - - it "throws an error with more than one path", -> - openEx() - submitNormalModeInputText('write path1 path2') - expect(atom.notifications.notifications[0].message).toEqual( - 'Command error: Only one file name allowed' - ) - - describe "when the file already exists", -> - existsPath = '' - - beforeEach -> - existsPath = projectPath('write-exists') - fs.writeFileSync(existsPath, 'abc') - - afterEach -> - fs.removeSync(existsPath) - - it "throws an error if the file already exists", -> - openEx() - submitNormalModeInputText("write #{existsPath}") - expect(atom.notifications.notifications[0].message).toEqual( - 'Command error: File exists (add ! to override)' - ) - expect(fs.readFileSync(existsPath, 'utf-8')).toEqual('abc') - - it "writes if forced with :write!", -> - openEx() - submitNormalModeInputText("write! #{existsPath}") - expect(atom.notifications.notifications).toEqual([]) - expect(fs.readFileSync(existsPath, 'utf-8')).toEqual('abc\ndef') - - describe ":wall", -> - it "saves all", -> - spyOn(atom.workspace, 'saveAll') - openEx() - submitNormalModeInputText('wall') - expect(atom.workspace.saveAll).toHaveBeenCalled() - - describe ":saveas", -> - describe "when editing a new file", -> - beforeEach -> - editor.getBuffer().setText('abc\ndef') - - it "opens the save dialog", -> - spyOn(atom, 'showSaveDialogSync') - openEx() - submitNormalModeInputText('saveas') - expect(atom.showSaveDialogSync).toHaveBeenCalled() - - it "saves when a path is specified in the save dialog", -> - filePath = projectPath('saveas-from-save-dialog') - spyOn(atom, 'showSaveDialogSync').andReturn(filePath) - openEx() - submitNormalModeInputText('saveas') - expect(fs.existsSync(filePath)).toBe(true) - expect(fs.readFileSync(filePath, 'utf-8')).toEqual('abc\ndef') - - it "saves when a path is specified in the save dialog", -> - spyOn(atom, 'showSaveDialogSync').andReturn(undefined) - spyOn(fs, 'writeFileSync') - openEx() - submitNormalModeInputText('saveas') - expect(fs.writeFileSync.calls.length).toBe(0) - - describe "when editing an existing file", -> - filePath = '' - i = 0 - - beforeEach -> - i++ - filePath = projectPath("saveas-#{i}") - editor.setText('abc\ndef') - editor.saveAs(filePath) - - it "complains if no path given", -> - editor.setText('abc') - openEx() - submitNormalModeInputText('saveas') - expect(atom.notifications.notifications[0].message).toEqual( - 'Command error: Argument required' - ) - - describe "with a specified path", -> - newPath = '' - - beforeEach -> - newPath = path.relative(dir, "#{filePath}.new") - editor.getBuffer().setText('abc') - openEx() - - afterEach -> - submitNormalModeInputText("saveas #{newPath}") - newPath = path.resolve(dir, fs.normalize(newPath)) - expect(fs.existsSync(newPath)).toBe(true) - expect(fs.readFileSync(newPath, 'utf-8')).toEqual('abc') - expect(editor.isModified()).toBe(false) - fs.removeSync(newPath) - - it "saves to the path", -> - - it "expands .", -> - newPath = path.join('.', newPath) - - it "expands ..", -> - newPath = path.join('..', newPath) - - it "expands ~", -> - newPath = path.join('~', newPath) - - it "throws an error with more than one path", -> - openEx() - submitNormalModeInputText('saveas path1 path2') - expect(atom.notifications.notifications[0].message).toEqual( - 'Command error: Only one file name allowed' - ) - - describe "when the file already exists", -> - existsPath = '' - - beforeEach -> - existsPath = projectPath('saveas-exists') - fs.writeFileSync(existsPath, 'abc') - - afterEach -> - fs.removeSync(existsPath) - - it "throws an error if the file already exists", -> - openEx() - submitNormalModeInputText("saveas #{existsPath}") - expect(atom.notifications.notifications[0].message).toEqual( - 'Command error: File exists (add ! to override)' - ) - expect(fs.readFileSync(existsPath, 'utf-8')).toEqual('abc') - - it "writes if forced with :saveas!", -> - openEx() - submitNormalModeInputText("saveas! #{existsPath}") - expect(atom.notifications.notifications).toEqual([]) - expect(fs.readFileSync(existsPath, 'utf-8')).toEqual('abc\ndef') - - describe ":quit", -> - pane = null - beforeEach -> - waitsForPromise -> - pane = atom.workspace.getActivePane() - spyOn(pane, 'destroyActiveItem').andCallThrough() - atom.workspace.open() - - it "closes the active pane item if not modified", -> - openEx() - submitNormalModeInputText('quit') - expect(pane.destroyActiveItem).toHaveBeenCalled() - expect(pane.getItems().length).toBe(1) - - describe "when the active pane item is modified", -> - beforeEach -> - editor.getBuffer().setText('def') - - it "opens the prompt to save", -> - spyOn(pane, 'promptToSaveItem') - openEx() - submitNormalModeInputText('quit') - expect(pane.promptToSaveItem).toHaveBeenCalled() - - describe ":quitall", -> - it "closes Atom", -> - spyOn(atom, 'close') - openEx() - submitNormalModeInputText('quitall') - expect(atom.close).toHaveBeenCalled() - - describe ":tabclose", -> - it "acts as an alias to :quit", -> - spyOn(Ex, 'tabclose').andCallThrough() - spyOn(Ex, 'quit').andCallThrough() - openEx() - submitNormalModeInputText('tabclose') - expect(Ex.quit).toHaveBeenCalledWith(Ex.tabclose.calls[0].args...) - - describe ":tabnext", -> - pane = null - beforeEach -> - waitsForPromise -> - pane = atom.workspace.getActivePane() - atom.workspace.open().then -> atom.workspace.open() - .then -> atom.workspace.open() - - it "switches to the next tab", -> - pane.activateItemAtIndex(1) - openEx() - submitNormalModeInputText('tabnext') - expect(pane.getActiveItemIndex()).toBe(2) - - it "wraps around", -> - pane.activateItemAtIndex(pane.getItems().length - 1) - openEx() - submitNormalModeInputText('tabnext') - expect(pane.getActiveItemIndex()).toBe(0) - - describe ":tabprevious", -> - pane = null - beforeEach -> - waitsForPromise -> - pane = atom.workspace.getActivePane() - atom.workspace.open().then -> atom.workspace.open() - .then -> atom.workspace.open() - - it "switches to the previous tab", -> - pane.activateItemAtIndex(1) - openEx() - submitNormalModeInputText('tabprevious') - expect(pane.getActiveItemIndex()).toBe(0) - - it "wraps around", -> - pane.activateItemAtIndex(0) - openEx() - submitNormalModeInputText('tabprevious') - expect(pane.getActiveItemIndex()).toBe(pane.getItems().length - 1) - - describe ":wq", -> - beforeEach -> - spyOn(Ex, 'write').andCallThrough() - spyOn(Ex, 'quit') - - it "writes the file, then quits", -> - spyOn(atom, 'showSaveDialogSync').andReturn(projectPath('wq-1')) - openEx() - submitNormalModeInputText('wq') - expect(Ex.write).toHaveBeenCalled() - # Since `:wq` only calls `:quit` after `:write` is finished, we need to - # wait a bit for the `:quit` call to occur - waitsFor((-> Ex.quit.wasCalled), "the :quit command to be called", 100) - - it "doesn't quit when the file is new and no path is specified in the save dialog", -> - spyOn(atom, 'showSaveDialogSync').andReturn(undefined) - openEx() - submitNormalModeInputText('wq') - expect(Ex.write).toHaveBeenCalled() - wasNotCalled = false - # FIXME: This seems dangerous, but setTimeout somehow doesn't work. - setImmediate((-> - wasNotCalled = not Ex.quit.wasCalled)) - waitsFor((-> wasNotCalled), 100) - - it "passes the file name", -> - openEx() - submitNormalModeInputText('wq wq-2') - expect(Ex.write) - .toHaveBeenCalled() - expect(Ex.write.calls[0].args[0].args.trim()).toEqual('wq-2') - waitsFor((-> Ex.quit.wasCalled), "the :quit command to be called", 100) - - describe ":xit", -> - it "acts as an alias to :wq", -> - spyOn(Ex, 'wq') - openEx() - submitNormalModeInputText('xit') - expect(Ex.wq).toHaveBeenCalled() - - describe ":x", -> - it "acts as an alias to :xit", -> - spyOn(Ex, 'xit') - openEx() - submitNormalModeInputText('x') - expect(Ex.xit).toHaveBeenCalled() - - describe ":wqall", -> - it "calls :wall, then :quitall", -> - spyOn(Ex, 'wall') - spyOn(Ex, 'quitall') - openEx() - submitNormalModeInputText('wqall') - expect(Ex.wall).toHaveBeenCalled() - expect(Ex.quitall).toHaveBeenCalled() - - describe ":edit", -> - describe "without a file name", -> - it "reloads the file from the disk", -> - filePath = projectPath("edit-1") - editor.getBuffer().setText('abc') - editor.saveAs(filePath) - fs.writeFileSync(filePath, 'def') - openEx() - submitNormalModeInputText('edit') - # Reloading takes a bit - waitsFor((-> editor.getText() is 'def'), - "the editor's content to change", 100) - - it "doesn't reload when the file has been modified", -> - filePath = projectPath("edit-2") - editor.getBuffer().setText('abc') - editor.saveAs(filePath) - editor.getBuffer().setText('abcd') - fs.writeFileSync(filePath, 'def') - openEx() - submitNormalModeInputText('edit') - expect(atom.notifications.notifications[0].message).toEqual( - 'Command error: No write since last change (add ! to override)') - isntDef = false - setImmediate(-> isntDef = editor.getText() isnt 'def') - waitsFor((-> isntDef), "the editor's content not to change", 50) - - it "reloads when the file has been modified and it is forced", -> - filePath = projectPath("edit-3") - editor.getBuffer().setText('abc') - editor.saveAs(filePath) - editor.getBuffer().setText('abcd') - fs.writeFileSync(filePath, 'def') - openEx() - submitNormalModeInputText('edit!') - expect(atom.notifications.notifications.length).toBe(0) - waitsFor((-> editor.getText() is 'def') - "the editor's content to change", 50) - - it "throws an error when editing a new file", -> - editor.getBuffer().reload() - openEx() - submitNormalModeInputText('edit') - expect(atom.notifications.notifications[0].message).toEqual( - 'Command error: No file name') - atom.commands.dispatch(editorElement, 'ex-mode:open') - submitNormalModeInputText('edit!') - expect(atom.notifications.notifications[1].message).toEqual( - 'Command error: No file name') - - describe "with a file name", -> - beforeEach -> - spyOn(atom.workspace, 'open') - editor.getBuffer().reload() - - it "opens the specified path", -> - filePath = projectPath('edit-new-test') - openEx() - submitNormalModeInputText("edit #{filePath}") - expect(atom.workspace.open).toHaveBeenCalledWith(filePath) - - it "opens a relative path", -> - openEx() - submitNormalModeInputText('edit edit-relative-test') - expect(atom.workspace.open).toHaveBeenCalledWith( - projectPath('edit-relative-test')) - - it "throws an error if trying to open more than one file", -> - openEx() - submitNormalModeInputText('edit edit-new-test-1 edit-new-test-2') - expect(atom.workspace.open.callCount).toBe(0) - expect(atom.notifications.notifications[0].message).toEqual( - 'Command error: Only one file name allowed') - - describe ":tabedit", -> - it "acts as an alias to :edit if supplied with a path", -> - spyOn(Ex, 'tabedit').andCallThrough() - spyOn(Ex, 'edit') - openEx() - submitNormalModeInputText('tabedit tabedit-test') - expect(Ex.edit).toHaveBeenCalledWith(Ex.tabedit.calls[0].args...) - - it "acts as an alias to :tabnew if not supplied with a path", -> - spyOn(Ex, 'tabedit').andCallThrough() - spyOn(Ex, 'tabnew') - openEx() - submitNormalModeInputText('tabedit ') - expect(Ex.tabnew) - .toHaveBeenCalledWith(Ex.tabedit.calls[0].args...) - - describe ":tabnew", -> - it "opens a new tab", -> - spyOn(atom.workspace, 'open') - openEx() - submitNormalModeInputText('tabnew') - expect(atom.workspace.open).toHaveBeenCalled() - - it "opens a new tab for editing when provided an argument", -> - spyOn(Ex, 'tabnew').andCallThrough() - spyOn(Ex, 'tabedit') - openEx() - submitNormalModeInputText('tabnew tabnew-test') - expect(Ex.tabedit) - .toHaveBeenCalledWith(Ex.tabnew.calls[0].args...) - - describe ":split", -> - it "splits the current file upwards/downward", -> - pane = atom.workspace.getActivePane() - if atom.config.get('ex-mode.splitbelow') - spyOn(pane, 'splitDown').andCallThrough() - filePath = projectPath('split') - editor.saveAs(filePath) - openEx() - submitNormalModeInputText('split') - expect(pane.splitDown).toHaveBeenCalled() - else - spyOn(pane, 'splitUp').andCallThrough() - filePath = projectPath('split') - editor.saveAs(filePath) - openEx() - submitNormalModeInputText('split') - expect(pane.splitUp).toHaveBeenCalled() - # FIXME: Should test whether the new pane contains a TextEditor - # pointing to the same path - - describe ":vsplit", -> - it "splits the current file to the left/right", -> - if atom.config.get('ex-mode.splitright') - pane = atom.workspace.getActivePane() - spyOn(pane, 'splitRight').andCallThrough() - filePath = projectPath('vsplit') - editor.saveAs(filePath) - openEx() - submitNormalModeInputText('vsplit') - expect(pane.splitLeft).toHaveBeenCalled() - else - pane = atom.workspace.getActivePane() - spyOn(pane, 'splitLeft').andCallThrough() - filePath = projectPath('vsplit') - editor.saveAs(filePath) - openEx() - submitNormalModeInputText('vsplit') - expect(pane.splitLeft).toHaveBeenCalled() - # FIXME: Should test whether the new pane contains a TextEditor - # pointing to the same path - - describe ":delete", -> - beforeEach -> - editor.setText('abc\ndef\nghi\njkl') - editor.setCursorBufferPosition([2, 0]) - - it "deletes the current line", -> - openEx() - submitNormalModeInputText('delete') - expect(editor.getText()).toEqual('abc\ndef\njkl') - - it "copies the deleted text", -> - openEx() - submitNormalModeInputText('delete') - expect(atom.clipboard.read()).toEqual('ghi\n') - - it "deletes the lines in the given range", -> - processedOpStack = false - exState.onDidProcessOpStack -> processedOpStack = true - openEx() - submitNormalModeInputText('1,2delete') - expect(editor.getText()).toEqual('ghi\njkl') - - waitsFor -> processedOpStack - editor.setText('abc\ndef\nghi\njkl') - editor.setCursorBufferPosition([1, 1]) - atom.commands.dispatch(editorElement, 'ex-mode:open') - submitNormalModeInputText(',/k/delete') - expect(editor.getText()).toEqual('abc\n') - - it "undos deleting several lines at once", -> - openEx() - submitNormalModeInputText('-1,.delete') - expect(editor.getText()).toEqual('abc\njkl') - atom.commands.dispatch(editorElement, 'core:undo') - expect(editor.getText()).toEqual('abc\ndef\nghi\njkl') - - describe ":substitute", -> - beforeEach -> - editor.setText('abcaABC\ndefdDEF\nabcaABC') - editor.setCursorBufferPosition([0, 0]) - - it "replaces a character on the current line", -> - openEx() - submitNormalModeInputText(':substitute /a/x') - expect(editor.getText()).toEqual('xbcaABC\ndefdDEF\nabcaABC') - - it "doesn't need a space before the arguments", -> - openEx() - submitNormalModeInputText(':substitute/a/x') - expect(editor.getText()).toEqual('xbcaABC\ndefdDEF\nabcaABC') - - it "respects modifiers passed to it", -> - openEx() - submitNormalModeInputText(':substitute/a/x/g') - expect(editor.getText()).toEqual('xbcxABC\ndefdDEF\nabcaABC') - - atom.commands.dispatch(editorElement, 'ex-mode:open') - submitNormalModeInputText(':substitute/a/x/gi') - expect(editor.getText()).toEqual('xbcxxBC\ndefdDEF\nabcaABC') - - it "replaces on multiple lines", -> - openEx() - submitNormalModeInputText(':%substitute/abc/ghi') - expect(editor.getText()).toEqual('ghiaABC\ndefdDEF\nghiaABC') - - atom.commands.dispatch(editorElement, 'ex-mode:open') - submitNormalModeInputText(':%substitute/abc/ghi/ig') - expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nghiaghi') - - it "set gdefault option", -> - openEx() - atom.config.set('ex-mode.gdefault', true) - submitNormalModeInputText(':substitute/a/x') - expect(editor.getText()).toEqual('xbcxABC\ndefdDEF\nabcaABC') - - atom.commands.dispatch(editorElement, 'ex-mode:open') - atom.config.set('ex-mode.gdefault', true) - submitNormalModeInputText(':substitute/a/x/g') - expect(editor.getText()).toEqual('xbcaABC\ndefdDEF\nabcaABC') - - describe ":yank", -> - beforeEach -> - editor.setText('abc\ndef\nghi\njkl') - editor.setCursorBufferPosition([2, 0]) - - it "yanks the current line", -> - openEx() - submitNormalModeInputText('yank') - expect(atom.clipboard.read()).toEqual('ghi\n') - - it "yanks the lines in the given range", -> - openEx() - submitNormalModeInputText('1,2yank') - expect(atom.clipboard.read()).toEqual('abc\ndef\n') - - describe "illegal delimiters", -> - test = (delim) -> - openEx() - submitNormalModeInputText(":substitute #{delim}a#{delim}x#{delim}gi") - expect(atom.notifications.notifications[0].message).toEqual( - "Command error: Regular expressions can't be delimited by alphanumeric characters, '\\', '\"' or '|'") - expect(editor.getText()).toEqual('abcaABC\ndefdDEF\nabcaABC') - - it "can't be delimited by letters", -> test 'n' - it "can't be delimited by numbers", -> test '3' - it "can't be delimited by '\\'", -> test '\\' - it "can't be delimited by '\"'", -> test '"' - it "can't be delimited by '|'", -> test '|' - - describe "empty replacement", -> - beforeEach -> - editor.setText('abcabc\nabcabc') - - it "removes the pattern without modifiers", -> - openEx() - submitNormalModeInputText(":substitute/abc//") - expect(editor.getText()).toEqual('abc\nabcabc') - - it "removes the pattern with modifiers", -> - openEx() - submitNormalModeInputText(":substitute/abc//g") - expect(editor.getText()).toEqual('\nabcabc') - - describe "replacing with escape sequences", -> - beforeEach -> - editor.setText('abc,def,ghi') - - test = (escapeChar, escaped) -> - openEx() - submitNormalModeInputText(":substitute/,/\\#{escapeChar}/g") - expect(editor.getText()).toEqual("abc#{escaped}def#{escaped}ghi") - - it "replaces with a tab", -> test('t', '\t') - it "replaces with a linefeed", -> test('n', '\n') - it "replaces with a carriage return", -> test('r', '\r') - - describe "case sensitivity", -> - describe "respects the smartcase setting", -> - beforeEach -> - editor.setText('abcaABC\ndefdDEF\nabcaABC') - - it "uses case sensitive search if smartcase is off and the pattern is lowercase", -> - atom.config.set('vim-mode.useSmartcaseForSearch', false) - openEx() - submitNormalModeInputText(':substitute/abc/ghi/g') - expect(editor.getText()).toEqual('ghiaABC\ndefdDEF\nabcaABC') - - it "uses case sensitive search if smartcase is off and the pattern is uppercase", -> - editor.setText('abcaABC\ndefdDEF\nabcaABC') - openEx() - submitNormalModeInputText(':substitute/ABC/ghi/g') - expect(editor.getText()).toEqual('abcaghi\ndefdDEF\nabcaABC') - - it "uses case insensitive search if smartcase is on and the pattern is lowercase", -> - editor.setText('abcaABC\ndefdDEF\nabcaABC') - atom.config.set('vim-mode.useSmartcaseForSearch', true) - openEx() - submitNormalModeInputText(':substitute/abc/ghi/g') - expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nabcaABC') - - it "uses case sensitive search if smartcase is on and the pattern is uppercase", -> - editor.setText('abcaABC\ndefdDEF\nabcaABC') - openEx() - submitNormalModeInputText(':substitute/ABC/ghi/g') - expect(editor.getText()).toEqual('abcaghi\ndefdDEF\nabcaABC') - - describe "\\c and \\C in the pattern", -> - beforeEach -> - editor.setText('abcaABC\ndefdDEF\nabcaABC') - - it "uses case insensitive search if smartcase is off and \c is in the pattern", -> - atom.config.set('vim-mode.useSmartcaseForSearch', false) - openEx() - submitNormalModeInputText(':substitute/abc\\c/ghi/g') - expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nabcaABC') - - it "doesn't matter where in the pattern \\c is", -> - atom.config.set('vim-mode.useSmartcaseForSearch', false) - openEx() - submitNormalModeInputText(':substitute/a\\cbc/ghi/g') - expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nabcaABC') - - it "uses case sensitive search if smartcase is on, \\C is in the pattern and the pattern is lowercase", -> - atom.config.set('vim-mode.useSmartcaseForSearch', true) - openEx() - submitNormalModeInputText(':substitute/a\\Cbc/ghi/g') - expect(editor.getText()).toEqual('ghiaABC\ndefdDEF\nabcaABC') - - it "overrides \\C with \\c if \\C comes first", -> - atom.config.set('vim-mode.useSmartcaseForSearch', true) - openEx() - submitNormalModeInputText(':substitute/a\\Cb\\cc/ghi/g') - expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nabcaABC') - - it "overrides \\C with \\c if \\c comes first", -> - atom.config.set('vim-mode.useSmartcaseForSearch', true) - openEx() - submitNormalModeInputText(':substitute/a\\cb\\Cc/ghi/g') - expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nabcaABC') - - it "overrides an appended /i flag with \\C", -> - atom.config.set('vim-mode.useSmartcaseForSearch', true) - openEx() - submitNormalModeInputText(':substitute/ab\\Cc/ghi/gi') - expect(editor.getText()).toEqual('ghiaABC\ndefdDEF\nabcaABC') - - describe "capturing groups", -> - beforeEach -> - editor.setText('abcaABC\ndefdDEF\nabcaABC') - - it "replaces \\1 with the first group", -> - openEx() - submitNormalModeInputText(':substitute/bc(.{2})/X\\1X') - expect(editor.getText()).toEqual('aXaAXBC\ndefdDEF\nabcaABC') - - it "replaces multiple groups", -> - openEx() - submitNormalModeInputText(':substitute/a([a-z]*)aA([A-Z]*)/X\\1XY\\2Y') - expect(editor.getText()).toEqual('XbcXYBCY\ndefdDEF\nabcaABC') - - it "replaces \\0 with the entire match", -> - openEx() - submitNormalModeInputText(':substitute/ab(ca)AB/X\\0X') - expect(editor.getText()).toEqual('XabcaABXC\ndefdDEF\nabcaABC') - - describe ":set", -> - it "throws an error without a specified option", -> - openEx() - submitNormalModeInputText(':set') - expect(atom.notifications.notifications[0].message).toEqual( - 'Command error: No option specified') - - it "sets multiple options at once", -> - atom.config.set('editor.showInvisibles', false) - atom.config.set('editor.showLineNumbers', false) - openEx() - submitNormalModeInputText(':set list number') - expect(atom.config.get('editor.showInvisibles')).toBe(true) - expect(atom.config.get('editor.showLineNumbers')).toBe(true) - - describe "the options", -> - beforeEach -> - atom.config.set('editor.showInvisibles', false) - atom.config.set('editor.showLineNumbers', false) - - it "sets (no)list", -> - openEx() - submitNormalModeInputText(':set list') - expect(atom.config.get('editor.showInvisibles')).toBe(true) - atom.commands.dispatch(editorElement, 'ex-mode:open') - submitNormalModeInputText(':set nolist') - expect(atom.config.get('editor.showInvisibles')).toBe(false) - - it "sets (no)nu(mber)", -> - openEx() - submitNormalModeInputText(':set nu') - expect(atom.config.get('editor.showLineNumbers')).toBe(true) - atom.commands.dispatch(editorElement, 'ex-mode:open') - submitNormalModeInputText(':set nonu') - expect(atom.config.get('editor.showLineNumbers')).toBe(false) - atom.commands.dispatch(editorElement, 'ex-mode:open') - submitNormalModeInputText(':set number') - expect(atom.config.get('editor.showLineNumbers')).toBe(true) - atom.commands.dispatch(editorElement, 'ex-mode:open') - submitNormalModeInputText(':set nonumber') - expect(atom.config.get('editor.showLineNumbers')).toBe(false) - - it "sets (no)sp(lit)r(ight)", -> - openEx() - submitNormalModeInputText(':set spr') - expect(atom.config.get('ex-mode.splitright')).toBe(true) - atom.commands.dispatch(editorElement, 'ex-mode:open') - submitNormalModeInputText(':set nospr') - expect(atom.config.get('ex-mode.splitright')).toBe(false) - atom.commands.dispatch(editorElement, 'ex-mode:open') - submitNormalModeInputText(':set splitright') - expect(atom.config.get('ex-mode.splitright')).toBe(true) - atom.commands.dispatch(editorElement, 'ex-mode:open') - submitNormalModeInputText(':set nosplitright') - expect(atom.config.get('ex-mode.splitright')).toBe(false) - - it "sets (no)s(plit)b(elow)", -> - openEx() - submitNormalModeInputText(':set sb') - expect(atom.config.get('ex-mode.splitbelow')).toBe(true) - atom.commands.dispatch(editorElement, 'ex-mode:open') - submitNormalModeInputText(':set nosb') - expect(atom.config.get('ex-mode.splitbelow')).toBe(false) - atom.commands.dispatch(editorElement, 'ex-mode:open') - submitNormalModeInputText(':set splitbelow') - expect(atom.config.get('ex-mode.splitbelow')).toBe(true) - atom.commands.dispatch(editorElement, 'ex-mode:open') - submitNormalModeInputText(':set nosplitbelow') - expect(atom.config.get('ex-mode.splitbelow')).toBe(false) - - it "sets (no)s(mart)c(a)s(e)", -> - openEx() - submitNormalModeInputText(':set scs') - expect(atom.config.get('vim-mode.useSmartcaseForSearch')).toBe(true) - openEx() - submitNormalModeInputText(':set noscs') - expect(atom.config.get('vim-mode.useSmartcaseForSearch')).toBe(false) - openEx() - submitNormalModeInputText(':set smartcase') - expect(atom.config.get('vim-mode.useSmartcaseForSearch')).toBe(true) - openEx() - submitNormalModeInputText(':set nosmartcase') - expect(atom.config.get('vim-mode.useSmartcaseForSearch')).toBe(false) - - it "sets (no)gdefault", -> - openEx() - submitNormalModeInputText(':set gdefault') - expect(atom.config.get('ex-mode.gdefault')).toBe(true) - atom.commands.dispatch(editorElement, 'ex-mode:open') - submitNormalModeInputText(':set nogdefault') - expect(atom.config.get('ex-mode.gdefault')).toBe(false) - - describe "aliases", -> - it "calls the aliased function without arguments", -> - ExClass.registerAlias('W', 'w') - spyOn(Ex, 'write') - openEx() - submitNormalModeInputText('W') - expect(Ex.write).toHaveBeenCalled() - - it "calls the aliased function with arguments", -> - ExClass.registerAlias('W', 'write') - spyOn(Ex, 'W').andCallThrough() - spyOn(Ex, 'write') - openEx() - submitNormalModeInputText('W') - WArgs = Ex.W.calls[0].args[0] - writeArgs = Ex.write.calls[0].args[0] - expect(WArgs).toBe writeArgs - - describe "with selections", -> - it "executes on the selected range", -> - spyOn(Ex, 's') - editor.setCursorBufferPosition([0, 0]) - editor.selectToBufferPosition([2, 1]) - atom.commands.dispatch(editorElement, 'ex-mode:open') - submitNormalModeInputText("'<,'>s/abc/def") - expect(Ex.s.calls[0].args[0].range).toEqual [0, 2] - - it "calls the functions multiple times if there are multiple selections", -> - spyOn(Ex, 's') - editor.setCursorBufferPosition([0, 0]) - editor.selectToBufferPosition([2, 1]) - editor.addCursorAtBufferPosition([3, 0]) - editor.selectToBufferPosition([3, 2]) - atom.commands.dispatch(editorElement, 'ex-mode:open') - submitNormalModeInputText("'<,'>s/abc/def") - calls = Ex.s.calls - expect(calls.length).toEqual 2 - expect(calls[0].args[0].range).toEqual [0, 2] - expect(calls[1].args[0].range).toEqual [3, 3] - - describe ':sort', -> - beforeEach -> - editor.setText('ghi\nabc\njkl\ndef\n142\nzzz\n91xfds9\n') - editor.setCursorBufferPosition([0, 0]) - - it "sorts entire file if range is not multi-line", -> - openEx() - submitNormalModeInputText('sort') - expect(editor.getText()).toEqual('142\n91xfds9\nabc\ndef\nghi\njkl\nzzz\n') - - it "sorts specific range if range is multi-line", -> - openEx() - submitNormalModeInputText('2,4sort') - expect(editor.getText()).toEqual('ghi\nabc\ndef\njkl\n142\nzzz\n91xfds9\n') diff --git a/spec/ex-input-spec.coffee b/spec/ex-input-spec.coffee deleted file mode 100644 index cd7fbd0..0000000 --- a/spec/ex-input-spec.coffee +++ /dev/null @@ -1,92 +0,0 @@ -helpers = require './spec-helper' -describe "the input element", -> - [editor, editorElement, vimState, exState] = [] - beforeEach -> - vimMode = atom.packages.loadPackage('vim-mode-plus') - exMode = atom.packages.loadPackage('ex-mode') - waitsForPromise -> - activationPromise = exMode.activate() - helpers.activateExMode() - activationPromise - - runs -> - spyOn(exMode.mainModule.globalExState, 'setVim').andCallThrough() - - waitsForPromise -> - vimMode.activate() - - waitsFor -> - exMode.mainModule.globalExState.setVim.calls.length > 0 - - runs -> - helpers.getEditorElement (element) -> - atom.commands.dispatch(element, "ex-mode:open") - editorElement = element - editor = editorElement.getModel() - atom.commands.dispatch(getCommandEditor(), "core:cancel") - vimState = vimMode.mainModule.getEditorState(editor) - exState = exMode.mainModule.exStates.get(editor) - vimState.resetNormalMode() - editor.setText("abc\ndef\nabc\ndef") - - afterEach -> - atom.commands.dispatch(getCommandEditor(), "core:cancel") - - getVisibility = () -> - editor.normalModeInputView.panel.visible - - getCommandEditor = () -> - editor.normalModeInputView.editorElement - - it "opens with 'ex-mode:open'", -> - atom.commands.dispatch(editorElement, "ex-mode:open") - expect(getVisibility()).toBe true - - it "closes with 'core:cancel'", -> - atom.commands.dispatch(editorElement, "ex-mode:open") - expect(getVisibility()).toBe true - atom.commands.dispatch(getCommandEditor(), "core:cancel") - expect(getVisibility()).toBe false - - it "closes when opening and then pressing backspace", -> - atom.commands.dispatch(editorElement, "ex-mode:open") - expect(getVisibility()).toBe true - atom.commands.dispatch(getCommandEditor(), "core:backspace") - expect(getVisibility()).toBe false - - it "doesn't close when there is text and pressing backspace", -> - atom.commands.dispatch(editorElement, "ex-mode:open") - expect(getVisibility()).toBe true - commandEditor = getCommandEditor() - model = commandEditor.getModel() - model.setText('abc') - atom.commands.dispatch(commandEditor, "core:backspace") - expect(getVisibility()).toBe true - expect(model.getText()).toBe 'ab' - - it "closes when there is text and pressing backspace multiple times", -> - atom.commands.dispatch(editorElement, "ex-mode:open") - expect(getVisibility()).toBe true - commandEditor = getCommandEditor() - model = commandEditor.getModel() - expect(model.getText()).toBe '' - model.setText('abc') - atom.commands.dispatch(commandEditor, "core:backspace") - expect(getVisibility()).toBe true - expect(model.getText()).toBe 'ab' - atom.commands.dispatch(commandEditor, "core:backspace") - expect(getVisibility()).toBe true - expect(model.getText()).toBe 'a' - atom.commands.dispatch(commandEditor, "core:backspace") - expect(getVisibility()).toBe true - expect(model.getText()).toBe '' - atom.commands.dispatch(commandEditor, "core:backspace") - expect(getVisibility()).toBe false - - it "contains '<,'> when opened while there are selections", -> - editor.setCursorBufferPosition([0, 0]) - editor.selectToBufferPosition([0, 1]) - editor.addCursorAtBufferPosition([2, 0]) - editor.selectToBufferPosition([2, 1]) - atom.commands.dispatch(editorElement, "ex-mode:open") - expect(getCommandEditor().getModel().getText()).toBe "'<,'>" diff --git a/spec/ex-mode-spec.coffee b/spec/ex-mode-spec.coffee new file mode 100644 index 0000000..bbc7fd7 --- /dev/null +++ b/spec/ex-mode-spec.coffee @@ -0,0 +1,62 @@ +ExMode = require '../lib/ex-mode' + +# Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs. +# +# To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit` +# or `fdescribe`). Remove the `f` to unfocus the block. + +describe "ExMode", -> + [workspaceElement, activationPromise] = [] + + beforeEach -> + workspaceElement = atom.views.getView(atom.workspace) + activationPromise = atom.packages.activatePackage('ex-mode') + + describe "when the ex-mode:toggle event is triggered", -> + it "hides and shows the modal panel", -> + # Before the activation event the view is not on the DOM, and no panel + # has been created + expect(workspaceElement.querySelector('.ex-mode')).not.toExist() + + # This is an activation event, triggering it will cause the package to be + # activated. + atom.commands.dispatch workspaceElement, 'ex-mode:toggle' + + waitsForPromise -> + activationPromise + + runs -> + expect(workspaceElement.querySelector('.ex-mode')).toExist() + + exModeElement = workspaceElement.querySelector('.ex-mode') + expect(exModeElement).toExist() + + exModePanel = atom.workspace.panelForItem(exModeElement) + expect(exModePanel.isVisible()).toBe true + atom.commands.dispatch workspaceElement, 'ex-mode:toggle' + expect(exModePanel.isVisible()).toBe false + + it "hides and shows the view", -> + # This test shows you an integration test testing at the view level. + + # Attaching the workspaceElement to the DOM is required to allow the + # `toBeVisible()` matchers to work. Anything testing visibility or focus + # requires that the workspaceElement is on the DOM. Tests that attach the + # workspaceElement to the DOM are generally slower than those off DOM. + jasmine.attachToDOM(workspaceElement) + + expect(workspaceElement.querySelector('.ex-mode')).not.toExist() + + # This is an activation event, triggering it causes the package to be + # activated. + atom.commands.dispatch workspaceElement, 'ex-mode:toggle' + + waitsForPromise -> + activationPromise + + runs -> + # Now we can test for view visibility + exModeElement = workspaceElement.querySelector('.ex-mode') + expect(exModeElement).toBeVisible() + atom.commands.dispatch workspaceElement, 'ex-mode:toggle' + expect(exModeElement).not.toBeVisible() diff --git a/spec/ex-mode-view-spec.coffee b/spec/ex-mode-view-spec.coffee new file mode 100644 index 0000000..d94ddf1 --- /dev/null +++ b/spec/ex-mode-view-spec.coffee @@ -0,0 +1,5 @@ +ExModeView = require '../lib/ex-mode-view' + +describe "ExModeView", -> + it "has one valid test", -> + expect("life").toBe "easy" diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee deleted file mode 100644 index 9ec4d58..0000000 --- a/spec/spec-helper.coffee +++ /dev/null @@ -1,65 +0,0 @@ -ExState = require '../lib/ex-state' -GlobalExState = require '../lib/global-ex-state' - -beforeEach -> - atom.workspace ||= {} - -activateExMode = -> - atom.workspace.open().then -> - atom.commands.dispatch(atom.views.getView(atom.workspace), 'ex-mode:open') - keydown('escape') - atom.workspace.getActivePane().destroyActiveItem() - - -getEditorElement = (callback) -> - textEditor = null - - waitsForPromise -> - atom.workspace.open().then (e) -> - textEditor = e - - runs -> - # element = document.createElement("atom-text-editor") - # element.setModel(textEditor) - # element.classList.add('vim-mode') - # element.exState = new ExState(element, new GlobalExState) - # - # element.addEventListener "keydown", (e) -> - # atom.keymaps.handleKeyboardEvent(e) - - element = atom.views.getView(textEditor) - - callback(element) - -dispatchKeyboardEvent = (target, eventArgs...) -> - e = document.createEvent('KeyboardEvent') - e.initKeyboardEvent(eventArgs...) - # 0 is the default, and it's valid ASCII, but it's wrong. - Object.defineProperty(e, 'keyCode', get: -> undefined) if e.keyCode is 0 - target.dispatchEvent e - -dispatchTextEvent = (target, eventArgs...) -> - e = document.createEvent('TextEvent') - e.initTextEvent(eventArgs...) - target.dispatchEvent e - -keydown = (key, {element, ctrl, shift, alt, meta, raw}={}) -> - key = "U+#{key.charCodeAt(0).toString(16)}" unless key is 'escape' or raw? - element ||= document.activeElement - eventArgs = [ - true, # bubbles - true, # cancelable - null, # view - key, # key - 0, # location - ctrl, alt, shift, meta - ] - - canceled = not dispatchKeyboardEvent(element, 'keydown', eventArgs...) - dispatchKeyboardEvent(element, 'keypress', eventArgs...) - if not canceled - if dispatchTextEvent(element, 'textInput', eventArgs...) - element.value += key - dispatchKeyboardEvent(element, 'keyup', eventArgs...) - -module.exports = {keydown, getEditorElement, activateExMode} diff --git a/styles/ex-mode.less b/styles/ex-mode.less index 2a714b0..da5d1df 100644 --- a/styles/ex-mode.less +++ b/styles/ex-mode.less @@ -6,25 +6,3 @@ .ex-mode { } - -div[is=ex-command-mode-input] atom-text-editor[mini]::before { - content: ":"; - opacity: 0.5; -} - -div[is=vim-normal-mode-input] atom-text-editor[mini]::before { - content: "/"; - opacity: 0.5; -} - -.command-mode-input atom-text-editor[mini] { - background-color: inherit; - border: none; - width: 100%; - font-weight: normal; - color: @text-color; - line-height: 1.28; - cursor: default; - white-space: nowrap; - padding-left: 10px; -}