Improve function specific autocomplete

Co-authored-by: Chris Henn <chris.henn@influxdata.com>
Co-authored-by: Andrew Watkins <andrew.watkinz@gmail.com>
pull/3583/head
Christopher Henn 2018-06-05 17:12:44 -07:00
parent 0efda40a47
commit 815597c069
No known key found for this signature in database
GPG Key ID: 909E48D5E1C526FA
3 changed files with 303 additions and 28 deletions

View File

@ -4,7 +4,8 @@ import {EditorChange} from 'codemirror'
import {ShowHintOptions} from 'src/types/codemirror'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {OnChangeScript, OnSubmitScript, Suggestion} from 'src/types/flux'
import {getFluxCompletions} from 'src/flux/helpers/autoComplete'
import {EXCLUDED_KEYS} from 'src/flux/constants/editor'
import {getSuggestions} from 'src/flux/helpers/autoComplete'
import 'src/external/codemirror'
interface Gutter {
@ -133,15 +134,29 @@ class TimeMachineEditor extends PureComponent<Props> {
private onTouchStart = () => {}
private handleKeyUp = (editor: EditorInstance, e: KeyboardEvent) => {
const {suggestions} = this.props
const space = ' '
private handleKeyUp = (__, e: KeyboardEvent) => {
const {ctrlKey, metaKey, key} = e
if (e.ctrlKey && e.key === space) {
editor.showHint({
hint: () => getFluxCompletions(this.editor, suggestions),
})
if (ctrlKey && key === ' ') {
this.showAutoComplete()
return
}
if (ctrlKey || metaKey || EXCLUDED_KEYS.includes(key)) {
return
}
this.showAutoComplete()
}
private showAutoComplete() {
const {suggestions} = this.props
this.editor.showHint({
hint: () => getSuggestions(this.editor, suggestions),
completeSingle: false,
})
}
private updateCode = (

View File

@ -1,49 +1,200 @@
import CodeMirror from 'codemirror'
import {IInstance} from 'react-codemirror2'
import {Suggestion} from 'src/types/flux'
import {Hints} from 'src/types/codemirror'
export const getFluxCompletions = (
export const getSuggestions = (
editor: IInstance,
list: Suggestion[]
allSuggestions: Suggestion[]
): Hints => {
const cursor = editor.getCursor()
const currentLine = editor.getLine(cursor.line)
const trailingWhitespace = /[\w$]+/
const currentLineNumber = cursor.line
const currentLineText = editor.getLine(cursor.line)
const cursorPosition = cursor.ch
let start = cursor.ch
const {start, end, suggestions} = getSuggestionsHelper(
currentLineText,
cursorPosition,
allSuggestions
)
return {
from: {line: currentLineNumber, ch: start},
to: {line: currentLineNumber, ch: end},
list: suggestions,
}
}
export const getSuggestionsHelper = (
currentLineText: string,
cursorPosition: number,
allSuggestions: Suggestion[]
) => {
if (shouldCompleteParam(currentLineText, cursorPosition)) {
return getParamSuggestions(currentLineText, cursorPosition, allSuggestions)
}
if (shouldCompleteFunction(currentLineText, cursorPosition)) {
return getFunctionSuggestions(
currentLineText,
cursorPosition,
allSuggestions
)
}
return {
start: -1,
end: -1,
suggestions: [],
}
}
const shouldCompleteFunction = (currentLineText, cursorPosition) => {
const startOfFunc = '('
const endOfFunc = ')'
const endOfParamKey = ':'
const endOfParam = ','
const pipe = '|>'
let i = cursorPosition
// First travel left; the first special characters we should see are from a pipe
while (i) {
const char = currentLineText[i]
const charBefore = currentLineText[i - 1]
if (char + charBefore === pipe || char === endOfFunc) {
break
} else if (char === startOfFunc || char === endOfParamKey) {
return false
}
i -= 1
}
i = cursorPosition
// Then travel right; the first special character we should see is an opening paren '('
while (i < currentLineText.length) {
const char = currentLineText[i]
if (char === endOfParamKey || char === endOfFunc || char === endOfParam) {
return false
}
i += 1
}
return true
}
const shouldCompleteParam = (currentLineText, cursorPosition) => {
let i = cursorPosition
while (i) {
const char = currentLineText[i]
const charBefore = currentLineText[i - 1]
if (char === ':' || char === '>' || char === ')') {
return false
}
if (char === '(' || charBefore + char === ', ') {
return true
}
i -= 1
}
return false
}
export const getParamSuggestions = (
currentLineText: string,
cursorPosition: number,
allSuggestions: Suggestion[]
) => {
let end = cursorPosition
while (end && currentLineText[end] !== '(') {
end -= 1
}
let start = end
while (start && /[\w\(]/.test(currentLineText[start])) {
start -= 1
}
const functionName = currentLineText.slice(start, end).trim()
const func = allSuggestions.find(({name}) => name === functionName)
if (!func) {
return {start, end, suggestions: []}
}
let startOfParamKey = cursorPosition
while (!['(', ' '].includes(currentLineText[startOfParamKey - 1])) {
startOfParamKey -= 1
}
return {
start: startOfParamKey,
end: cursorPosition,
suggestions: Object.entries(func.params).map(([paramName, paramType]) => {
let displayText = paramName
// Work around a bug in Flux where types are sometimes returned as "invalid"
if (paramType !== 'invalid') {
displayText = `${paramName} <${paramType}>`
}
return {
text: `${paramName}: `,
displayText,
}
}),
}
}
export const getFunctionSuggestions = (
currentLineText: string,
cursorPosition: number,
allSuggestions: Suggestion[]
) => {
const trailingWhitespace = /[\w]+/
let start = cursorPosition
let end = start
// Move end marker until a space or end of line is reached
while (
end < currentLine.length &&
trailingWhitespace.test(currentLine.charAt(end))
end < currentLineText.length &&
trailingWhitespace.test(currentLineText.charAt(end))
) {
end += 1
}
// Move start marker until a space or the beginning of line is reached
while (start && trailingWhitespace.test(currentLine.charAt(start - 1))) {
while (start && trailingWhitespace.test(currentLineText.charAt(start - 1))) {
start -= 1
}
// If not completing inside a current word, return list of all possible suggestions
if (start === end) {
return {
from: CodeMirror.Pos(cursor.line, start),
to: CodeMirror.Pos(cursor.line, end),
list: list.map(s => s.name),
}
return {start, end, suggestions: allSuggestions.map(s => s.name)}
}
const currentWord = currentLine.slice(start, end)
const currentWord = currentLineText.slice(start, end)
const listFilter = new RegExp(`^${currentWord}`, 'i')
// Otherwise return suggestions that contain the current word as a substring
return {
from: CodeMirror.Pos(cursor.line, start),
to: CodeMirror.Pos(cursor.line, end),
list: list.filter(s => s.name.match(listFilter)).map(s => s.name),
}
const names = allSuggestions.map(s => s.name)
const filtered = names.filter(name => name.match(listFilter))
const suggestions = filtered.map(displayText => ({
text: `${displayText}(`,
displayText,
}))
return {start, end, suggestions}
}

View File

@ -0,0 +1,109 @@
import {getSuggestionsHelper} from 'src/flux/helpers/autoComplete'
const ALL_SUGGESTIONS = [
{name: 'filter', params: {foo: 'function', bux: 'string'}},
{name: 'first', params: {baz: 'invalid'}},
{name: 'baz', params: {bar: 'array'}},
]
describe('Flux.helpers.autoComplete', () => {
describe('function completion', () => {
it('can complete a function when partially typed', () => {
const lineText = ' |> fi'
const cursorPosition = lineText.length
const actual = getSuggestionsHelper(
lineText,
cursorPosition,
ALL_SUGGESTIONS
)
const expected = {
start: 4,
end: 6,
suggestions: [
{displayText: 'filter', text: 'filter('},
{displayText: 'first', text: 'first('},
],
}
expect(actual).toEqual(expected)
})
it('shows all completions when no function is typed', () => {
const lineText = ' |> '
const cursorPosition = lineText.length
const actual = getSuggestionsHelper(
lineText,
cursorPosition,
ALL_SUGGESTIONS
)
const expected = {
start: 4,
end: 4,
suggestions: ['filter', 'first', 'baz'],
}
expect(actual).toEqual(expected)
})
it('shows all completions after a closing a function with no parameters', () => {
const lineText = ' |> filter() '
const cursorPosition = lineText.length
const actual = getSuggestionsHelper(
lineText,
cursorPosition,
ALL_SUGGESTIONS
)
const expected = {
start: 13,
end: 13,
suggestions: ['filter', 'first', 'baz'],
}
expect(actual).toEqual(expected)
})
describe('parameter completion', () => {
it('shows all parameters for a function', () => {
const lineText = ' |> filter('
const cursorPosition = lineText.length
const actual = getSuggestionsHelper(
lineText,
cursorPosition,
ALL_SUGGESTIONS
)
const expected = {
start: 11,
end: 11,
suggestions: [
{displayText: 'foo <function>', text: 'foo: '},
{displayText: 'bux <string>', text: 'bux: '},
],
}
expect(actual).toEqual(expected)
})
it('shows parameters after the second position', () => {
const lineText = ' |> filter(foo: "bar", '
const cursorPosition = lineText.length
const actual = getSuggestionsHelper(
lineText,
cursorPosition,
ALL_SUGGESTIONS
)
const expected = {
start: 23,
end: 23,
suggestions: [
{displayText: 'foo <function>', text: 'foo: '},
{displayText: 'bux <string>', text: 'bux: '},
],
}
expect(actual).toEqual(expected)
})
})
})
})