diff --git a/components/smart/CodeMirror.vue b/components/smart/CodeMirror.vue index 58d8997fd..abe3845de 100644 --- a/components/smart/CodeMirror.vue +++ b/components/smart/CodeMirror.vue @@ -8,6 +8,7 @@ import "codemirror/mode/javascript/javascript" import { ref, watch } from "@nuxtjs/composition-api" import { useCodemirror } from "~/helpers/editor/codemirror" import { LinterDefinition } from "~/helpers/editor/linting/linter" +import { Completer } from "~/helpers/editor/completion" const props = withDefaults( defineProps<{ @@ -16,11 +17,13 @@ const props = withDefaults( placeholder?: string wrap?: boolean linter: LinterDefinition | null + completer: Completer | null }>(), { placeholder: "", wrap: true, linter: null as any, + completer: null as any, } ) @@ -45,5 +48,6 @@ useCodemirror(editor, value, { lineWrapping: props.wrap, }, linter: props.linter, + completer: props.completer, }) diff --git a/helpers/editor/codemirror.ts b/helpers/editor/codemirror.ts index 6681f792a..852d46598 100644 --- a/helpers/editor/codemirror.ts +++ b/helpers/editor/codemirror.ts @@ -7,6 +7,7 @@ import "codemirror/theme/3024-night.css" import "codemirror/lib/codemirror.css" import "codemirror/addon/lint/lint.css" import "codemirror/addon/dialog/dialog.css" +import "codemirror/addon/hint/show-hint.css" import "codemirror/addon/fold/foldgutter.css" import "codemirror/addon/fold/foldgutter" @@ -15,6 +16,7 @@ import "codemirror/addon/fold/comment-fold" import "codemirror/addon/fold/indent-fold" import "codemirror/addon/display/autorefresh" import "codemirror/addon/lint/lint" +import "codemirror/addon/hint/show-hint" import "codemirror/addon/display/placeholder" import "codemirror/addon/edit/closebrackets" import "codemirror/addon/search/search" @@ -24,10 +26,12 @@ import "codemirror/addon/dialog/dialog" import { watch, onMounted, ref, Ref, useContext } from "@nuxtjs/composition-api" import { LinterDefinition } from "./linting/linter" +import { Completer } from "./completion" type CodeMirrorOptions = { extendedEditorConfig: Omit linter: LinterDefinition | null + completer: Completer | null } const DEFAULT_EDITOR_CONFIG: CodeMirror.EditorConfiguration = { @@ -76,6 +80,31 @@ export function useCodemirror( } } + const updateCompleterConfig = () => { + if (options.completer) { + cm.value?.setOption("hintOptions", { + completeSingle: false, + hint: async (editor: CodeMirror.Editor) => { + const pos = editor.getCursor() + const text = editor.getValue() + + const result = await options.completer!(text, pos) + + console.log("complete!") + console.log(result) + + return { + from: result.start, + to: result.end, + list: result.completions + .sort((a, b) => a.score - b.score) + .map((x) => x.text), + } + }, + }) + } + } + // Boot-up CodeMirror, set the value and listeners onMounted(() => { cm.value = CodeMirror(el.value!, DEFAULT_EDITOR_CONFIG) @@ -83,6 +112,7 @@ export function useCodemirror( setTheme() updateEditorConfig() updateLinterConfig() + updateCompleterConfig() cm.value.on("change", (instance) => { // External update propagation (via watchers) should be ignored @@ -90,6 +120,13 @@ export function useCodemirror( value.value = instance.getValue() } }) + + /* TODO: Show autocomplete on typing (this is just for testing) */ + cm.value.on("keyup", (instance, event) => { + if (!instance.state.completionActive && event.key !== "Enter") { + instance.showHint() + } + }) }) const setTheme = () => { @@ -120,6 +157,7 @@ export function useCodemirror( deep: true, }) watch(() => options.linter, updateLinterConfig, { immediate: true }) + watch(() => options.completer, updateCompleterConfig, { immediate: true }) // Watch value updates watch(value, (newVal) => { diff --git a/helpers/editor/completion/index.ts b/helpers/editor/completion/index.ts new file mode 100644 index 000000000..f5f927b06 --- /dev/null +++ b/helpers/editor/completion/index.ts @@ -0,0 +1,33 @@ +export type CompletionEntry = { + text: string + meta: string + score: number +} + +export type CompleterResult = { + /** + * List of completions to display + */ + completions: CompletionEntry[] + /** + * Start of the completion position + * (on completion the start..end region is replaced) + */ + start: { line: number; ch: number } + /** + * End of the completion position + * (on completion the start..end region is replaced) + */ + end: { line: number; ch: number } +} + +export type Completer = ( + /** + * The contents of the editor + */ + text: string, + /** + * Position where the completer is fired + */ + completePos: { line: number; ch: number } +) => Promise diff --git a/helpers/editor/completion/preRequest.ts b/helpers/editor/completion/preRequest.ts new file mode 100644 index 000000000..1a3387cff --- /dev/null +++ b/helpers/editor/completion/preRequest.ts @@ -0,0 +1,30 @@ +import { convertIndexToLineCh } from "../utils" +import { Completer, CompletionEntry } from "." +import { getPreRequestScriptCompletions } from "~/helpers/tern" + +const completer: Completer = async (text, completePos) => { + const results = await getPreRequestScriptCompletions( + text, + completePos.line, + completePos.ch + ) + + const start = convertIndexToLineCh(text, results.start) + const end = convertIndexToLineCh(text, results.end) + + const completions = results.completions.map((completion: any, i: number) => { + return { + text: completion.name, + meta: completion.isKeyword ? "keyword" : completion.type, + score: results.completions.length - i, + } + }) + + return { + start, + end, + completions, + } +} + +export default completer