Compare commits
No commits in common. "master" and "v0.1.0" have entirely different histories.
28 changed files with 153 additions and 2628 deletions
|
|
@ -1,9 +0,0 @@
|
||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
end_of_line = lf
|
|
||||||
insert_final_newline = true
|
|
||||||
|
|
||||||
[*.{coffee,json}]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
23
.github/CONTRIBUTING.md
vendored
23
.github/CONTRIBUTING.md
vendored
|
|
@ -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
|
|
||||||
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -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!))
|
|
||||||
45
.travis.yml
45
.travis.yml
|
|
@ -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
|
|
||||||
90
CHANGELOG.md
90
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
|
## 0.1.0 - First Release
|
||||||
* Every feature added
|
* Every feature added
|
||||||
* Every bug fixed
|
* Every bug fixed
|
||||||
|
|
|
||||||
65
README.md
Executable file → Normal file
65
README.md
Executable file → Normal file
|
|
@ -2,67 +2,6 @@
|
||||||
|
|
||||||
ex-mode for Atom's vim-mode
|
ex-mode for Atom's vim-mode
|
||||||
|
|
||||||
## Use
|
## Usage
|
||||||
|
|
||||||
Install both [vim-mode-plus](https://github.com/t9md/atom-vim-mode-plus) (or
|
Install vim-mode. Type `:` in command mode. Enter `w`. or `write`.
|
||||||
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 <file>` | 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 <file>` | Save as |
|
|
||||||
| `wall/wa` | Save all tabs |
|
|
||||||
| `sp/split` | Split window |
|
|
||||||
| `sp/split <file>` | Open file in split window |
|
|
||||||
| `s/substitute` | Substitute regular expression in active line |
|
|
||||||
| `vsp/vsplit` | Vertical split window |
|
|
||||||
| `vsp/vsplit <file>` | Open file in vertical split window |
|
|
||||||
| `delete` | Cut active line |
|
|
||||||
| `yank` | Copy active line |
|
|
||||||
| `set <options>` | Set options |
|
|
||||||
| `sort` | Sort all lines in file |
|
|
||||||
| `sort <line range>` | 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
|
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,5 @@
|
||||||
|
|
||||||
# For more detailed documentation see
|
# For more detailed documentation see
|
||||||
# https://atom.io/docs/latest/advanced/keymaps
|
# 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)':
|
'atom-text-editor.vim-mode:not(.insert-mode)':
|
||||||
':': 'ex-mode:open'
|
':': 'ex-mode:open'
|
||||||
|
|
|
||||||
|
|
@ -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 []
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
class CommandError
|
|
||||||
constructor: (@message) ->
|
|
||||||
@name = 'Command Error'
|
|
||||||
|
|
||||||
module.exports = CommandError
|
|
||||||
|
|
@ -1,201 +1,21 @@
|
||||||
ExViewModel = require './ex-view-model'
|
ExViewModel = require './ex-view-model'
|
||||||
Ex = require './ex'
|
Ex = require './ex'
|
||||||
Find = require './find'
|
|
||||||
CommandError = require './command-error'
|
class CommandError
|
||||||
|
constructor: (@message) ->
|
||||||
|
@name = 'Command Error'
|
||||||
|
|
||||||
class Command
|
class Command
|
||||||
constructor: (@editor, @exState) ->
|
constructor: (@editor, @exState) ->
|
||||||
@selections = @exState.getSelections()
|
@viewModel = new ExViewModel(@)
|
||||||
@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
|
|
||||||
|
|
||||||
execute: (input) ->
|
execute: (input) ->
|
||||||
@vimState = @exState.globalExState.vim?.getEditorState(@editor)
|
return unless input.characters.length > 0
|
||||||
# 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<valid mark> is equal to :mark <valid mark> - only a-zA-Z is
|
|
||||||
# in vim-mode for now
|
|
||||||
if cl.length is 2 and cl[0] is 'k' and /[a-z]/i.test(cl[1])
|
|
||||||
command = 'mark'
|
|
||||||
args = cl[1]
|
|
||||||
else if not /[a-z]/i.test(cl[0])
|
|
||||||
command = cl[0]
|
|
||||||
args = cl[1..]
|
|
||||||
else
|
|
||||||
[m, command, args] = cl.match(/^(\w+)(.*)/)
|
|
||||||
|
|
||||||
# If the command matches an existing one exactly, execute that one
|
|
||||||
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]
|
|
||||||
|
|
||||||
|
func = (new Ex)[input.characters]
|
||||||
if func?
|
if func?
|
||||||
if runOverSelections
|
func()
|
||||||
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 })
|
|
||||||
else
|
else
|
||||||
throw new CommandError("Not an editor command: #{input.characters}")
|
throw new CommandError("#{input.characters}")
|
||||||
|
|
||||||
module.exports = Command
|
module.exports = {Command, CommandError}
|
||||||
|
|
|
||||||
58
lib/ex-command-mode-input-view.coffee
Normal file
58
lib/ex-command-mode-input-view.coffee
Normal file
|
|
@ -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()
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
GlobalExState = require './global-ex-state'
|
GlobalExState = require './global-ex-state'
|
||||||
ExState = require './ex-state'
|
ExState = require './ex-state'
|
||||||
Ex = require './ex'
|
|
||||||
{Disposable, CompositeDisposable} = require 'event-kit'
|
{Disposable, CompositeDisposable} = require 'event-kit'
|
||||||
|
|
||||||
module.exports = ExMode =
|
module.exports = ExMode =
|
||||||
|
|
@ -27,36 +26,3 @@ module.exports = ExMode =
|
||||||
|
|
||||||
deactivate: ->
|
deactivate: ->
|
||||||
@disposables.dispose()
|
@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'
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
)
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
|
{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
|
||||||
|
|
||||||
Command = require './command'
|
{Command, CommandError} = require './command'
|
||||||
CommandError = require './command-error'
|
|
||||||
|
|
||||||
class ExState
|
class ExState
|
||||||
constructor: (@editorElement, @globalExState) ->
|
constructor: (@editorElement, @globalExState) ->
|
||||||
|
|
@ -34,9 +33,6 @@ class ExState
|
||||||
onDidFailToExecute: (fn) ->
|
onDidFailToExecute: (fn) ->
|
||||||
@emitter.on('failed-to-execute', fn)
|
@emitter.on('failed-to-execute', fn)
|
||||||
|
|
||||||
onDidProcessOpStack: (fn) ->
|
|
||||||
@emitter.on('processed-op-stack', fn)
|
|
||||||
|
|
||||||
pushOperations: (operations) ->
|
pushOperations: (operations) ->
|
||||||
@opStack.push operations
|
@opStack.push operations
|
||||||
|
|
||||||
|
|
@ -48,25 +44,14 @@ class ExState
|
||||||
processOpStack: ->
|
processOpStack: ->
|
||||||
[command, input] = @opStack
|
[command, input] = @opStack
|
||||||
if input.characters.length > 0
|
if input.characters.length > 0
|
||||||
@history.unshift command
|
|
||||||
try
|
try
|
||||||
command.execute(input)
|
command.execute(input)
|
||||||
|
@history.unshift command
|
||||||
catch e
|
catch e
|
||||||
if (e instanceof CommandError)
|
if (e instanceof CommandError)
|
||||||
atom.notifications.addError("Command error: #{e.message}")
|
|
||||||
@emitter.emit('failed-to-execute')
|
@emitter.emit('failed-to-execute')
|
||||||
else
|
else
|
||||||
throw e
|
throw e
|
||||||
@clearOpStack()
|
@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
|
module.exports = ExState
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,20 @@
|
||||||
{ViewModel, Input} = require './view-model'
|
{ViewModel, Input} = require './view-model'
|
||||||
AutoComplete = require './autocomplete'
|
|
||||||
Ex = require './ex'
|
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
class ExViewModel extends ViewModel
|
class ExViewModel extends ViewModel
|
||||||
constructor: (@exCommand, withSelection) ->
|
constructor: (@exCommand) ->
|
||||||
super(@exCommand, class: 'command')
|
super(@exCommand, class: 'command')
|
||||||
@historyIndex = -1
|
@historyIndex = -1
|
||||||
|
|
||||||
if withSelection
|
@view.editor.on('core:move-up', @increaseHistoryEx)
|
||||||
@view.editorElement.getModel().setText("'<,'>")
|
@view.editor.on('core:move-down', @decreaseHistoryEx)
|
||||||
|
|
||||||
@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())
|
|
||||||
|
|
||||||
restoreHistory: (index) ->
|
restoreHistory: (index) ->
|
||||||
@view.editorElement.getModel().setText(@history(index).value)
|
@view.editor.setText(@history(index).value)
|
||||||
|
|
||||||
history: (index) ->
|
history: (index) ->
|
||||||
@exState.getExHistoryItem(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: =>
|
increaseHistoryEx: =>
|
||||||
if @history(@historyIndex + 1)?
|
if @history(@historyIndex + 1)?
|
||||||
@historyIndex += 1
|
@historyIndex += 1
|
||||||
|
|
@ -45,7 +24,7 @@ class ExViewModel extends ViewModel
|
||||||
if @historyIndex <= 0
|
if @historyIndex <= 0
|
||||||
# get us back to a clean slate
|
# get us back to a clean slate
|
||||||
@historyIndex = -1
|
@historyIndex = -1
|
||||||
@view.editorElement.getModel().setText('')
|
@view.editor.setText('')
|
||||||
else
|
else
|
||||||
@historyIndex -= 1
|
@historyIndex -= 1
|
||||||
@restoreHistory(@historyIndex)
|
@restoreHistory(@historyIndex)
|
||||||
|
|
|
||||||
471
lib/ex.coffee
471
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
|
class Ex
|
||||||
@singleton: =>
|
write: -> atom.workspace.getActiveEditor().save()
|
||||||
@ex ||= new Ex
|
w: => @write()
|
||||||
|
|
||||||
@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)
|
|
||||||
|
|
||||||
module.exports = Ex
|
module.exports = Ex
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
class GlobalExState
|
class GlobalExState
|
||||||
commandHistory: []
|
commandHistory: []
|
||||||
setVim: (@vim) ->
|
|
||||||
|
|
||||||
module.exports = GlobalExState
|
module.exports = GlobalExState
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,18 @@
|
||||||
ExNormalModeInputElement = require './ex-normal-mode-input-element'
|
ExCommandModeInputView = require './ex-command-mode-input-view'
|
||||||
|
|
||||||
class ViewModel
|
class ViewModel
|
||||||
constructor: (@command, opts={}) ->
|
constructor: (@command, opts={}) ->
|
||||||
{@editor, @exState} = @command
|
{@editor, @exState} = @command
|
||||||
|
|
||||||
@view = new ExNormalModeInputElement().initialize(@, opts)
|
@view = new ExCommandModeInputView(@, opts)
|
||||||
@editor.normalModeInputView = @view
|
@editor.commandModeInputView = @view
|
||||||
@exState.onDidFailToExecute => @view.remove()
|
@exState.onDidFailToExecute => @view.remove()
|
||||||
@done = false
|
|
||||||
|
|
||||||
confirm: (view) ->
|
confirm: (view) ->
|
||||||
@exState.pushOperations(new Input(@view.value))
|
@exState.pushOperations(new Input(@view.value))
|
||||||
@done = true
|
|
||||||
|
|
||||||
cancel: (view) ->
|
cancel: (view) ->
|
||||||
unless @done
|
@exState.pushOperations(new Input(''))
|
||||||
@exState.pushOperations(new Input(''))
|
|
||||||
@done = true
|
|
||||||
|
|
||||||
class Input
|
class Input
|
||||||
constructor: (@characters) ->
|
constructor: (@characters) ->
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
32
package.json
32
package.json
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "ex-mode",
|
"name": "ex-mode",
|
||||||
"main": "./lib/ex-mode",
|
"main": "./lib/ex-mode",
|
||||||
"version": "0.18.0",
|
"version": "0.1.0",
|
||||||
"description": "Ex for Atom's vim-mode",
|
"description": "Ex for Atom's vim-mode",
|
||||||
"activationCommands": {
|
"activationCommands": {
|
||||||
"atom-workspace": "ex-mode:open"
|
"atom-workspace": "ex-mode:open"
|
||||||
|
|
@ -9,36 +9,10 @@
|
||||||
"repository": "https://github.com/lloeki/ex-mode",
|
"repository": "https://github.com/lloeki/ex-mode",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"atom": ">=0.200.0 <2.0.0"
|
"atom": ">=0.174.0 <2.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"underscore-plus": "1.x",
|
"underscore-plus": "1.x",
|
||||||
"event-kit": "^0.7.2",
|
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -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 "'<,'>"
|
|
||||||
62
spec/ex-mode-spec.coffee
Normal file
62
spec/ex-mode-spec.coffee
Normal file
|
|
@ -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()
|
||||||
5
spec/ex-mode-view-spec.coffee
Normal file
5
spec/ex-mode-view-spec.coffee
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
ExModeView = require '../lib/ex-mode-view'
|
||||||
|
|
||||||
|
describe "ExModeView", ->
|
||||||
|
it "has one valid test", ->
|
||||||
|
expect("life").toBe "easy"
|
||||||
|
|
@ -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}
|
|
||||||
|
|
@ -6,25 +6,3 @@
|
||||||
|
|
||||||
.ex-mode {
|
.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;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue