feature(vim) add hot keys

This commit is contained in:
coderaiser 2017-09-07 18:22:47 +03:00
parent dc79ab4a25
commit 324022743b
13 changed files with 649 additions and 21 deletions

28
HELP.md
View file

@ -89,18 +89,20 @@ Cloud Commander supports command line parameters:
| `--console` | enable console
| `--terminal` | enable terminal
| `--terminal-path` | set terminal path
| `--vim` | enable vim hot keys
| `--no-server` | do not start server
| `--no-auth` | disable authorization
| `--no-online` | load scripts from local server
| `--no-open` | do not open web browser when server started
| `--no-name` | set empty tab name in web browser
| `--no-one-panel-mode` | unset one panel mode
| `--no-progress` | do not show progress of file operations
| `--no-html-dialogs` | do not use html dialogs
| `--no-one-panel-mode` | unset one panel mode
| `--no-contact` | disable contact
| `--no-config-dialog` | disable config dialog
| `--no-console` | disable console
| `--no-terminal` | disable terminal
| `--no-name` | set empty tab name in web browser
| `--no-vim` | disable vim hot keys
If no parameters given Cloud Commander reads information from `~/.cloudcmd.json` and use
@ -174,6 +176,27 @@ Hot keys
| `~` | console
| `Ctrl + Click` | open file on new tab
### Vim
When `--vim` option provided, or configuration parameter `vim` set, next hot keys become available:
|Key |Operation
|:----------------------|:--------------------------------------------
| `j` | navigate to next file
| `k` | navigate to previous file
| `dd` | remove current file
| `G` | navigate to bottom file
| `gg` | navigate to top file
| `v` | visual mode
| `y` | copy (selected in visual mode files)
| `p` | paste files
| `Esc` | unselect all
Commands can be joined, for example:
- `5j` will navigate `5` files below current;
- `d5j` will remove next `5` files;
- `dG` will remove all files from current to bottom;
View
---------------
![View](/img/screen/view.png "View")
@ -333,6 +356,7 @@ Here is description of options:
"console" : true, /* enable console */
"terminal" : false, /* disable terminal */
"terminalPath" : '', /* path of a terminal */
"vim" : false, /* disable vim hot keys */
}
```

View file

@ -533,6 +533,14 @@ function CmdProto() {
return Cmd;
};
this.unselectFile = (currentFile) => {
const current = currentFile || DOM.getCurrentFile();
current.classList.remove(SELECTED_FILE);
return Cmd;
};
this.toggleSelectedFile = (currentFile) => {
const current = currentFile || DOM.getCurrentFile();
const name = DOM.getCurrentName(current);

View file

@ -9,6 +9,7 @@ const exec = require('execon');
const Events = require('../dom/events');
const Buffer = require('../dom/buffer');
const KEY = require('./key');
const vim = require('./vim');
const setCurrentByChar = require('./set-current-by-char');
const fullstore = require('fullstore/legacy');
const Chars = fullstore();
@ -71,15 +72,19 @@ function KeyProto() {
char = isSymbol;
}
/* in case buttons can be processed */
if (!Key.isBind())
return;
if (!isNumpad && !alt && !ctrl && !meta && (isBetween || isSymbol))
const isVim = CloudCmd.config('vim');
if (!isVim && !isNumpad && !alt && !ctrl && !meta && (isBetween || isSymbol))
return setCurrentByChar(char, Chars);
Chars([]);
switchKey(event);
if (isVim)
vim(char, event);
}
function getSymbol(shift, keyCode) {
@ -106,6 +111,8 @@ function KeyProto() {
function switchKey(event) {
let i, isSelected, prev, next;
let current = Info.element;
let dataName;
const name = Info.name;
const {Operation} = CloudCmd;
@ -301,9 +308,9 @@ function KeyProto() {
event.preventDefault();
const attr = Info.panel.getAttribute('data-name');
dataName = Info.panel.getAttribute('data-name');
if (attr === 'js-right')
if (dataName === 'js-right')
DOM.duplicatePanel();
break;
@ -314,9 +321,9 @@ function KeyProto() {
event.preventDefault();
name = Info.panel.getAttribute('data-name');
dataName = Info.panel.getAttribute('data-name');
if (name === 'js-left')
if (dataName === 'js-left')
DOM.duplicatePanel();
break;

View file

@ -1,3 +1,5 @@
'use strict';
module.exports = {
BACKSPACE : 8,
TAB : 9,
@ -20,6 +22,10 @@ module.exports = {
ZERO : 48,
SEMICOLON : 52,
COLON : 54,
A : 65,
C : 67,
@ -27,6 +33,9 @@ module.exports = {
G : 71,
J : 74,
K : 75,
M : 77,
O : 79,

View file

@ -3,7 +3,6 @@
'use strict';
const Info = DOM.CurrentInfo;
const {escapeRegExp} = require('../../common/util');
module.exports = function setCurrentByChar(char, charStore) {
@ -56,5 +55,5 @@ module.exports = function setCurrentByChar(char, charStore) {
DOM.setCurrentFile(firstByName);
charStore([char]);
}
}
};

172
client/key/vim.js Normal file
View file

@ -0,0 +1,172 @@
'use strict';
/* global CloudCmd, DOM */
const Info = DOM.CurrentInfo;
const KEY = require('./key');
const fullstore = require('fullstore/legacy');
const store = fullstore('');
const visual = fullstore(false);
const stopVisual = () => {
visual(false);
};
const end = () => {
store('');
};
const rmFirst = (a) => {
return a
.split('')
.slice(1)
.join('');
};
module.exports = (key, event) => {
const current = Info.element;
const keyCode = event.keyCode;
const prevStore = store();
const value = store(prevStore.concat(key));
if (keyCode === KEY.ENTER)
return end();
if (keyCode === KEY.ESC) {
DOM.unselectFiles();
visual(false);
return end();
}
if (key === 'j') {
move('next', {
prevStore,
current,
});
return end();
}
if (key === 'k') {
move('previous', {
prevStore,
current,
});
return end();
}
if (/gg/.test(value)) {
move('previous', {
current,
prevStore,
max: Infinity,
});
return end();
}
if (key === 'd' && (visual() || prevStore === 'd')) {
CloudCmd.Operation.show('delete');
stopVisual();
return end();
}
if (key === 'G') {
move('next', {
current,
prevStore,
max: Infinity,
});
return end();
}
if (key === 'y') {
if (!visual())
return end();
DOM.Buffer.copy();
stopVisual();
DOM.unselectFiles();
return end();
}
if (/^p$/i.test(key)) {
DOM.Buffer.paste();
return end();
}
if (/^v$/i.test(key)) {
DOM.toggleSelectedFile(current);
visual(!visual());
return end();
}
};
module.exports.selectFile = selectFile;
function move(sibling, {max, current, prevStore}) {
const isDelete = prevStore[0] === 'd';
if (isDelete) {
visual(true);
prevStore = rmFirst(prevStore);
}
const n = max || getNumber(prevStore);
if (isNaN(n))
return;
setCurrent({
n,
current,
sibling,
visual: visual(),
});
if (isDelete)
CloudCmd.Operation.show('delete');
}
function getNumber(value) {
if (!value)
return 1;
if (value === 'g')
return 1;
return parseInt(value);
}
function selectFile(current) {
const name = DOM.getCurrentName(current);
if (name === '..')
return;
DOM.selectFile(current);
}
function setCurrent({n, current, visual, sibling}) {
const select = visual ? selectFile : DOM.unselectFile;
select(current);
const position = `${sibling}Sibling`;
for (let i = 0; i < n; i++) {
const next = current[position];
if (!next)
break;
current = next;
select(current);
}
DOM.setCurrentFile(current);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 229 KiB

After

Width:  |  Height:  |  Size: 228 KiB

Before After
Before After

View file

@ -27,6 +27,7 @@
"console": true,
"terminal": false,
"terminalPath": "",
"showConfig": "false"
"showConfig": "false",
"vim": "true"
}

View file

@ -15,24 +15,26 @@
"--port ": "set port number",
"--progress ": "show progress of file operations",
"--html-dialogs ": "use html dialogs",
"--open ": "open web browser when server started",
"--name ": "set tab name in web browser",
"--one-panel-mode ": "set one panel mode",
"--config-dialog ": "enable config dialog",
"--console ": "enable console",
"--contact ": "enable contact",
"--terminal ": "enable terminal",
"--terminal-path ": "set terminal path",
"--open ": "open web browser when server started",
"--name ": "set tab name in web browser",
"--vim ": "enable vim hot keys",
"--no-server ": "do not start server",
"--no-auth ": "disable authorization",
"--no-online ": "load scripts from local server",
"--no-open ": "do not open web browser when server started",
"--no-name ": "set default tab name in web browser",
"--no-one-panel-mode ": "unset one panel mode",
"--no-progress ": "do not show progress of file operations",
"--no-html-dialogs ": "do not use html dialogs",
"--no-one-panel-mode ": "unset one panel mode",
"--no-config-dialog ": "disable config dialog",
"--no-console ": "disable console",
"--no-contact ": "disable contact",
"--no-terminal ": "disable terminal",
"--no-name ": "set default tab name in web browser"
"--no-vim ": "disable vim hot keys"
}

View file

@ -38,26 +38,28 @@ programs in browser from any computer, mobile or tablet device.
--port set port number
--progress show progress of file operations
--html-dialogs use html dialogs
--open open web browser when server started
--name set tab name in web browser
--one-panel-mode set one panel mode
--contact enable contact
--config-dialog enable config dialog
--console enable console
--terminal enable terminal
--terminal-path set terminal path
--open open web browser when server started
--name set tab name in web browser
--vim enable vim hot keys
--no-auth disable authorization
--no-server do not start server
--no-online load scripts from local server
--no-open do not open web browser when server started
--no-name set default tab name in web browser
--no-one-panel-mode unset one panel mode
--no-progress do not show progress of file operations
--no-html-dialogs do not use html dialogs
--no-one-panel-mode unset one panel mode
--no-contact disable contact
--no-config-dialog disable config dialog
--no-console disable console
--no-terminal disable terminal
--no-name set default tab name in web browser
--no-vim disable vim hot keys
.SH RESOURCES AND DOCUMENTATION

View file

@ -60,6 +60,7 @@
"fix:js:eslint:client": "redrun eslint:client -- --fix",
"fix:js:eslint:server": "redrun eslint:server -- --fix",
"test": "tape 'test/**/*.js'",
"test:client": "tape 'test/client/**/*.js'",
"spell": "yaspeller .",
"wisdom": "redrun build",
"wisdom:type": "bin/release.js",
@ -91,7 +92,8 @@
"watch:server": "nodemon bin/cloudcmd.js",
"watch:lint": "nodemon -w client -w server -w webpack.config.js -x 'redrun lint:js'",
"watch:lint:client": "nodemon -w client -w webpack.config.js -x 'redrun lint:js:eslint:client'",
"watch:test": "nodemon -w server -w test -w common -x \"npm run test\"",
"watch:test": "nodemon -w server -w test -w common -x \"npm test\"",
"watch:test:client": "nodemon -w client -w test/client -x \"npm run test:client\"",
"watch:coverage": "nodemon -w server -w test -w common -x \"npm run coverage\"",
"w:c": "redrun watch:client",
"w:c:d": "redrun watch:client:dev",

394
test/client/key/vim.js Normal file
View file

@ -0,0 +1,394 @@
'use strict';
const test = require('tape');
const diff = require('sinon-called-with-diff');
const sinon = diff(require('sinon'));
const dir = '../../../client/key/';
const KEY = require(dir + 'key');
initGlobals();
const DOM = global.DOM;
const Buffer = DOM.Buffer;
const vim = require(dir + 'vim');
test('cloudcmd: client: key: set next file: no', (t) => {
const element = {
};
const setCurrentFile = sinon.stub();
global.DOM.CurrentInfo.element = element;
global.DOM.setCurrentFile = setCurrentFile;
vim('j', {});
t.ok(setCurrentFile.calledWith(element), 'should set next file');
t.end();
});
test('cloudcmd: client: key: set next file current', (t) => {
const nextSibling = 'hello';
const element = {
nextSibling
};
const setCurrentFile = sinon.stub();
global.DOM.CurrentInfo.element = element;
global.DOM.setCurrentFile = setCurrentFile;
vim('j', {});
t.ok(setCurrentFile.calledWith(nextSibling), 'should set next file');
t.end();
});
test('cloudcmd: client: key: set next file current', (t) => {
const nextSibling = 'hello';
const element = {
nextSibling
};
const setCurrentFile = sinon.stub();
global.DOM.CurrentInfo.element = element;
global.DOM.setCurrentFile = setCurrentFile;
vim('m', {});
vim('j', {});
vim('j', {});
t.ok(setCurrentFile.calledWith(nextSibling), 'should set next file');
t.end();
});
test('cloudcmd: client: key: set next file current: g', (t) => {
const nextSibling = 'hello';
const element = {
nextSibling
};
const setCurrentFile = sinon.stub();
global.DOM.CurrentInfo.element = element;
global.DOM.setCurrentFile = setCurrentFile;
vim('g', {});
vim('j', {});
t.ok(setCurrentFile.calledWith(nextSibling), 'should ignore g');
t.end();
});
test('cloudcmd: client: key: set +2 file current', (t) => {
const last = {};
const nextSibling = {
nextSibling: last
};
const element = {
nextSibling
};
const setCurrentFile = sinon.stub();
global.DOM.CurrentInfo.element = element;
global.DOM.setCurrentFile = setCurrentFile;
const event = {};
vim('2', event);
vim('j', event);
t.ok(setCurrentFile.calledWith(last), 'should set next file');
t.end();
});
test('cloudcmd: client: key: select +2 files from current before delete', (t) => {
const last = {};
const nextSibling = {
nextSibling: last
};
const element = {
nextSibling
};
const setCurrentFile = sinon.stub();
global.DOM.CurrentInfo.element = element;
global.DOM.setCurrentFile = setCurrentFile;
global.DOM.selectFile = sinon.stub();
global.DOM.getCurrentName = () => false;
global.CloudCmd.Operation.show = sinon.stub();
const event = {};
vim('d', event);
vim('2', event);
vim('j', event);
t.ok(setCurrentFile.calledWith(last), 'should set next file');
t.end();
});
test('cloudcmd: client: key: delete +2 files from current', (t) => {
const last = {};
const nextSibling = {
nextSibling: last
};
const element = {
nextSibling
};
const setCurrentFile = sinon.stub();
const show = sinon.stub();
global.DOM.CurrentInfo.element = element;
global.DOM.setCurrentFile = setCurrentFile;
global.DOM.selectFile = sinon.stub();
global.DOM.getCurrentName = () => false;
global.CloudCmd.Operation.show = show;
const event = {};
vim('d', event);
vim('2', event);
vim('j', event);
t.ok(show.calledWith('delete'), 'should call delete');
t.end();
});
test('cloudcmd: client: key: set previous file current', (t) => {
const previousSibling = 'hello';
const element = {
previousSibling
};
const setCurrentFile = sinon.stub();
global.DOM.CurrentInfo.element = element;
global.DOM.setCurrentFile = setCurrentFile;
vim('k', {});
t.ok(setCurrentFile.calledWith(previousSibling), 'should set previous file');
t.end();
});
test('cloudcmd: client: key: copy: no', (t) => {
const copy = sinon.stub();
Buffer.copy = copy;
vim('y', {});
t.notOk(copy.called, 'should not copy files');
t.end();
});
test('cloudcmd: client: key: copy', (t) => {
const copy = sinon.stub();
Buffer.copy = copy;
vim('v', {});
vim('y', {});
t.ok(copy.calledWith(), 'should copy files');
t.end();
});
test('cloudcmd: client: key: copy: unselectFiles', (t) => {
const unselectFiles = sinon.stub();
DOM.unselectFiles = unselectFiles;
vim('v', {});
vim('y', {});
t.ok(unselectFiles.calledWith(), 'should unselect files');
t.end();
});
test('cloudcmd: client: key: paste', (t) => {
const paste = sinon.stub();
Buffer.paste = paste;
vim('p', {});
t.ok(paste.calledWith(), 'should paste files');
t.end();
});
test('cloudcmd: client: key: selectFile: ..', (t) => {
const selectFile = sinon.stub();
const getCurrentName = sinon.stub();
DOM.selectFile = selectFile;
DOM.getCurrentName = () => '..';
const current = {};
vim.selectFile(current);
t.notOk(getCurrentName.called, 'should not call selectFile');
t.end();
});
test('cloudcmd: client: key: selectFile', (t) => {
const selectFile = sinon.stub();
DOM.selectFile = selectFile;
DOM.getCurrentName = (a) => a.name;
const current = {};
vim.selectFile(current);
t.ok(selectFile.calledWith(current), 'should call selectFile');
t.end();
});
test('cloudcmd: client: key: set last file current', (t) => {
const last = 'last';
const nextSibling = {
nextSibling: last
};
const element = {
nextSibling
};
const setCurrentFile = sinon.stub();
global.DOM.CurrentInfo.element = element;
global.DOM.setCurrentFile = setCurrentFile;
vim('G', {});
t.ok(setCurrentFile.calledWith(last), 'should set last file');
t.end();
});
test('cloudcmd: client: key: set first file current', (t) => {
const first = 'first';
const previousSibling= {
previousSibling: first
};
const element = {
previousSibling
};
const setCurrentFile = sinon.stub();
global.DOM.CurrentInfo.element = element;
global.DOM.setCurrentFile = setCurrentFile;
vim('g', {});
vim('g', {});
t.ok(setCurrentFile.calledWith(first), 'should set first file');
t.end();
});
test('cloudcmd: client: key: visual', (t) => {
const element = {
};
const toggleSelectedFile = sinon.stub();
global.DOM.CurrentInfo.element = element;
global.DOM.toggleSelectedFile = toggleSelectedFile;
vim('v', {});
t.ok(toggleSelectedFile.calledWith(element), 'should toggle selection');
t.end();
});
test('cloudcmd: client: key: ESC', (t) => {
const element = {
};
const unselectFiles = sinon.stub();
global.DOM.CurrentInfo.element = element;
global.DOM.unselectFiles = unselectFiles ;
vim('', {
keyCode: KEY.ESC
});
t.ok(unselectFiles.calledWith(), 'should toggle selection');
t.end();
});
test('cloudcmd: client: key: Enter', (t) => {
const nextSibling = 'hello';
const element = {
nextSibling
};
const setCurrentFile = sinon.stub();
global.DOM.CurrentInfo.element = element;
global.DOM.setCurrentFile = setCurrentFile;
vim('', {
keyCode: KEY.ENTER
});
vim('j', {});
t.ok(setCurrentFile.calledWith(nextSibling), 'should set next file');
t.end();
});
function initGlobals() {
const CurrentInfo = {
element: {},
};
const noop = () => {};
const Buffer = {
copy: noop,
};
global.DOM = {
Buffer,
CurrentInfo,
selectFile: noop,
unselectFile: noop,
unselectFiles: noop,
setCurrentFile: noop,
toggleSelectedFile: noop,
};
const show = () => {};
global.CloudCmd = {
Operation: {
show
}
};
}

View file

@ -48,6 +48,15 @@
<option {{ deepword-selected }}>deepword</option>
</select>
</li>
<li>
<label>
<input
data-name="js-vim"
type="checkbox"
{{ vim }}>
Vim
</label>
</li>
<li>
<label>
<input
@ -58,7 +67,6 @@
</label>
</li>
<li>
<label>
<input data-name="js-buffer" type="checkbox" {{ buffer }}>
Buffer