Merge branch 'message-passing-refactoring'

jh-changes
Shin'ya Ueoka 7 years ago
commit b2cddcd69b
  1. 11
      src/actions/background.js
  2. 35
      src/actions/command.js
  3. 28
      src/actions/console.js
  4. 40
      src/actions/index.js
  5. 15
      src/actions/input.js
  6. 122
      src/background/index.js
  7. 82
      src/background/key-queue.js
  8. 71
      src/background/keys.js
  9. 4
      src/background/tabs.js
  10. 61
      src/console/console-frame.js
  11. 146
      src/console/console.js
  12. 27
      src/console/frames.js
  13. 130
      src/content/index.js
  14. 53
      src/reducers/background.js
  15. 24
      src/reducers/command.js
  16. 39
      src/reducers/console.js
  17. 48
      src/reducers/content.js
  18. 23
      src/reducers/input.js
  19. 54
      src/shared/actions.js
  20. 19
      src/shared/messages.js
  21. 14
      test/actions/background.test.js
  22. 51
      test/actions/command.test.js
  23. 37
      test/actions/console.test.js
  24. 21
      test/actions/input.test.js
  25. 50
      test/background/key-queue.test.js
  26. 70
      test/background/keys.test.js
  27. 43
      test/reducers/console.test.js
  28. 34
      test/reducers/input.test.js
  29. 25
      test/shared/messages.test.js

@ -0,0 +1,11 @@
import actions from '../actions';
export function requestCompletions(line) {
let command = line.split(' ', 1)[0];
let keywords = line.replace(command + ' ', '');
return {
type: actions.BACKGROUND_REQUEST_COMPLETIONS,
command,
keywords
};
}

@ -0,0 +1,35 @@
import actions from '../actions';
const normalizeUrl = (string) => {
try {
return new URL(string).href
} catch (e) {
return 'http://' + string;
}
}
export function exec(line) {
let name = line.split(' ')[0];
let remaining = line.replace(name + ' ', '');
switch (name) {
case 'open':
// TODO use search engined and pass keywords to them
return {
type: actions.COMMAND_OPEN_URL,
url: normalizeUrl(remaining)
};
case 'tabopen':
return {
type: actions.COMMAND_TABOPEN_URL,
url: normalizeUrl(remaining)
};
case 'b':
case 'buffer':
return {
type: actions.COMMAND_BUFFER,
keywords: remaining
};
}
throw new Error(name + ' command is not defined');
}

@ -0,0 +1,28 @@
import actions from '../actions';
export function showCommand(text) {
return {
type: actions.CONSOLE_SHOW_COMMAND,
text: text
};
}
export function setCompletions(completions) {
return {
type: actions.CONSOLE_SET_COMPLETIONS,
completions: completions
};
}
export function showError(text) {
return {
type: actions.CONSOLE_SHOW_ERROR,
text: text
};
}
export function hide() {
return {
type: actions.CONSOLE_HIDE
};
}

@ -0,0 +1,40 @@
export default {
// console commands
CONSOLE_SHOW_COMMAND: 'vimvixen.console.show.command',
CONSOLE_SET_COMPLETIONS: 'vimvixen.console.set.completions',
CONSOLE_SHOW_ERROR: 'vimvixen.console.show.error',
CONSOLE_HIDE: 'vimvixen.console.hide',
// Background commands
BACKGROUND_REQUEST_COMPLETIONS: 'vimvixen.background.request.completions',
TABS_CLOSE: 'tabs.close',
TABS_REOPEN: 'tabs.reopen',
TABS_PREV: 'tabs.prev',
TABS_NEXT: 'tabs.next',
TABS_RELOAD: 'tabs.reload',
ZOOM_IN: 'zoom.in',
ZOOM_OUT: 'zoom.out',
ZOOM_NEUTRAL: 'zoom.neutral',
// content commands
CMD_OPEN: 'cmd.open',
CMD_TABS_OPEN: 'cmd.tabs.open',
CMD_BUFFER: 'cmd.buffer',
SCROLL_LINES: 'scroll.lines',
SCROLL_PAGES: 'scroll.pages',
SCROLL_TOP: 'scroll.top',
SCROLL_BOTTOM: 'scroll.bottom',
SCROLL_LEFT: 'scroll.left',
SCROLL_RIGHT: 'scroll.right',
FOLLOW_START: 'follow.start',
HISTORY_PREV: 'history.prev',
HISTORY_NEXT: 'history.next',
// User input
INPUT_KEY_PRESS: 'input.key,press',
INPUT_CLEAR_KEYS: 'input.clear.keys',
COMMAND_OPEN_URL: 'command.open.url',
COMMAND_TABOPEN_URL: 'command.tabopen.url',
COMMAND_BUFFER: 'command.buffer',
};

@ -0,0 +1,15 @@
import actions from '../actions';
export function keyPress(code, ctrl) {
return {
type: actions.INPUT_KEY_PRESS,
code,
ctrl
};
}
export function clearKeys() {
return {
type: actions.INPUT_CLEAR_KEYS
}
}

@ -1,103 +1,45 @@
import * as actions from '../shared/actions'; import * as keys from './keys';
import * as tabs from './tabs'; import * as inputActions from '../actions/input';
import * as zooms from './zooms'; import backgroundReducers from '../reducers/background';
import KeyQueue from './key-queue'; import commandReducer from '../reducers/command';
import inputReducers from '../reducers/input';
const queue = new KeyQueue(); let inputState = inputReducers(undefined, {});
const keyPressHandle = (request, sender) => { const keyQueueChanged = (sender, prevState, state) => {
let action = queue.push({ if (state.keys.length === 0) {
code: request.code,
ctrl: request.ctrl
});
if (!action) {
return Promise.resolve(); return Promise.resolve();
} }
if (actions.isBackgroundAction(action[0])) { let prefix = keys.asKeymapChars(state.keys);
return doBackgroundAction(sender, action); let matched = Object.keys(keys.defaultKeymap).filter((keys) => {
} else if (actions.isContentAction(action[0])) { return keys.startsWith(prefix);
return Promise.resolve({
type: 'response.action',
action: action
}); });
} if (matched.length == 0) {
return Promise.resolve(); return handleMessage(inputActions.clearKeys(), sender);
}; } else if (matched.length > 1 || matched.length === 1 && prefix !== matched[0]) {
const doBackgroundAction = (sender, action) => {
switch(action[0]) {
case actions.TABS_CLOSE:
return tabs.closeTab(sender.tab.id);
case actions.TABS_REOPEN:
return tabs.reopenTab();
case actions.TABS_PREV:
return tabs.selectPrevTab(sender.tab.index, actions[1] || 1);
case actions.TABS_NEXT:
return tabs.selectNextTab(sender.tab.index, actions[1] || 1);
case actions.TABS_RELOAD:
return tabs.reload(sender.tab, actions[1] || false);
case actions.ZOOM_IN:
return zooms.zoomIn();
case actions.ZOOM_OUT:
return zooms.zoomOut();
case actions.ZOOM_NEUTRAL:
return zooms.neutral();
}
return Promise.resolve(); return Promise.resolve();
}
const normalizeUrl = (string) => {
try {
return new URL(string).href
} catch (e) {
return 'http://' + string;
}
}
const cmdBuffer = (sender, arg) => {
if (isNaN(arg)) {
return tabs.selectByKeyword(sender.tab, arg);
} else {
let index = parseInt(arg, 10) - 1;
return tabs.selectAt(index);
}
}
const cmdEnterHandle = (request, sender) => {
let words = request.text.split(' ').filter((s) => s.length > 0);
switch (words[0]) {
case 'open':
return browser.tabs.update(sender.tab.id, { url: normalizeUrl(words[1]) });
case 'tabopen':
return browser.tabs.create({ url: normalizeUrl(words[1]) });
case 'b':
case 'buffer':
return cmdBuffer(sender, words[1]);
} }
throw new Error(words[0] + ' command is not defined'); let action = keys.defaultKeymap[matched];
return handleMessage(inputActions.clearKeys(), sender).then(() => {
return backgroundReducers(undefined, action, sender).then(() => {
return browser.tabs.sendMessage(sender.tab.id, action);
});
});
}; };
browser.runtime.onMessage.addListener((request, sender) => { const handleMessage = (action, sender) => {
switch (request.type) { let nextInputState = inputReducers(inputState, action);
case 'event.keypress': if (JSON.stringify(nextInputState) !== JSON.stringify(inputState)) {
return keyPressHandle(request, sender); let prevState = inputState;
case 'event.cmd.enter': inputState = nextInputState;
return cmdEnterHandle(request, sender); return keyQueueChanged(sender, prevState, inputState);
case 'event.cmd.tabs.completion':
return tabs.getCompletions(request.text).then((tabs) => {
let items = tabs.map((tab) => {
return {
caption: tab.title,
content: tab.title,
url: tab.url,
icon: tab.favIconUrl
} }
return backgroundReducers(undefined, action, sender).then(() => {
return commandReducer(undefined, action, sender).then(() => {
return browser.tabs.sendMessage(sender.tab.id, action);
}); });
return {
name: "Buffers",
items: items
};
}); });
} };
});
browser.runtime.onMessage.addListener(handleMessage);

@ -1,82 +0,0 @@
import * as actions from '../shared/actions';
const DEFAULT_KEYMAP = {
':': [ actions.CMD_OPEN ],
'o': [ actions.CMD_TABS_OPEN, false ],
'O': [ actions.CMD_TABS_OPEN, true ],
'b': [ actions.CMD_BUFFER ],
'k': [ actions.SCROLL_LINES, -1 ],
'j': [ actions.SCROLL_LINES, 1 ],
'<C-E>': [ actions.SCROLL_LINES, -1 ],
'<C-Y>': [ actions.SCROLL_LINES, 1 ],
'<C-U>': [ actions.SCROLL_PAGES, -0.5 ],
'<C-D>': [ actions.SCROLL_PAGES, 0.5 ],
'<C-B>': [ actions.SCROLL_PAGES, -1 ],
'<C-F>': [ actions.SCROLL_PAGES, 1 ],
'gg': [ actions.SCROLL_TOP ],
'G': [ actions.SCROLL_BOTTOM ],
'0': [ actions.SCROLL_LEFT ],
'$': [ actions.SCROLL_RIGHT ],
'd': [ actions.TABS_CLOSE ],
'u': [ actions.TABS_REOPEN],
'h': [ actions.TABS_PREV, 1 ],
'l': [ actions.TABS_NEXT, 1 ],
'r': [ actions.TABS_RELOAD, false ],
'R': [ actions.TABS_RELOAD, true ],
'zi': [ actions.ZOOM_IN ],
'zo': [ actions.ZOOM_OUT ],
'zz': [ actions.ZOOM_NEUTRAL],
'f': [ actions.FOLLOW_START, false ],
'F': [ actions.FOLLOW_START, true ],
'H': [ actions.HISTORY_PREV ],
'L': [ actions.HISTORY_NEXT ],
}
export default class KeyQueue {
constructor(keymap = DEFAULT_KEYMAP) {
this.data = [];
this.keymap = keymap;
}
push(key) {
this.data.push(key);
let current = this.asKeymapChars();
let filtered = Object.keys(this.keymap).filter((keys) => {
return keys.startsWith(current);
});
if (filtered.length == 0) {
this.data = [];
return null;
} else if (filtered.length === 1 && current === filtered[0]) {
let action = this.keymap[filtered[0]];
this.data = [];
return action;
}
return null;
}
asKeymapChars() {
return this.data.map((k) => {
let c = String.fromCharCode(k.code);
if (k.ctrl) {
return '<C-' + c.toUpperCase() + '>';
} else {
return c
}
}).join('');
}
asCaretChars() {
return this.data.map((k) => {
let c = String.fromCharCode(k.code);
if (k.ctrl) {
return '^' + c.toUpperCase();
} else {
return c;
}
}).join('');
}
}

@ -1,28 +1,57 @@
const identifyKey = (key1, key2) => { import actions from '../actions';
return (key1.code === key2.code) &&
((key1.shift || false) === (key2.shift || false)) &&
((key1.ctrl || false) === (key2.ctrl || false)) &&
((key1.alt || false) === (key2.alt || false)) &&
((key1.meta || false) === (key2.meta || false));
};
const hasPrefix = (keys, prefix) => { const defaultKeymap = {
if (keys.length < prefix.length) { ':': { type: actions.CMD_OPEN },
return false; 'o': { type: actions.CMD_TABS_OPEN, alter: false },
} 'O': { type: actions.CMD_TABS_OPEN, alter: true },
for (let i = 0; i < prefix.length; ++i) { 'b': { type: actions.CMD_BUFFER },
if (!identifyKey(keys[i], prefix[i])) { 'k': { type: actions.SCROLL_LINES, count: -1 },
return false; 'j': { type: actions.SCROLL_LINES, count: 1 },
} '<C-E>': { type: actions.SCROLL_LINES, count: -1 },
'<C-Y>': { type: actions.SCROLL_LINES, count: 1 },
'<C-U>': { type: actions.SCROLL_PAGES, count: -0.5 },
'<C-D>': { type: actions.SCROLL_PAGES, count: 0.5 },
'<C-B>': { type: actions.SCROLL_PAGES, count: -1 },
'<C-F>': { type: actions.SCROLL_PAGES, count: 1 },
'gg': { type: actions.SCROLL_TOP },
'G': { type: actions.SCROLL_BOTTOM },
'0': { type: actions.SCROLL_LEFT },
'$': { type: actions.SCROLL_RIGHT },
'd': { type: actions.TABS_CLOSE },
'u': { type: actions.TABS_REOPEN },
'h': { type: actions.TABS_PREV, count: 1 },
'l': { type: actions.TABS_NEXT, count: 1 },
'r': { type: actions.TABS_RELOAD, cache: false },
'R': { type: actions.TABS_RELOAD, cache: true },
'zi': { type: actions.ZOOM_IN },
'zo': { type: actions.ZOOM_OUT },
'zz': { type: actions.ZOOM_NEUTRAL },
'f': { type: actions.FOLLOW_START, newTab: false },
'F': { type: actions.FOLLOW_START, newTab: true },
'H': { type: actions.HISTORY_PREV },
'L': { type: actions.HISTORY_NEXT },
}
const asKeymapChars = (keys) => {
return keys.map((k) => {
let c = String.fromCharCode(k.code);
if (k.ctrl) {
return '<C-' + c.toUpperCase() + '>';
} else {
return c
} }
return true; }).join('');
} }
const identifyKeys = (keys1, keys2) => { const asCaretChars = (keys) => {
if (keys1.length !== keys2.length) { return keys.map((k) => {
return false; let c = String.fromCharCode(k.code);
if (k.ctrl) {
return '^' + c.toUpperCase();
} else {
return c;
} }
return hasPrefix(keys1, keys2); }).join('');
} }
export { identifyKey, identifyKeys, hasPrefix }; export { defaultKeymap, asKeymapChars, asCaretChars };

@ -59,7 +59,7 @@ const getCompletions = (keyword) => {
}; };
const selectPrevTab = (current, count) => { const selectPrevTab = (current, count) => {
return browser.tabs.query({ currentWindow: true }, (tabs) => { return browser.tabs.query({ currentWindow: true }).then((tabs) => {
if (tabs.length < 2) { if (tabs.length < 2) {
return; return;
} }
@ -70,7 +70,7 @@ const selectPrevTab = (current, count) => {
}; };
const selectNextTab = (current, count) => { const selectNextTab = (current, count) => {
return browser.tabs.query({ currentWindow: true }, (tabs) => { return browser.tabs.query({ currentWindow: true }).then((tabs) => {
if (tabs.length < 2) { if (tabs.length < 2) {
return; return;
} }

@ -1,61 +0,0 @@
import './console-frame.scss';
import * as messages from '../shared/messages';
export default class ConsoleFrame {
constructor(win) {
let element = window.document.createElement('iframe');
element.src = browser.runtime.getURL('build/console.html');
element.className = 'vimvixen-console-frame';
win.document.body.append(element);
this.element = element;
this.errorShown = true;
this.hide();
}
showCommand(text) {
this.showFrame();
let message = {
type: 'vimvixen.console.show.command',
text: text
};
messages.send(this.element.contentWindow, message);
this.errorShown = false;
}
showError(text) {
this.showFrame();
let message = {
type: 'vimvixen.console.show.error',
text: text
};
messages.send(this.element.contentWindow, message);
this.errorShown = true;
this.element.blur();
}
showFrame() {
this.element.style.display = 'block';
}
hide() {
this.element.style.display = 'none';
this.element.blur();
this.errorShown = false;
}
isErrorShown() {
return this.element.style.display === 'block' && this.errorShown;
}
setCompletions(completions) {
messages.send(this.element.contentWindow, {
type: 'vimvixen.console.set.completions',
completions: completions
});
}
}

@ -1,36 +1,18 @@
import './console.scss'; import './console.scss';
import * as backgroundActions from '../actions/background';
import * as consoleActions from '../actions/console';
import * as commandActions from '../actions/command';
import Completion from './completion'; import Completion from './completion';
import * as messages from '../shared/messages'; import consoleReducer from '../reducers/console';
const parent = window.parent;
// TODO consider object-oriented // TODO consider object-oriented
var prevValue = ""; var prevValue = "";
var completion = null; var completion = null;
var completionOrigin = ""; var completionOrigin = "";
let state = consoleReducer(undefined, {});
const blurMessage = () => {
return {
type: 'vimvixen.command.blur'
};
};
const keydownMessage = (input) => {
return {
type: 'vimvixen.command.enter',
value: input.value
};
};
const keyupMessage = (input) => {
return {
type: 'vimvixen.command.change',
value: input.value
};
};
const handleBlur = () => { const handleBlur = () => {
messages.send(parent, blurMessage()); return browser.runtime.sendMessage(consoleActions.hide());
}; };
const completeNext = () => { const completeNext = () => {
@ -64,13 +46,13 @@ const completePrev = () => {
} }
const handleKeydown = (e) => { const handleKeydown = (e) => {
let input = window.document.querySelector('#vimvixen-console-command-input');
switch(e.keyCode) { switch(e.keyCode) {
case KeyboardEvent.DOM_VK_ESCAPE: case KeyboardEvent.DOM_VK_ESCAPE:
messages.send(parent, blurMessage()); return input.blur();
break;
case KeyboardEvent.DOM_VK_RETURN: case KeyboardEvent.DOM_VK_RETURN:
messages.send(parent, keydownMessage(e.target)); return browser.runtime.sendMessage(commandActions.exec(e.target.value));
break;
case KeyboardEvent.DOM_VK_TAB: case KeyboardEvent.DOM_VK_TAB:
if (e.shiftKey) { if (e.shiftKey) {
completePrev(); completePrev();
@ -90,8 +72,10 @@ const handleKeyup = (e) => {
if (e.target.value === prevValue) { if (e.target.value === prevValue) {
return; return;
} }
messages.send(parent, keyupMessage(e.target));
prevValue = e.target.value; prevValue = e.target.value;
return browser.runtime.sendMessage(
backgroundActions.requestCompletions(e.target.value)
);
}; };
window.addEventListener('load', () => { window.addEventListener('load', () => {
@ -101,35 +85,6 @@ window.addEventListener('load', () => {
input.addEventListener('keyup', handleKeyup); input.addEventListener('keyup', handleKeyup);
}); });
const showCommand = (text) => {
let command = window.document.querySelector('#vimvixen-console-command');
command.style.display = 'block';
let error = window.document.querySelector('#vimvixen-console-error');
error.style.display = 'none';
let input = window.document.querySelector('#vimvixen-console-command-input');
input.value = text;
input.focus();
completion = null;
let container = window.document.querySelector('#vimvixen-console-completion');
container.innerHTML = '';
messages.send(parent, keyupMessage(input));
}
const showError = (text) => {
let error = window.document.querySelector('#vimvixen-console-error');
error.textContent = text;
error.style.display = 'block';
let command = window.document.querySelector('#vimvixen-console-command');
command.style.display = 'none';
let completion = window.document.querySelector('#vimvixen-console-completion');
completion.style.display = 'none';
}
const createCompletionTitle = (text) => { const createCompletionTitle = (text) => {
let li = document.createElement('li'); let li = document.createElement('li');
li.className = 'vimvixen-console-completion-title'; li.className = 'vimvixen-console-completion-title';
@ -154,55 +109,72 @@ const createCompletionItem = (icon, caption, url) => {
return li; return li;
} }
const setCompletions = (completions) => { const selectCompletion = (target) => {
let container = window.document.querySelector('#vimvixen-console-completion'); let container = window.document.querySelector('#vimvixen-console-completion');
container.style.display = 'block'; Array.prototype.forEach.call(container.children, (ele) => {
container.innerHTML = ''; if (!ele.classList.contains('vimvixen-console-completion-item')) {
return;
}
if (ele === target) {
ele.classList.add('vimvixen-completion-selected');
} else {
ele.classList.remove('vimvixen-completion-selected');
}
});
};
const updateCompletions = (completions) => {
let completionsContainer = window.document.querySelector('#vimvixen-console-completion');
let input = window.document.querySelector('#vimvixen-console-command-input');
completionsContainer.innerHTML = '';
let pairs = []; let pairs = [];
for (let group of completions) { for (let group of completions) {
let title = createCompletionTitle(group.name); let title = createCompletionTitle(group.name);
container.append(title); completionsContainer.append(title);
for (let item of group.items) { for (let item of group.items) {
let li = createCompletionItem(item.icon, item.caption, item.url); let li = createCompletionItem(item.icon, item.caption, item.url);
container.append(li); completionsContainer.append(li);
pairs.push([item, li]); pairs.push([item, li]);
} }
} }
completion = new Completion(pairs); completion = new Completion(pairs);
let input = window.document.querySelector('#vimvixen-console-command-input');
completionOrigin = input.value.split(' ')[0]; completionOrigin = input.value.split(' ')[0];
} }
const selectCompletion = (target) => { const update = (prevState, state) => {
let container = window.document.querySelector('#vimvixen-console-completion'); let error = window.document.querySelector('#vimvixen-console-error');
Array.prototype.forEach.call(container.children, (ele) => { let command = window.document.querySelector('#vimvixen-console-command');
if (!ele.classList.contains('vimvixen-console-completion-item')) { let input = window.document.querySelector('#vimvixen-console-command-input');
return;
error.style.display = state.errorShown ? 'block' : 'none';
error.textContent = state.errorText;
command.style.display = state.commandShown ? 'block' : 'none';
if (!prevState.commandShown && state.commandShown) {
// setup input on firstly shown
input.value = state.commandText;
input.focus();
} }
if (ele === target) {
ele.classList.add('vimvixen-completion-selected'); if (JSON.stringify(state.completions) !== JSON.stringify(prevState.completions)) {
} else { updateCompletions(state.completions);
ele.classList.remove('vimvixen-completion-selected');
} }
}); }
};
messages.receive(window, (message) => { browser.runtime.onMessage.addListener((action) => {
switch (message.type) { let nextState = consoleReducer(state, action);
case 'vimvixen.console.show.command': if (JSON.stringify(nextState) !== JSON.stringify(state)) {
showCommand(message.text); update(state, nextState);
break; state = nextState;
case 'vimvixen.console.show.error':
showError(message.text);
break;
case 'vimvixen.console.set.completions':
setCompletions(message.completions);
break;
} }
}); });
window.addEventListener('load', () => {
update({}, state);
});

@ -0,0 +1,27 @@
import './console-frame.scss';
import * as consoleActions from '../actions/console';
const initialize = (doc) => {
let iframe = doc.createElement('iframe');
iframe.src = browser.runtime.getURL('build/console.html');
iframe.id = 'vimvixen-console-frame';
iframe.className = 'vimvixen-console-frame';
doc.body.append(iframe);
return iframe;
}
const showCommand = (text) => {
return browser.runtime.sendMessage(consoleActions.showCommand(text));
};
const showError = (text) => {
return browser.runtime.sendMessage(consoleActions.showError(text));
}
const blur = (doc) => {
let iframe = doc.getElementById('vimvixen-console-frame');
iframe.blur();
}
export { initialize, showCommand, showError, blur };

@ -1,129 +1,41 @@
import * as scrolls from './scrolls'; import '../console/console-frame.scss';
import * as histories from './histories'; import * as inputActions from '../actions/input';
import * as actions from '../shared/actions'; import * as consoleFrames from '../console/frames';
import * as messages from '../shared/messages'; import actions from '../actions';
import ConsoleFrame from '../console/console-frame'; import contentReducer from '../reducers/content';
import Follow from './follow';
let vvConsole = new ConsoleFrame(window); consoleFrames.initialize(window.document);
const doAction = (action) => { browser.runtime.onMessage.addListener((action) => {
if (typeof action === 'undefined' || action === null) { contentReducer(undefined, action);
return; return Promise.resolve();
} });
switch (action[0]) {
case actions.CMD_OPEN:
vvConsole.showCommand('');
break;
case actions.CMD_TABS_OPEN:
if (action[1] || false) {
// alter url
vvConsole.showCommand('open ' + window.location.href);
} else {
vvConsole.showCommand('open ');
}
break;
case actions.CMD_BUFFER:
vvConsole.showCommand('buffer ');
break;
case actions.SCROLL_LINES:
scrolls.scrollLines(window, action[1]);
break;
case actions.SCROLL_PAGES:
scrolls.scrollPages(window, action[1]);
break;
case actions.SCROLL_TOP:
scrolls.scrollTop(window);
break;
case actions.SCROLL_BOTTOM:
scrolls.scrollBottom(window);
break;
case actions.SCROLL_LEFT:
scrolls.scrollLeft(window);
break;
case actions.SCROLL_RIGHT:
scrolls.scrollRight(window);
break;
case actions.FOLLOW_START:
new Follow(window.document, action[1] || false);
break;
case actions.HISTORY_PREV:
histories.prev(window);
break;
case actions.HISTORY_NEXT:
histories.next(window);
break;
}
}
const handleResponse = (response) => {
if (!response) {
return;
}
switch(response.type) {
case 'response.action':
doAction(response.action);
break;
}
};
window.addEventListener("keypress", (e) => { window.addEventListener("keypress", (e) => {
if (e.target instanceof HTMLInputElement) { if (e.target instanceof HTMLInputElement) {
return; return;
} }
browser.runtime.sendMessage(inputActions.keyPress(e.which, e.ctrlKey))
let request = {
type: 'event.keypress',
code: e.which,
ctrl: e.ctrlKey,
}
browser.runtime.sendMessage(request)
.then(handleResponse)
.catch((err) => { .catch((err) => {
console.error("Vim Vixen:", err); console.error("Vim Vixen:", err);
vvConsole.showError(err.message); return consoleFrames.showError(err.message);
}); });
}); });
const doCompletion = (line) => { browser.runtime.onMessage.addListener((action) => {
if (line.startsWith('buffer ')) { switch (action.type) {
let keyword = line.replace('buffer ', ''); case actions.CONSOLE_HIDE:
window.focus();
browser.runtime.sendMessage({ return consoleFrames.blur(window.document);
type: 'event.cmd.tabs.completion',
text: keyword
}).then((completions) => {
vvConsole.setCompletions([completions]);
}).catch((err) => {
console.error("Vim Vixen:", err);
vvConsole.showError(err.message);
});
}
};
messages.receive(window, (message) => {
switch (message.type) {
case 'vimvixen.command.blur':
if (!vvConsole.isErrorShown()) {
vvConsole.hide();
}
break;
case 'vimvixen.command.enter': case 'vimvixen.command.enter':
browser.runtime.sendMessage({ return browser.runtime.sendMessage({
type: 'event.cmd.enter', type: 'event.cmd.enter',
text: message.value text: action.value
}).catch((err) => { }).catch((err) => {
console.error("Vim Vixen:", err); console.error("Vim Vixen:", err);
vvConsole.showError(err.message); return consoleFrames.showError(err.message);
}); });
break;
case 'vimvixen.command.change':
doCompletion(message.value);
break;
default: default:
return; return Promise.resolve();
} }
}); });

@ -0,0 +1,53 @@
import * as tabs from '../background/tabs';
import * as zooms from '../background/zooms';
import * as consoleActions from '../actions/console';
import actions from '../actions';
const doCompletion = (command, keywords, sender) => {
if (command === 'buffer') {
return tabs.getCompletions(keywords).then((tabs) => {
let items = tabs.map((tab) => {
return {
caption: tab.title,
content: tab.title,
url: tab.url,
icon: tab.favIconUrl
}
});
let completions = {
name: "Buffers",
items: items
};
return browser.tabs.sendMessage(
sender,
consoleActions.setCompletions([completions]));
});
}
return Promise.resolve();
};
export default function reducer(state, action = {}, sender) {
// TODO hide sender object
switch (action.type) {
case actions.BACKGROUND_REQUEST_COMPLETIONS:
return doCompletion(action.command, action.keywords, sender.tab.id);
case actions.TABS_CLOSE:
return tabs.closeTab(sender.tab.id);
case actions.TABS_REOPEN:
return tabs.reopenTab();
case actions.TABS_PREV:
return tabs.selectPrevTab(sender.tab.index, action.count);
case actions.TABS_NEXT:
return tabs.selectNextTab(sender.tab.index, action.count);
case actions.TABS_RELOAD:
return tabs.reload(sender.tab, action.cache);
case actions.ZOOM_IN:
return zooms.zoomIn();
case actions.ZOOM_OUT:
return zooms.zoomOut();
case actions.ZOOM_NEUTRAL:
return zooms.neutral();
default:
return Promise.resolve();
}
}

@ -0,0 +1,24 @@
import * as tabs from '../background/tabs';
import actions from '../actions';
const cmdBuffer = (sender, arg) => {
if (isNaN(arg)) {
return tabs.selectByKeyword(sender.tab, arg);
} else {
let index = parseInt(arg, 10) - 1;
return tabs.selectAt(index);
}
}
export default function reducer(state, action, sender) {
switch (action.type) {
case actions.COMMAND_OPEN_URL:
return browser.tabs.update(sender.tab.id, { url: action.url });
case actions.COMMAND_TABOPEN_URL:
return browser.tabs.create({ url: action.url });
case actions.COMMAND_BUFFER:
return cmdBuffer(sender, action.keywords);
default:
return Promise.resolve();
}
}

@ -0,0 +1,39 @@
import actions from '../actions';
const defaultState = {
errorShown: false,
errorText: '',
commandShown: false,
commandText: '',
completions: [],
};
export default function reducer(state = defaultState, action = {}) {
switch (action.type) {
case actions.CONSOLE_SHOW_COMMAND:
return Object.assign({}, state, {
commandShown: true,
commandText: action.text,
errorShown: false,
completions: []
});
case actions.CONSOLE_SET_COMPLETIONS:
return Object.assign({}, state, {
completions: action.completions
});
case actions.CONSOLE_SHOW_ERROR:
return Object.assign({}, state, {
errorText: action.text,
errorShown: true,
commandShown: false,
});
case actions.CONSOLE_HIDE:
return Object.assign({}, state, {
errorShown: false,
commandShown: false
});
default:
return state;
}
}

@ -0,0 +1,48 @@
import * as consoleFrames from '../console/frames';
import * as histories from '../content/histories';
import * as scrolls from '../content/scrolls';
import Follow from '../content/follow';
import actions from '../actions';
export default function reducer(state, action = {}) {
switch (action.type) {
case actions.CMD_OPEN:
return consoleFrames.showCommand('');
case actions.CMD_TABS_OPEN:
if (action.alter) {
// alter url
return consoleFrames.showCommand('open ' + window.location.href);
} else {
return consoleFrames.showCommand('open ');
}
case actions.CMD_BUFFER:
return consoleFrames.showCommand('buffer ');
case actions.SCROLL_LINES:
scrolls.scrollLines(window, action.count);
break;
case actions.SCROLL_PAGES:
scrolls.scrollPages(window, action.count);
break;
case actions.SCROLL_TOP:
scrolls.scrollTop(window);
break;
case actions.SCROLL_BOTTOM:
scrolls.scrollBottom(window);
break;
case actions.SCROLL_LEFT:
scrolls.scrollLeft(window);
break;
case actions.SCROLL_RIGHT:
scrolls.scrollRight(window);
break;
case actions.FOLLOW_START:
new Follow(window.document, action.newTab);
break;
case actions.HISTORY_PREV:
histories.prev(window);
break;
case actions.HISTORY_NEXT:
histories.next(window);
break;
}
}

@ -0,0 +1,23 @@
import actions from '../actions';
const defaultState = {
keys: [],
};
export default function reducer(state = defaultState, action = {}) {
switch (action.type) {
case actions.INPUT_KEY_PRESS:
return Object.assign({}, state, {
keys: state.keys.concat([{
code: action.code,
ctrl: action.ctrl
}])
});
case actions.INPUT_CLEAR_KEYS:
return Object.assign({}, state, {
keys: [],
});
default:
return state;
}
}

@ -1,54 +0,0 @@
export const CMD_OPEN = 'cmd.open';
export const CMD_TABS_OPEN = 'cmd.tabs.open';
export const CMD_BUFFER = 'cmd.buffer';
export const TABS_CLOSE = 'tabs.close';
export const TABS_REOPEN = 'tabs.reopen';
export const TABS_PREV = 'tabs.prev';
export const TABS_NEXT = 'tabs.next';
export const TABS_RELOAD = 'tabs.reload';
export const SCROLL_LINES = 'scroll.lines';
export const SCROLL_PAGES = 'scroll.pages';
export const SCROLL_TOP = 'scroll.top';
export const SCROLL_BOTTOM = 'scroll.bottom';
export const SCROLL_LEFT= 'scroll.left';
export const SCROLL_RIGHT= 'scroll.right';
export const FOLLOW_START = 'follow.start';
export const HISTORY_PREV = 'history.prev';
export const HISTORY_NEXT = 'history.next';
export const ZOOM_IN = 'zoom.in';
export const ZOOM_OUT = 'zoom.out';
export const ZOOM_NEUTRAL = 'zoom.neutral';
const BACKGROUND_ACTION_SET = new Set([
TABS_CLOSE,
TABS_REOPEN,
TABS_PREV,
TABS_NEXT,
TABS_RELOAD,
ZOOM_IN,
ZOOM_OUT,
ZOOM_NEUTRAL
]);
const CONTENT_ACTION_SET = new Set([
CMD_OPEN,
CMD_TABS_OPEN,
CMD_BUFFER,
SCROLL_LINES,
SCROLL_PAGES,
SCROLL_TOP,
SCROLL_BOTTOM,
SCROLL_LEFT,
SCROLL_RIGHT,
FOLLOW_START,
HISTORY_PREV,
HISTORY_NEXT
]);
export const isBackgroundAction = (action) => {
return BACKGROUND_ACTION_SET.has(action);
};
export const isContentAction = (action) => {
return CONTENT_ACTION_SET.has(action);
};

@ -1,19 +0,0 @@
const receive = (win, callback) => {
win.addEventListener('message', (e) => {
let message;
try {
message = JSON.parse(e.data);
} catch (e) {
// ignore message posted by author of web page
return;
}
callback(message);
})
}
const send = (win, message) => {
win.postMessage(JSON.stringify(message), '*');
}
export { receive, send };

@ -0,0 +1,14 @@
import { expect } from "chai";
import actions from '../../src/actions';
import * as backgroundActions from '../../src/actions/background';
describe("background actions", () => {
describe("requestCompletions", () => {
it('create BACKGROUND_REQUEST_COMPLETIONS action', () => {
let action = backgroundActions.requestCompletions('buffer hoge fuga');
expect(action.type).to.equal(actions.BACKGROUND_REQUEST_COMPLETIONS);
expect(action.command).to.equal('buffer');
expect(action.keywords).to.equal('hoge fuga');
});
});
});

@ -0,0 +1,51 @@
import { expect } from "chai";
import actions from '../../src/actions';
import * as commandActions from '../../src/actions/command';
describe("command actions", () => {
describe("exec", () => {
context("open command", () => {
it('create COMMAND_OPEN_URL acion with a full url', () => {
let action = commandActions.exec("open https://github.com/")
expect(action.type).to.equal(actions.COMMAND_OPEN_URL);
expect(action.url).to.equal('https://github.com/');
});
it('create COMMAND_OPEN_URL acion with a domain name', () => {
let action = commandActions.exec("open github.com")
expect(action.type).to.equal(actions.COMMAND_OPEN_URL);
expect(action.url).to.equal('http://github.com');
});
});
context("tabopen command", () => {
it('create COMMAND_TABOPEN_URL acion with a full url', () => {
let action = commandActions.exec("tabopen https://github.com/")
expect(action.type).to.equal(actions.COMMAND_TABOPEN_URL);
expect(action.url).to.equal('https://github.com/');
});
it('create COMMAND_TABOPEN_URL acion with a domain name', () => {
let action = commandActions.exec("tabopen github.com")
expect(action.type).to.equal(actions.COMMAND_TABOPEN_URL);
expect(action.url).to.equal('http://github.com');
});
});
context("buffer command", () => {
it('create COMMAND_BUFFER acion with a keywords', () => {
let action = commandActions.exec("buffer foo bar")
expect(action.type).to.equal(actions.COMMAND_BUFFER);
expect(action.keywords).to.equal('foo bar');
});
});
context("b command", () => {
it('create COMMAND_BUFFER acion with a keywords', () => {
let action = commandActions.exec("b foo bar")
expect(action.type).to.equal(actions.COMMAND_BUFFER);
expect(action.keywords).to.equal('foo bar');
});
});
});
});

@ -0,0 +1,37 @@
import { expect } from "chai";
import actions from '../../src/actions';
import * as consoleActions from '../../src/actions/console';
describe("console actions", () => {
describe("showCommand", () => {
it('create CONSOLE_SHOW_COMMAND action', () => {
let action = consoleActions.showCommand('hello');
expect(action.type).to.equal(actions.CONSOLE_SHOW_COMMAND);
expect(action.text).to.equal('hello');
});
});
describe("setCompletions", () => {
it('create CONSOLE_SET_COMPLETIONS action', () => {
let action = consoleActions.setCompletions([1,2,3]);
expect(action.type).to.equal(actions.CONSOLE_SET_COMPLETIONS);
expect(action.completions).to.deep.equal([1, 2, 3]);
});
});
describe("showError", () => {
it('create CONSOLE_SHOW_ERROR action', () => {
let action = consoleActions.showError('an error');
expect(action.type).to.equal(actions.CONSOLE_SHOW_ERROR);
expect(action.text).to.equal('an error');
});
});
describe("hide", () => {
it('create CONSOLE_HIDE action', () => {
let action = consoleActions.hide();
expect(action.type).to.equal(actions.CONSOLE_HIDE);
});
});
});

@ -0,0 +1,21 @@
import { expect } from "chai";
import actions from '../../src/actions';
import * as inputActions from '../../src/actions/input';
describe("input actions", () => {
describe("keyPress", () => {
it('create INPUT_KEY_PRESS action', () => {
let action = inputActions.keyPress(123, true);
expect(action.type).to.equal(actions.INPUT_KEY_PRESS);
expect(action.code).to.equal(123);
expect(action.ctrl).to.be.true;
});
});
describe("clearKeys", () => {
it('create INPUT_CLEAR_KEYSaction', () => {
let action = inputActions.clearKeys();
expect(action.type).to.equal(actions.INPUT_CLEAR_KEYS);
});
});
});

@ -1,50 +0,0 @@
import { expect } from "chai";
import KeyQueue from '../../src/background/key-queue';
describe("keyQueue class", () => {
const KEYMAP = {
'g<C-X>GG': [],
'gg': [ 'scroll.top' ],
};
const g = 'g'.charCodeAt(0);
const G = 'G'.charCodeAt(0);
const x = 'x'.charCodeAt(0);
describe("#push", () => {
it("returns matched action", () => {
let queue = new KeyQueue(KEYMAP);
queue.push({ code: g });
let action = queue.push({ code: g });
expect(action).to.deep.equal([ 'scroll.top' ]);
});
it("returns null on no actions matched", () => {
let queue = new KeyQueue(KEYMAP);
queue.push({ code: g });
let action = queue.push({ code: G });
expect(action).to.be.null;
expect(queue.asKeymapChars()).to.be.empty;
});
});
describe('#asKeymapChars', () => {
let queue = new KeyQueue(KEYMAP);
queue.push({ code: g });
queue.push({ code: x, ctrl: true });
queue.push({ code: G });
expect(queue.asKeymapChars()).to.equal('g<C-X>G');
});
describe('#asCaretChars', () => {
let queue = new KeyQueue(KEYMAP);
queue.push({ code: g });
queue.push({ code: x, ctrl: true });
queue.push({ code: G });
expect(queue.asCaretChars()).to.equal('g^XG');
});
});

@ -1,55 +1,31 @@
import { expect } from "chai"; import { expect } from "chai";
import { identifyKey, identifyKeys, hasPrefix } from '../../src/background/keys'; import * as keys from '../../src/background/keys';
describe('keys', () => { describe("keys", () => {
describe('#identifyKey', () => { const KEYMAP = {
it('return true if key matched', () => { 'g<C-X>GG': [],
expect(identifyKey( 'gg': { type: 'scroll.top' },
{ code: 100 }, };
{ code: 100 })).to.be.true;
expect(identifyKey(
{ code: 100, shift: true, ctrl: true },
{ code: 100, shift: true, ctrl: true })).to.be.true;
expect(identifyKey(
{ code: 100, shift: false, ctrl: false },
{ code: 100 })).to.be.true;
});
it('return false if key not matched', () => { const g = 'g'.charCodeAt(0);
expect(identifyKey( const G = 'G'.charCodeAt(0);
{ code: 100 }, const x = 'x'.charCodeAt(0);
{ code: 101 })).to.be.false;
expect(identifyKey(
{ code: 100, shift: true, ctrl: true },
{ code: 100, shift: true })).to.be.false;
});
});
describe('#identifyKeys', () => { describe('#asKeymapChars', () => {
it ('return true if keys matched', () => { let keySequence = [
let keys = [{ code: 100 }, { code: 101, ctrl: false}]; { code: g },
let prefix = [{ code: 100, ctrl: false }, { code: 101 }]; { code: x, ctrl: true },
expect(hasPrefix(keys, prefix)).to.be.true; { code: G }
];
expect(keys.asKeymapChars(keySequence)).to.equal('g<C-X>G');
}); });
it ('return false if keys matched', () => { describe('#asCaretChars', () => {
let keys = [{ code: 100 }, { code: 101, ctrl: true }]; let keySequence = [
let prefix = [{ code: 100 }, { code: 101 }]; { code: g },
expect(hasPrefix(keys, prefix)).to.be.false; { code: x, ctrl: true },
}); { code: G }
}); ];
expect(keys.asCaretChars(keySequence)).to.equal('g^XG');
describe('#hasPrefix', () => {
it ('return true if prefix matched', () => {
let keys = [{ code: 100 }, { code: 101 }, { code: 102 }];
let prefix = [{ code: 100 }, { code: 101 }];
expect(hasPrefix(keys, prefix)).to.be.true;
});
it ('return false if prefix not matched', () => {
let keys = [{ code: 100 }, { code: 101 }, { code: 102 }];
let prefix = [{ code: 102 }];
expect(hasPrefix(keys, prefix)).to.be.false;
});
}); });
}); });

@ -0,0 +1,43 @@
import { expect } from "chai";
import actions from '../../src/actions';
import consoleReducer from '../../src/reducers/console';
describe("console reducer", () => {
it('return the initial state', () => {
let state = consoleReducer(undefined, {});
expect(state).to.have.property('errorShown', false);
expect(state).to.have.property('errorText', '');
expect(state).to.have.property('commandShown', false);
expect(state).to.have.property('commandText', '');
expect(state).to.have.deep.property('completions', []);
});
it('return next state for CONSOLE_SHOW_COMMAND', () => {
let action = { type: actions.CONSOLE_SHOW_COMMAND, text: 'open ' };
let state = consoleReducer({}, action);
expect(state).to.have.property('commandShown', true);
expect(state).to.have.property('commandText', 'open ');
expect(state).to.have.property('errorShown', false);
});
it('return next state for CONSOLE_SET_COMPLETIONS', () => {
let action = { type: actions.CONSOLE_SET_COMPLETIONS, completions: [1, 2, 3] };
let state = consoleReducer({}, action);
expect(state).to.have.deep.property('completions', [1, 2, 3]);
});
it('return next state for CONSOLE_SHOW_ERROR', () => {
let action = { type: actions.CONSOLE_SHOW_ERROR, text: 'an error' };
let state = consoleReducer({}, action);
expect(state).to.have.property('errorShown', true);
expect(state).to.have.property('errorText', 'an error');
expect(state).to.have.property('commandShown', false);
});
it('return next state for CONSOLE_HIDE', () => {
let action = { type: actions.CONSOLE_HIDE };
let state = consoleReducer({}, action);
expect(state).to.have.property('errorShown', false);
expect(state).to.have.property('commandShown', false);
});
});

@ -0,0 +1,34 @@
import { expect } from "chai";
import actions from '../../src/actions';
import inputReducer from '../../src/reducers/input';
describe("input reducer", () => {
it('return the initial state', () => {
let state = inputReducer(undefined, {});
expect(state).to.have.deep.property('keys', []);
});
it('return next state for INPUT_KEY_PRESS', () => {
let action = { type: actions.INPUT_KEY_PRESS, code: 123, ctrl: true };
let state = inputReducer(undefined, action);
expect(state).to.have.deep.property('keys', [{ code: 123, ctrl: true }]);
action = { type: actions.INPUT_KEY_PRESS, code: 456, ctrl: false };
state = inputReducer(state, action);
expect(state).to.have.deep.property('keys', [
{ code: 123, ctrl: true },
{ code: 456, ctrl: false }
]);
});
it('return next state for INPUT_CLEAR_KEYS', () => {
let action = { type: actions.INPUT_CLEAR_KEYS };
let state = inputReducer({
keys: [
{ code: 123, ctrl: true },
{ code: 456, ctrl: false }
]
}, action);
expect(state).to.have.deep.property('keys', []);
});
});

@ -1,25 +0,0 @@
import { expect } from "chai";
import * as messages from '../../src/shared/messages';
describe('messages', () => {
describe('#receive', () => {
it('received a message', (done) => {
messages.receive(window, (message) => {
expect(message).to.deep.equal({ type: 'vimvixen.test' });
done();
});
window.postMessage(JSON.stringify({ type: 'vimvixen.test' }), '*');
});
});
describe('#send', () => {
it('sends a message', (done) => {
window.addEventListener('message', (e) => {
let json = JSON.parse(e.data);
expect(json).to.deep.equal({ type: 'vimvixen.test' });
done();
});
messages.send(window, { type: 'vimvixen.test' });
});
});
});