diff --git a/src/background/index.js b/src/background/index.js index 15c8ab0..8913a83 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -55,9 +55,9 @@ const normalizeUrl = (string) => { } } -const cmdBuffer = (arg) => { +const cmdBuffer = (sender, arg) => { if (isNaN(arg)) { - return tabs.selectByKeyword(arg); + return tabs.selectByKeyword(sender.tab, arg); } else { let index = parseInt(arg, 10) - 1; return tabs.selectAt(index); @@ -73,7 +73,7 @@ const cmdEnterHandle = (request, sender) => { return browser.tabs.create({ url: normalizeUrl(words[1]) }); case 'b': case 'buffer': - return cmdBuffer(words[1]); + return cmdBuffer(sender, words[1]); } throw new Error(words[0] + ' command is not defined'); }; @@ -84,9 +84,20 @@ browser.runtime.onMessage.addListener((request, sender) => { return keyPressHandle(request, sender); case 'event.cmd.enter': return cmdEnterHandle(request, sender); - case 'event.cmd.suggest': - // TODO make suggestion and return - break; + 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 { + name: "Buffers", + items: items + }; + }); } - return Promise.resolve(); }); diff --git a/src/background/tabs.js b/src/background/tabs.js index efecdc4..111bbd9 100644 --- a/src/background/tabs.js +++ b/src/background/tabs.js @@ -31,7 +31,7 @@ const selectAt = (index) => { }); }; -const selectByKeyword = (keyword) => { +const selectByKeyword = (current, keyword) => { return browser.tabs.query({ currentWindow: true }).then((tabs) => { let matched = tabs.filter((t) => { return t.url.includes(keyword) || t.title.includes(keyword) @@ -39,14 +39,25 @@ const selectByKeyword = (keyword) => { if (matched.length == 0) { throw new RangeError('No matching buffer for ' + keyword); - } else if (matched.length >= 2) { - throw new RangeError('More than one match for ' + keyword); } - + for (let tab of matched) { + if (tab.index > current.index) { + return browser.tabs.update(tab.id, { active: true }); + } + } return browser.tabs.update(matched[0].id, { active: true }); }); } +const getCompletions = (keyword) => { + return browser.tabs.query({ currentWindow: true }).then((tabs) => { + let matched = tabs.filter((t) => { + return t.url.includes(keyword) || t.title.includes(keyword) + }) + return matched; + }); +}; + const selectPrevTab = (current, count) => { return browser.tabs.query({ currentWindow: true }, (tabs) => { if (tabs.length < 2) { @@ -76,4 +87,4 @@ const reload = (current, cache) => { ); }; -export { closeTab, reopenTab, selectAt, selectByKeyword, selectNextTab, selectPrevTab, reload }; +export { closeTab, reopenTab, selectAt, selectByKeyword, getCompletions, selectPrevTab, selectNextTab, reload }; diff --git a/src/console/completion.js b/src/console/completion.js new file mode 100644 index 0000000..0c21cb0 --- /dev/null +++ b/src/console/completion.js @@ -0,0 +1,26 @@ +export default class Completion { + constructor(completions) { + if (typeof completions.length !== 'number') { + throw new TypeError('completions does not have a length in number'); + } + this.completions = completions + this.index = 0; + } + + prev() { + if (this.completions.length === 0) { + return null; + } + this.index = (this.index + this.completions.length - 1) % this.completions.length + return this.completions[this.index]; + } + + next() { + if (this.completions.length === 0) { + return null; + } + let item = this.completions[this.index]; + this.index = (this.index + 1) % this.completions.length + return item; + } +} diff --git a/src/console/console-frame.js b/src/console/console-frame.js index ea9f523..e6bf3f5 100644 --- a/src/console/console-frame.js +++ b/src/console/console-frame.js @@ -51,4 +51,11 @@ export default class ConsoleFrame { isErrorShown() { return this.element.style.display === 'block' && this.errorShown; } + + setCompletions(completions) { + messages.send(this.element.contentWindow, { + type: 'vimvixen.console.set.completions', + completions: completions + }); + } } diff --git a/src/console/console.html b/src/console/console.html index 2eb445d..4222f12 100644 --- a/src/console/console.html +++ b/src/console/console.html @@ -9,7 +9,7 @@

-

+
{ return { @@ -30,6 +33,36 @@ const handleBlur = () => { messages.send(parent, blurMessage()); }; +const completeNext = () => { + if (!completion) { + return; + } + let item = completion.next(); + if (!item) { + return; + } + + let input = window.document.querySelector('#vimvixen-console-command-input'); + input.value = completionOrigin + ' ' + item[0].content; + + selectCompletion(item[1]); +} + +const completePrev = () => { + if (!completion) { + return; + } + let item = completion.prev(); + if (!item) { + return; + } + + let input = window.document.querySelector('#vimvixen-console-command-input'); + input.value = completionOrigin + ' ' + item[0].content; + + selectCompletion(item[1]); +} + const handleKeydown = (e) => { switch(e.keyCode) { case KeyboardEvent.DOM_VK_ESCAPE: @@ -38,10 +71,22 @@ const handleKeydown = (e) => { case KeyboardEvent.DOM_VK_RETURN: messages.send(parent, keydownMessage(e.target)); break; + case KeyboardEvent.DOM_VK_TAB: + if (e.shiftKey) { + completePrev(); + } else { + completeNext(); + } + e.stopPropagation(); + e.preventDefault(); + break; } }; const handleKeyup = (e) => { + if (e.keyCode === KeyboardEvent.DOM_VK_TAB) { + return; + } if (e.target.value === prevValue) { return; } @@ -66,6 +111,11 @@ const showCommand = (text) => { 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) => { @@ -75,8 +125,74 @@ const showError = (text) => { 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) => { + let li = document.createElement('li'); + li.className = 'vimvixen-console-completion-title'; + li.textContent = text; + return li } +const createCompletionItem = (icon, caption, url) => { + let captionEle = document.createElement('span'); + captionEle.className = 'vimvixen-console-completion-item-caption'; + captionEle.textContent = caption + + let urlEle = document.createElement('span'); + urlEle.className = 'vimvixen-console-completion-item-url'; + urlEle.textContent = url + + let li = document.createElement('li'); + li.style.backgroundImage = 'url(' + icon + ')'; + li.className = 'vimvixen-console-completion-item'; + li.append(captionEle); + li.append(urlEle); + return li; +} + +const setCompletions = (completions) => { + let container = window.document.querySelector('#vimvixen-console-completion'); + container.style.display = 'block'; + container.innerHTML = ''; + + let pairs = []; + + for (let group of completions) { + let title = createCompletionTitle(group.name); + container.append(title); + + for (let item of group.items) { + let li = createCompletionItem(item.icon, item.caption, item.url); + container.append(li); + + pairs.push([item, li]); + } + } + + completion = new Completion(pairs); + + let input = window.document.querySelector('#vimvixen-console-command-input'); + completionOrigin = input.value.split(' ')[0]; +} + +const selectCompletion = (target) => { + let container = window.document.querySelector('#vimvixen-console-completion'); + Array.prototype.forEach.call(container.children, (ele) => { + 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'); + } + }); +}; + messages.receive(window, (message) => { switch (message.type) { case 'vimvixen.console.show.command': @@ -85,5 +201,8 @@ messages.receive(window, (message) => { case 'vimvixen.console.show.error': showError(message.text); break; + case 'vimvixen.console.set.completions': + setCompletions(message.completions); + break; } }); diff --git a/src/console/console.scss b/src/console/console.scss index 0de873d..7bb46dd 100644 --- a/src/console/console.scss +++ b/src/console/console.scss @@ -8,6 +8,7 @@ body { bottom: 0; left: 0; right: 0; + overflow: hidden; } .vimvixen-console { @@ -23,20 +24,45 @@ body { line-height: 16px; } - &-error { - background-color: red; - font-weight: bold; - color: white; + &-completion { + background-color: white; @include consoole-font; - } + &-title { + background-color: lightgray; + font-weight: bold; + margin: 0; + padding: 0; + } + + &-item { + padding-left: 1.5rem; + background-position: 0 center; + background-size: contain; + background-repeat: no-repeat; + white-space: nowrap; - &-title { - background-color: lightgray; + &.vimvixen-completion-selected { + background-color: yellow; + } + + &-caption { + display: inline-block; + width: 40%; + } + + &-url { + display: inline-block; + color: green; + } + } + } + + &-error { + background-color: red; font-weight: bold; - margin: 0; - padding: 0; + color: white; @include consoole-font; } diff --git a/src/content/index.js b/src/content/index.js index 8b3eb58..fdc7e89 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -83,11 +83,27 @@ window.addEventListener("keypress", (e) => { browser.runtime.sendMessage(request) .then(handleResponse) .catch((err) => { + console.error("Vim Vixen:", err); vvConsole.showError(err.message); - console.log(`Vim Vixen: ${err}`); }); }); +const doCompletion = (line) => { + if (line.startsWith('buffer ')) { + let keyword = line.replace('buffer ', ''); + + browser.runtime.sendMessage({ + 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': @@ -99,17 +115,13 @@ messages.receive(window, (message) => { browser.runtime.sendMessage({ type: 'event.cmd.enter', text: message.value - }).catch((e) => { - vvConsole.showError(e.message); + }).catch((err) => { + console.error("Vim Vixen:", err); + vvConsole.showError(err.message); }); break; case 'vimvixen.command.change': - browser.runtime.sendMessage({ - type: 'event.cmd.suggest', - text: message.value - }).catch((e) => { - vvConsole.showError(e.message); - }); + doCompletion(message.value); break; default: return; diff --git a/test/console/completion.test.js b/test/console/completion.test.js new file mode 100644 index 0000000..a789c15 --- /dev/null +++ b/test/console/completion.test.js @@ -0,0 +1,48 @@ +import { expect } from "chai"; +import Completion from '../../src/console/completion'; + +describe('Completion class', () => { + describe('#constructor', () => { + it('creates new object by iterable items', () => { + new Completion([1,2,3,4,5]); + new Completion([]); + new Completion('hello'); + new Completion(''); + }); + + it('creates new object by iterable items', () => { + expect(() => new Completion({ key: 'value' })).to.throw(TypeError); + expect(() => new Completion(12345)).to.throw(TypeError); + }); + }); + + describe('#next', () => { + it('complete next items', () => { + let completion = new Completion([3, 4, 5]); + expect(completion.next()).to.equal(3); + expect(completion.next()).to.equal(4); + expect(completion.next()).to.equal(5); + expect(completion.next()).to.equal(3); + }); + + it('returns null when empty completions', () => { + let completion = new Completion([]); + expect(completion.next()).to.be.null; + }); + }); + + describe('#prev', () => { + it('complete prev items', () => { + let completion = new Completion([3, 4, 5]); + expect(completion.prev()).to.equal(5); + expect(completion.prev()).to.equal(4); + expect(completion.prev()).to.equal(3); + expect(completion.prev()).to.equal(5); + }); + + it('returns null when empty completions', () => { + let completion = new Completion([]); + expect(completion.prev()).to.be.null; + }); + }); +});