diff --git a/src/components/SqlEditor.vue b/src/components/SqlEditor.vue index 8c826a0..22830c0 100644 --- a/src/components/SqlEditor.vue +++ b/src/components/SqlEditor.vue @@ -1,39 +1,22 @@ diff --git a/src/hint.js b/src/hint.js new file mode 100644 index 0000000..c584bb8 --- /dev/null +++ b/src/hint.js @@ -0,0 +1,45 @@ +import CM from 'codemirror' +import 'codemirror/addon/hint/show-hint.js' +import 'codemirror/addon/hint/sql-hint.js' +import store from '@/store' +import { debounce } from 'debounce' + +export function getHints (cm, options) { + const token = cm.getTokenAt(cm.getCursor()).string.toUpperCase() + const result = CM.hint.sql(cm, options) + // Don't show the hint if there is only one option + // and the token is already completed with this option + if (result.list.length === 1 && result.list[0].text.toUpperCase() === token) { + result.list = [] + } + return result +} + +const hintOptions = { + get tables () { + const tables = {} + if (store.state.schema) { + store.state.schema.forEach(table => { + tables[table.name] = table.columns.map(column => column.name) + }) + } + return tables + }, + completeSingle: false, + completeOnSingleClick: true, + alignWithWord: false +} + +export default { + show: debounce(function (editor) { + // Don't show autocomplete after a space or semicolon or in string literals + const token = editor.getTokenAt(editor.getCursor()) + const ch = token.string.slice(-1) + const tokenType = token.type + if (tokenType === 'string' || !ch || ch === ' ' || ch === ';') { + return + } + + CM.showHint(editor, getHints, hintOptions) + }, 400) +} diff --git a/tests/unit/components/SqlEditor.spec.js b/tests/unit/components/SqlEditor.spec.js new file mode 100644 index 0000000..43ea490 --- /dev/null +++ b/tests/unit/components/SqlEditor.spec.js @@ -0,0 +1,11 @@ +import { expect } from 'chai' +import { mount } from '@vue/test-utils' +import SqlEditor from '@/components/SqlEditor.vue' + +describe('SqlEditor.vue', () => { + it('Emits input event when a query is changed', async() => { + const wrapper = mount(SqlEditor) + await wrapper.findComponent({ name: 'codemirror'}).vm.$emit('input', 'SELECT * FROM foo') + expect(wrapper.emitted('input')[0]).to.eql(['SELECT * FROM foo']) + }) +}) diff --git a/tests/unit/hint.spec.js b/tests/unit/hint.spec.js new file mode 100644 index 0000000..5525aee --- /dev/null +++ b/tests/unit/hint.spec.js @@ -0,0 +1,205 @@ +import { expect } from 'chai' +import sinon from 'sinon' +import { state } from '@/store' +import { default as hint, getHints } from '@/hint' +import CM from 'codemirror' + +describe('hint.js', () => { + it('Calculates table list for hint', () => { + // mock store state + const schema = [ + { + name: 'foo', + columns: [ + { name: 'fooId', type: 'INTEGER' }, + { name: 'name', type: 'NVARCHAR(20)' } + ] + }, + { + name: 'bar', + columns: [ + { name: 'barId', type: 'INTEGER' } + ] + } + ] + sinon.stub(state, 'schema').value(schema) + + // mock showHint and editor + sinon.stub(CM, 'showHint') + const editor = { + getTokenAt() { + return { + string: 'SELECT', + type: 'keyword' + } + }, + getCursor: sinon.stub() + } + + const clock = sinon.useFakeTimers(); + hint.show(editor) + clock.tick(500) + + expect(CM.showHint.called).to.equal(true) + expect(CM.showHint.firstCall.args[2].tables).to.eql({ + foo: ['fooId', 'name'], + bar: ['barId'] + }) + + sinon.restore() + }) + + it("Doesn't show hint when in string or space, or ';'", () => { + // mock showHint and editor + sinon.stub(CM, 'showHint') + const editor = { + getTokenAt() { + return { + string: 'foo', + type: 'string' + } + }, + getCursor: sinon.stub() + } + + const clock = sinon.useFakeTimers() + hint.show(editor) + clock.tick(500) + + expect(CM.showHint.called).to.equal(false) + + sinon.restore() + }) + + it("Doesn't show hint after space", () => { + // mock showHint and editor + sinon.stub(CM, 'showHint') + const editor = { + getTokenAt() { + return { + string: ' ', + type: null + } + }, + getCursor: sinon.stub() + } + + const clock = sinon.useFakeTimers() + hint.show(editor) + clock.tick(500) + + expect(CM.showHint.called).to.equal(false) + + sinon.restore() + }) + + it("Doesn't show hint after ';'", () => { + // mock showHint and editor + sinon.stub(CM, 'showHint') + const editor = { + getTokenAt() { + return { + string: ';', + type: 'punctuation' + } + }, + getCursor: sinon.stub() + } + + const clock = sinon.useFakeTimers() + hint.show(editor) + clock.tick(500) + + expect(CM.showHint.called).to.equal(false) + + sinon.restore() + }) + + it("getHints returns [ ] if there is only one option and the token is already completed with this option", () => { + // mock CM.hint.sql and editor + sinon.stub(CM.hint, 'sql').returns({ list: [{ text: 'SELECT' }] }) + const editor = { + getTokenAt() { + return { + string: 'select', + type: 'keyword' + } + }, + getCursor: sinon.stub() + } + + const hints = getHints(editor, {}) + expect(hints.list).to.eql([]) + + sinon.restore() + }) + + it("getHints returns hints as is when there are more than one option", () => { + // mock CM.hint.sql and editor + let list = [ + { text: 'SELECT' }, + { text: 'ST' } + ] + sinon.stub(CM.hint, 'sql').returns({ list }) + const editor = { + getTokenAt() { + return { + string: 'se', + type: 'keyword' + } + }, + getCursor: sinon.stub() + } + + const hints = getHints(editor, {}) + expect(hints.list).to.eql(list) + + sinon.restore() + }) + + it("getHints returns hints as is when there only one option but the token is not cpmpleted", () => { + // mock CM.hint.sql and editor + let list = [{ text: 'SELECT' }] + sinon.stub(CM.hint, 'sql').returns({ list }) + const editor = { + getTokenAt() { + return { + string: 'sele', + type: 'keyword' + } + }, + getCursor: sinon.stub() + } + + const hints = getHints(editor, {}) + expect(hints.list).to.eql(list) + + sinon.restore() + }) + + it('tables is empty object when schema is null', () => { + // mock store state + sinon.stub(state, 'schema').value(null) + + // mock showHint and editor + sinon.stub(CM, 'showHint') + const editor = { + getTokenAt() { + return { + string: 'SELECT', + type: 'keyword' + } + }, + getCursor: sinon.stub() + } + + const clock = sinon.useFakeTimers(); + hint.show(editor) + clock.tick(500) + + expect(CM.showHint.called).to.equal(true) + expect(CM.showHint.firstCall.args[2].tables).to.eql({}) + + sinon.restore() + }) +})