From 0c2fcf74bbb49727163ea64486da2a611feebbe7 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Tue, 12 Feb 2019 21:16:38 +0900 Subject: [PATCH 1/5] Use single index on completions of the console --- src/console/components/console/completion.jsx | 8 +-- src/console/reducers/index.js | 69 ++++++++----------- test/console/reducers/console.test.js | 38 ++++------ 3 files changed, 45 insertions(+), 70 deletions(-) diff --git a/src/console/components/console/completion.jsx b/src/console/components/console/completion.jsx index c60543b..5f128d7 100644 --- a/src/console/components/console/completion.jsx +++ b/src/console/components/console/completion.jsx @@ -27,19 +27,17 @@ const CompletionItem = (props) => { class CompletionComponent extends Component { render() { let eles = []; + let index = 0; for (let i = 0; i < this.props.completions.length; ++i) { let group = this.props.completions[i]; eles.push(); - for (let j = 0; j < group.items.length; ++j) { + for (let j = 0; j < group.items.length; ++j, ++index) { let item = group.items[j]; - let selected = - i === this.props.groupSelection && - j === this.props.itemSelection; eles.push(); } } diff --git a/src/console/reducers/index.js b/src/console/reducers/index.js index 7dcad17..bcc7a2e 100644 --- a/src/console/reducers/index.js +++ b/src/console/reducers/index.js @@ -6,52 +6,42 @@ const defaultState = { consoleText: '', completionSource: '', completions: [], - groupSelection: -1, - itemSelection: -1, + select: -1, }; const nextSelection = (state) => { if (state.completions.length === 0) { - return [-1, -1]; + return -1; } - if (state.groupSelection < 0) { - return [0, 0]; + if (state.select < 0) { + return 0; } - let group = state.completions[state.groupSelection]; - if (state.groupSelection + 1 >= state.completions.length && - state.itemSelection + 1 >= group.items.length) { - return [-1, -1]; + let length = state.completions + .map(g => g.items.length) + .reduce((x, y) => x + y); + if (state.select + 1 < length) { + return state.select + 1; } - if (state.itemSelection + 1 >= group.items.length) { - return [state.groupSelection + 1, 0]; - } - return [state.groupSelection, state.itemSelection + 1]; + return -1; }; const prevSelection = (state) => { - if (state.groupSelection < 0) { - return [ - state.completions.length - 1, - state.completions[state.completions.length - 1].items.length - 1 - ]; - } - if (state.groupSelection === 0 && state.itemSelection === 0) { - return [-1, -1]; - } else if (state.itemSelection === 0) { - return [ - state.groupSelection - 1, - state.completions[state.groupSelection - 1].items.length - 1 - ]; + let length = state.completions + .map(g => g.items.length) + .reduce((x, y) => x + y); + if (state.select < 0) { + return length - 1; } - return [state.groupSelection, state.itemSelection - 1]; + return state.select - 1; }; -const nextConsoleText = (completions, group, item, defaults) => { - if (group < 0 || item < 0) { +const nextConsoleText = (completions, select, defaults) => { + if (select < 0) { return defaults; } - return completions[group].items[item].content; + let items = completions.map(g => g.items).reduce((g1, g2) => g1.concat(g2)); + return items[select].content; }; // eslint-disable-next-line max-lines-per-function @@ -90,25 +80,20 @@ export default function reducer(state = defaultState, action = {}) { return { ...state, completions: action.completions, completionSource: action.completionSource, - groupSelection: -1, - itemSelection: -1, }; + select: -1 }; case actions.CONSOLE_COMPLETION_NEXT: { - let next = nextSelection(state); + let select = nextSelection(state); return { ...state, - groupSelection: next[0], - itemSelection: next[1], + select: select, consoleText: nextConsoleText( - state.completions, next[0], next[1], - state.completionSource), }; + state.completions, select, state.completionSource) }; } case actions.CONSOLE_COMPLETION_PREV: { - let next = prevSelection(state); + let select = prevSelection(state); return { ...state, - groupSelection: next[0], - itemSelection: next[1], + select: select, consoleText: nextConsoleText( - state.completions, next[0], next[1], - state.completionSource), }; + state.completions, select, state.completionSource) }; } default: return state; diff --git a/test/console/reducers/console.test.js b/test/console/reducers/console.test.js index db40088..d5a38cf 100644 --- a/test/console/reducers/console.test.js +++ b/test/console/reducers/console.test.js @@ -8,8 +8,7 @@ describe("console reducer", () => { expect(state).to.have.property('messageText', ''); expect(state).to.have.property('consoleText', ''); expect(state).to.have.deep.property('completions', []); - expect(state).to.have.property('groupSelection', -1); - expect(state).to.have.property('itemSelection', -1); + expect(state).to.have.property('select', -1); }); it('return next state for CONSOLE_HIDE', () => { @@ -60,8 +59,7 @@ describe("console reducer", () => { it ('return next state for CONSOLE_SET_COMPLETIONS', () => { let state = { - groupSelection: 0, - itemSelection: 0, + select: 0, completions: [], } let action = { @@ -76,15 +74,13 @@ describe("console reducer", () => { } state = reducer(state, action); expect(state).to.have.property('completions', action.completions); - expect(state).to.have.property('groupSelection', -1); - expect(state).to.have.property('itemSelection', -1); + expect(state).to.have.property('select', -1); }); it ('return next state for CONSOLE_COMPLETION_NEXT', () => { let action = { type: actions.CONSOLE_COMPLETION_NEXT }; let state = { - groupSelection: -1, - itemSelection: -1, + select: -1, completions: [{ name: 'Apple', items: [1, 2] @@ -95,24 +91,22 @@ describe("console reducer", () => { }; state = reducer(state, action); - expect(state).to.have.property('groupSelection', 0); - expect(state).to.have.property('itemSelection', 0); + expect(state).to.have.property('select', 0); state = reducer(state, action); - expect(state).to.have.property('groupSelection', 0); - expect(state).to.have.property('itemSelection', 1); + expect(state).to.have.property('select', 1); state = reducer(state, action); + expect(state).to.have.property('select', 2); + state = reducer(state, action); - expect(state).to.have.property('groupSelection', -1); - expect(state).to.have.property('itemSelection', -1); + expect(state).to.have.property('select', -1); }); it ('return next state for CONSOLE_COMPLETION_PREV', () => { let action = { type: actions.CONSOLE_COMPLETION_PREV }; let state = { - groupSelection: -1, - itemSelection: -1, + select: -1, completions: [{ name: 'Apple', items: [1, 2] @@ -123,17 +117,15 @@ describe("console reducer", () => { }; state = reducer(state, action); - expect(state).to.have.property('groupSelection', 1); - expect(state).to.have.property('itemSelection', 0); + expect(state).to.have.property('select', 2); state = reducer(state, action); - expect(state).to.have.property('groupSelection', 0); - expect(state).to.have.property('itemSelection', 1); + expect(state).to.have.property('select', 1); state = reducer(state, action); + expect(state).to.have.property('select', 0); + state = reducer(state, action); - expect(state).to.have.property('groupSelection', -1); - expect(state).to.have.property('itemSelection', -1); + expect(state).to.have.property('select', -1); }); - }); From 014963b32700d251bbb6991ce150025f578ea971 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Wed, 13 Feb 2019 22:11:35 +0900 Subject: [PATCH 2/5] Implement completion scroll --- .eslintrc | 1 + src/console/components/console/completion.jsx | 51 +++++++++++++++++-- src/console/reducers/index.js | 1 + 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/.eslintrc b/.eslintrc index cf11670..0f41e10 100644 --- a/.eslintrc +++ b/.eslintrc @@ -40,6 +40,7 @@ "no-alert": "off", "no-bitwise": "off", "no-console": ["error", { "allow": ["warn", "error"] }], + "no-continue": "off", "no-empty-function": "off", "no-magic-numbers": "off", "no-mixed-operators": "off", diff --git a/src/console/components/console/completion.jsx b/src/console/components/console/completion.jsx index 5f128d7..096653b 100644 --- a/src/console/components/console/completion.jsx +++ b/src/console/components/console/completion.jsx @@ -1,6 +1,8 @@ import { Component, h } from 'preact'; import { connect } from 'preact-redux'; +const COMPLETION_MAX_ITEMS = 33; + const CompletionTitle = (props) => { return
  • {props.title}
  • ; }; @@ -25,23 +27,64 @@ const CompletionItem = (props) => { class CompletionComponent extends Component { + constructor() { + super(); + this.state = { viewOffset: 0, select: -1 }; + } + + static getDerivedStateFromProps(nextProps, prevState) { + if (prevState.select === nextProps.select) { + return null; + } + + let viewSelect = (() => { + let view = 0; + let index = 0; + for (let group of nextProps.completions) { + ++view; + // TODO refactor + for (let _ of group.items) { + if (index === nextProps.select) { + return view; + } + ++view; + ++index; + } + } + })(); + + let viewOffset = 0; + if (nextProps.select < 0) { + viewOffset = 0; + } else if (prevState.select < nextProps.select) { + viewOffset = Math.max(prevState.viewOffset, + viewSelect - COMPLETION_MAX_ITEMS + 1); + } else if (prevState.select > nextProps.select) { + viewOffset = Math.min(prevState.viewOffset, viewSelect); + } + return { viewOffset, select: nextProps.select }; + } + render() { let eles = []; let index = 0; - for (let i = 0; i < this.props.completions.length; ++i) { - let group = this.props.completions[i]; + + for (let group of this.props.completions) { eles.push(); - for (let j = 0; j < group.items.length; ++j, ++index) { - let item = group.items[j]; + for (let item of group.items) { eles.push(); + ++index; } } + let viewOffset = this.state.viewOffset; + eles = eles.slice(viewOffset, viewOffset + COMPLETION_MAX_ITEMS); + return (
      { eles } diff --git a/src/console/reducers/index.js b/src/console/reducers/index.js index bcc7a2e..614a72f 100644 --- a/src/console/reducers/index.js +++ b/src/console/reducers/index.js @@ -7,6 +7,7 @@ const defaultState = { completionSource: '', completions: [], select: -1, + viewIndex: 0, }; const nextSelection = (state) => { From f43a2d2a9eda2a56709dca59bd003beec889d557 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Thu, 14 Feb 2019 20:55:55 +0900 Subject: [PATCH 3/5] Clean completion component --- src/console/components/console.jsx | 4 +++- src/console/components/console/completion.jsx | 22 +++++++------------ 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/console/components/console.jsx b/src/console/components/console.jsx index 23c93e3..f8213db 100644 --- a/src/console/components/console.jsx +++ b/src/console/components/console.jsx @@ -6,6 +6,8 @@ import Completion from './console/completion'; import Message from './console/message'; import * as consoleActions from '../../console/actions/console'; +const COMPLETION_MAX_ITEMS = 33; + class ConsoleComponent extends Component { onBlur() { if (this.props.mode === 'command' || this.props.mode === 'find') { @@ -105,7 +107,7 @@ class ConsoleComponent extends Component { case 'command': case 'find': return
      - + { this.input = c; }} mode={this.props.mode} diff --git a/src/console/components/console/completion.jsx b/src/console/components/console/completion.jsx index 096653b..ae081e6 100644 --- a/src/console/components/console/completion.jsx +++ b/src/console/components/console/completion.jsx @@ -1,8 +1,6 @@ import { Component, h } from 'preact'; import { connect } from 'preact-redux'; -const COMPLETION_MAX_ITEMS = 33; - const CompletionTitle = (props) => { return
    • {props.title}
    • ; }; @@ -38,18 +36,14 @@ class CompletionComponent extends Component { } let viewSelect = (() => { - let view = 0; let index = 0; - for (let group of nextProps.completions) { - ++view; - // TODO refactor - for (let _ of group.items) { - if (index === nextProps.select) { - return view; - } - ++view; - ++index; + for (let i = 0; i < nextProps.completions.length; ++i) { + ++index; + let g = nextProps.completions[i]; + if (nextProps.select + i + 1 < index + g.items.length) { + return nextProps.select + i + 1; } + index += g.items.length; } })(); @@ -58,7 +52,7 @@ class CompletionComponent extends Component { viewOffset = 0; } else if (prevState.select < nextProps.select) { viewOffset = Math.max(prevState.viewOffset, - viewSelect - COMPLETION_MAX_ITEMS + 1); + viewSelect - nextProps.size + 1); } else if (prevState.select > nextProps.select) { viewOffset = Math.min(prevState.viewOffset, viewSelect); } @@ -83,7 +77,7 @@ class CompletionComponent extends Component { } let viewOffset = this.state.viewOffset; - eles = eles.slice(viewOffset, viewOffset + COMPLETION_MAX_ITEMS); + eles = eles.slice(viewOffset, viewOffset + this.props.size); return (
        From bc327e87a816beb6834a1d79c95c94ba34651ab3 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Fri, 15 Feb 2019 20:37:02 +0900 Subject: [PATCH 4/5] Fix Completion interface and add test --- src/console/components/console.jsx | 6 +- src/console/components/console/completion.jsx | 3 +- .../components/console/completion.test.jsx | 138 ++++++++++++++++++ 3 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 test/console/components/console/completion.test.jsx diff --git a/src/console/components/console.jsx b/src/console/components/console.jsx index f8213db..7994f78 100644 --- a/src/console/components/console.jsx +++ b/src/console/components/console.jsx @@ -107,7 +107,11 @@ class ConsoleComponent extends Component { case 'command': case 'find': return
        - + { this.input = c; }} mode={this.props.mode} diff --git a/src/console/components/console/completion.jsx b/src/console/components/console/completion.jsx index ae081e6..be6b93f 100644 --- a/src/console/components/console/completion.jsx +++ b/src/console/components/console/completion.jsx @@ -87,5 +87,4 @@ class CompletionComponent extends Component { } } -const mapStateToProps = state => state; -export default connect(mapStateToProps)(CompletionComponent); +export default CompletionComponent; diff --git a/test/console/components/console/completion.test.jsx b/test/console/components/console/completion.test.jsx new file mode 100644 index 0000000..0b48fe2 --- /dev/null +++ b/test/console/components/console/completion.test.jsx @@ -0,0 +1,138 @@ +import { h, render } from 'preact'; +import Completion from 'console/components/console/completion' + +describe("console/components/console/completion", () => { + let completions = [{ + name: "Fruit", + items: [{ caption: "apple" }, { caption: "banana" }, { caption: "cherry" }], + }, { + name: "Element", + items: [{ caption: "argon" }, { caption: "boron" }, { caption: "carbon" }], + }]; + + beforeEach(() => { + document.body.innerHTML = ''; + }); + + it('renders Completion component', () => { + let ul = render(, document.body); + + expect(ul.children).to.have.lengthOf(8); + expect(ul.children[0].textContent).to.equal('Fruit'); + expect(ul.children[1].textContent).to.equal('apple'); + expect(ul.children[2].textContent).to.equal('banana'); + expect(ul.children[3].textContent).to.equal('cherry'); + expect(ul.children[4].textContent).to.equal('Element'); + expect(ul.children[5].textContent).to.equal('argon'); + expect(ul.children[6].textContent).to.equal('boron'); + expect(ul.children[7].textContent).to.equal('carbon'); + }); + + it('highlight current item', () => { + let ul = render(, document.body); + expect(ul.children[5].className.split(' ')).to.include('vimvixen-completion-selected'); + }); + + it('does not highlight any items', () => { + let ul = render(, document.body); + for (let li of ul.children) { + expect(li.className.split(' ')).not.to.include('vimvixen-completion-selected'); + } + }); + + + it('limits completion items', () => { + let ul = render(, document.body); + expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['Fruit', 'apple', 'banana']); + + ul = render(, document.body, ul); + + expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['Fruit', 'apple', 'banana']); + expect(ul.children[1].className.split(' ')).to.include('vimvixen-completion-selected'); + }) + + it('scrolls up to down with select', () => { + let ul = render(, document.body); + + expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['Fruit', 'apple', 'banana']); + expect(ul.children[2].className.split(' ')).to.include('vimvixen-completion-selected'); + + ul = render(, document.body, ul); + + expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['apple', 'banana', 'cherry']); + expect(ul.children[2].className.split(' ')).to.include('vimvixen-completion-selected'); + + ul = render(, document.body, ul); + + expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['cherry', 'Element', 'argon']); + expect(ul.children[2].className.split(' ')).to.include('vimvixen-completion-selected'); + }); + + it('scrolls up to down with select', () => { + let ul = render(, document.body); + + expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['argon', 'boron', 'carbon']); + expect(ul.children[2].className.split(' ')).to.include('vimvixen-completion-selected'); + + ul = render(, document.body, ul); + + expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['argon', 'boron', 'carbon']); + expect(ul.children[1].className.split(' ')).to.include('vimvixen-completion-selected'); + + ul = render(, document.body, ul); + + expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['argon', 'boron', 'carbon']); + expect(ul.children[0].className.split(' ')).to.include('vimvixen-completion-selected'); + + ul = render(, document.body, ul); + + expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['cherry', 'Element', 'argon']); + expect(ul.children[0].className.split(' ')).to.include('vimvixen-completion-selected'); + }); +}); From 9868da1ac479ee75a4abe8142a9b406a18b5ac52 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Fri, 15 Feb 2019 20:42:40 +0900 Subject: [PATCH 5/5] Remove unused import --- src/console/components/console/completion.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/console/components/console/completion.jsx b/src/console/components/console/completion.jsx index be6b93f..d836cec 100644 --- a/src/console/components/console/completion.jsx +++ b/src/console/components/console/completion.jsx @@ -1,5 +1,4 @@ import { Component, h } from 'preact'; -import { connect } from 'preact-redux'; const CompletionTitle = (props) => { return
      • {props.title}
      • ;