diff --git a/public/app/plugins/datasource/prometheus/components/monaco-query-field/MonacoQueryField.tsx b/public/app/plugins/datasource/prometheus/components/monaco-query-field/MonacoQueryField.tsx index a16e5393283..03db833c858 100644 --- a/public/app/plugins/datasource/prometheus/components/monaco-query-field/MonacoQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/monaco-query-field/MonacoQueryField.tsx @@ -1,5 +1,5 @@ -import React, { useRef } from 'react'; -import { CodeEditor, CodeEditorMonacoOptions } from '@grafana/ui'; +import React, { useRef, useEffect } from 'react'; +import { CodeEditor, CodeEditorMonacoOptions, Monaco, monacoTypes } from '@grafana/ui'; import { useLatest } from 'react-use'; import { promLanguageDefinition } from 'monaco-promql'; import { getCompletionProvider } from './monaco-completion-provider'; @@ -22,6 +22,24 @@ const options: CodeEditorMonacoOptions = { fixedOverflowWidgets: true, }; +const PROMQL_LANG_ID = promLanguageDefinition.id; + +// we must only run the promql-setup code once +let PROMQL_SETUP_STARTED = false; + +function ensurePromQL(monaco: Monaco) { + if (PROMQL_SETUP_STARTED === false) { + PROMQL_SETUP_STARTED = true; + const { aliases, extensions, mimetypes, loader } = promLanguageDefinition; + monaco.languages.register({ id: PROMQL_LANG_ID, aliases, extensions, mimetypes }); + + loader().then((mod) => { + monaco.languages.setMonarchTokensProvider(PROMQL_LANG_ID, mod.language); + monaco.languages.setLanguageConfiguration(PROMQL_LANG_ID, mod.languageConfiguration); + }); + } +} + const MonacoQueryField = (props: Props) => { const containerRef = useRef(null); const { languageProvider, history, onChange, initialValue } = props; @@ -29,6 +47,15 @@ const MonacoQueryField = (props: Props) => { const lpRef = useLatest(languageProvider); const historyRef = useLatest(history); + const autocompleteDisposeFun = useRef<(() => void) | null>(null); + + useEffect(() => { + // when we unmount, we unregister the autocomplete-function, if it was registered + return () => { + autocompleteDisposeFun.current?.(); + }; + }, []); + return (
{ monacoOptions={options} language="promql" value={initialValue} - onBeforeEditorMount={(monaco) => { + onBeforeEditorMount={ensurePromQL} + onEditorDidMount={(editor, monaco) => { // we construct a DataProvider object const getSeries = (selector: string) => lpRef.current.getSeries(selector); @@ -69,19 +97,34 @@ const MonacoQueryField = (props: Props) => { }; const dataProvider = { getSeries, getHistory, getAllMetricNames }; + const completionProvider = getCompletionProvider(monaco, dataProvider); - const langId = promLanguageDefinition.id; - monaco.languages.register(promLanguageDefinition); - promLanguageDefinition.loader().then((mod) => { - monaco.languages.setMonarchTokensProvider(langId, mod.language); - monaco.languages.setLanguageConfiguration(langId, mod.languageConfiguration); - const completionProvider = getCompletionProvider(monaco, dataProvider); - monaco.languages.registerCompletionItemProvider(langId, completionProvider); - }); + // completion-providers in monaco are not registered directly to editor-instances, + // they are registerd to languages. this makes it hard for us to have + // separate completion-providers for every query-field-instance + // (but we need that, because they might connect to different datasources). + // the trick we do is, we wrap the callback in a "proxy", + // and in the proxy, the first thing is, we check if we are called from + // "our editor instance", and if not, we just return nothing. if yes, + // we call the completion-provider. + const filteringCompletionProvider: monacoTypes.languages.CompletionItemProvider = { + ...completionProvider, + provideCompletionItems: (model, position, context, token) => { + // if the model-id does not match, then this call is from a different editor-instance, + // not "our instance", so return nothing + if (editor.getModel()?.id !== model.id) { + return { suggestions: [] }; + } + return completionProvider.provideCompletionItems(model, position, context, token); + }, + }; - // FIXME: should we unregister this at end end? - }} - onEditorDidMount={(editor, monaco) => { + const { dispose } = monaco.languages.registerCompletionItemProvider( + PROMQL_LANG_ID, + filteringCompletionProvider + ); + + autocompleteDisposeFun.current = dispose; // this code makes the editor resize itself so that the content fits // (it will grow taller when necessary) // FIXME: maybe move this functionality into CodeEditor, like: