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()
+ })
+})