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
parent
0efda40a47
commit
815597c069
|
@ -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 = (
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue