From 7187f64d4c511b9f597e36bfb93d69c838ad3c86 Mon Sep 17 00:00:00 2001 From: Christopher Henn Date: Tue, 5 Jun 2018 14:23:20 -0700 Subject: [PATCH] Use server-provided completion suggestions Co-authored-by: Chris Henn Co-authored-by: Andrew Watkins --- ui/src/flux/components/TimeMachine.tsx | 1 + ui/src/flux/components/TimeMachineEditor.tsx | 35 ++++++++++---- ui/src/flux/helpers/autoComplete.ts | 49 ++++++++++++++++++++ ui/src/types/codemirror.ts | 35 ++++++++++++++ 4 files changed, 110 insertions(+), 10 deletions(-) create mode 100644 ui/src/flux/helpers/autoComplete.ts create mode 100644 ui/src/types/codemirror.ts diff --git a/ui/src/flux/components/TimeMachine.tsx b/ui/src/flux/components/TimeMachine.tsx index 26a51fb58..d08145593 100644 --- a/ui/src/flux/components/TimeMachine.tsx +++ b/ui/src/flux/components/TimeMachine.tsx @@ -109,6 +109,7 @@ class TimeMachine extends PureComponent { status={status} script={script} visibility={visibility} + suggestions={suggestions} onChangeScript={onChangeScript} onSubmitScript={onSubmitScript} /> diff --git a/ui/src/flux/components/TimeMachineEditor.tsx b/ui/src/flux/components/TimeMachineEditor.tsx index 9506a067d..db5690a96 100644 --- a/ui/src/flux/components/TimeMachineEditor.tsx +++ b/ui/src/flux/components/TimeMachineEditor.tsx @@ -1,9 +1,10 @@ import React, {PureComponent} from 'react' -import {Controlled as CodeMirror, IInstance} from 'react-codemirror2' +import {Controlled as ReactCodeMirror, IInstance} from 'react-codemirror2' import {EditorChange} from 'codemirror' -import 'src/external/codemirror' +import {ShowHintOptions} from 'src/types/codemirror' import {ErrorHandling} from 'src/shared/decorators/errors' -import {OnChangeScript, OnSubmitScript} from 'src/types/flux' +import {OnChangeScript, OnSubmitScript, Suggestion} from 'src/types/flux' +import {getFluxCompletions} from 'src/flux/helpers/autoComplete' interface Gutter { line: number @@ -21,10 +22,11 @@ interface Props { status: Status onChangeScript: OnChangeScript onSubmitScript: OnSubmitScript + suggestions: Suggestion[] } interface EditorInstance extends IInstance { - showHint: (options?: any) => void + showHint: (options?: ShowHintOptions) => void } @ErrorHandling @@ -36,19 +38,21 @@ class TimeMachineEditor extends PureComponent { } public componentDidUpdate(prevProps) { - if (this.props.status.type === 'error') { + const {status, visibility} = this.props + + if (status.type === 'error') { this.makeError() } - if (this.props.status.type !== 'error') { + if (status.type !== 'error') { this.editor.clearGutter('error-gutter') } - if (prevProps.visibility === this.props.visibility) { + if (prevProps.visibility === visibility) { return } - if (this.props.visibility === 'visible') { + if (visibility === 'visible') { setTimeout(() => this.editor.refresh(), 60) } } @@ -61,7 +65,6 @@ class TimeMachineEditor extends PureComponent { theme: 'time-machine', tabIndex: 1, readonly: false, - extraKeys: {'Ctrl-Space': 'autocomplete'}, completeSingle: false, autoRefresh: true, mode: 'flux', @@ -70,7 +73,7 @@ class TimeMachineEditor extends PureComponent { return (
- { onTouchStart={this.onTouchStart} editorDidMount={this.handleMount} onBlur={this.handleBlur} + onKeyUp={this.handleKeyUp} />
) @@ -128,6 +132,17 @@ class TimeMachineEditor extends PureComponent { private onTouchStart = () => {} + private handleKeyUp = (editor: EditorInstance, e: KeyboardEvent) => { + const {suggestions} = this.props + const space = ' ' + + if (e.ctrlKey && e.key === space) { + editor.showHint({ + hint: () => getFluxCompletions(this.editor, suggestions), + }) + } + } + private updateCode = ( _: IInstance, __: EditorChange, diff --git a/ui/src/flux/helpers/autoComplete.ts b/ui/src/flux/helpers/autoComplete.ts new file mode 100644 index 000000000..72223a96b --- /dev/null +++ b/ui/src/flux/helpers/autoComplete.ts @@ -0,0 +1,49 @@ +import CodeMirror from 'codemirror' +import {IInstance} from 'react-codemirror2' + +import {Suggestion} from 'src/types/flux' +import {Hints} from 'src/types/codemirror' + +export const getFluxCompletions = ( + editor: IInstance, + list: Suggestion[] +): Hints => { + const cursor = editor.getCursor() + const currentLine = editor.getLine(cursor.line) + const trailingWhitespace = /[\w$]+/ + + let start = cursor.ch + 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 += 1 + } + + // Move start marker until a space or the beginning of line is reached + while (start && trailingWhitespace.test(currentLine.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), + } + } + + const currentWord = currentLine.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), + } +} diff --git a/ui/src/types/codemirror.ts b/ui/src/types/codemirror.ts new file mode 100644 index 000000000..4f56e7742 --- /dev/null +++ b/ui/src/types/codemirror.ts @@ -0,0 +1,35 @@ +import {Editor, Position} from 'codemirror' + +export interface Hints { + from: Position + to: Position + list: Array +} + +// Interface used by showHint.js Codemirror add-on +// When completions aren't simple strings, they should be objects with the following properties: + +export interface Hint { + text: string + className?: string + displayText?: string + from?: Position + /** Called if a completion is picked. If provided *you* are responsible for applying the completion */ + hint?: (cm: Editor, data: Hints, cur: Hint) => void + render?: (element: HTMLLIElement, data: Hints, cur: Hint) => void + to?: Position +} + +type HintFunction = (cm: Editor) => Hints + +interface AsyncHintFunction { + (cm: Editor, callback: (hints: Hints) => any): any + async?: boolean +} + +export interface ShowHintOptions { + completeSingle?: boolean + hint: HintFunction | AsyncHintFunction +} + +export type ShowHint = (cm: Editor, options: ShowHintOptions) => void