11import { css } from '@emotion/css' ;
2- import React , { useRef } from 'react' ;
2+ import React , { useEffect , useRef } from 'react' ;
33import { useLatest } from 'react-use' ;
44
55import { GrafanaTheme2 } from '@grafana/data' ;
66import { selectors } from '@grafana/e2e-selectors' ;
7- import { useTheme2 , ReactMonacoEditor , monacoTypes } from '@grafana/ui' ;
7+ import { useTheme2 , ReactMonacoEditor , monacoTypes , Monaco } from '@grafana/ui' ;
8+
9+ import { languageConfiguration , monarchlanguage } from '../../language' ;
810
911import { Props } from './MonacoQueryFieldProps' ;
12+ import { CompletionDataProvider } from './completion/CompletionDataProvider' ;
13+ import { getCompletionProvider , getSuggestOptions } from './completion/completionUtils' ;
1014
1115const options : monacoTypes . editor . IStandaloneEditorConstructionOptions = {
1216 codeLens : false ,
@@ -35,6 +39,7 @@ const options: monacoTypes.editor.IStandaloneEditorConstructionOptions = {
3539 horizontalScrollbarSize : 0 ,
3640 } ,
3741 scrollBeyondLastLine : false ,
42+ suggest : getSuggestOptions ( ) ,
3843 suggestFontSize : 12 ,
3944 wordWrap : 'on' ,
4045} ;
@@ -48,6 +53,25 @@ const options: monacoTypes.editor.IStandaloneEditorConstructionOptions = {
4853// up & down. this we want to avoid)
4954const EDITOR_HEIGHT_OFFSET = 2 ;
5055
56+ // we must only run the lang-setup code once
57+ let LANGUAGE_SETUP_STARTED = false ;
58+ const LANG_ID = 'victorialogs-logsql' ;
59+
60+ function ensureVictoriaLogsLogsQL ( monaco : Monaco ) {
61+ if ( LANGUAGE_SETUP_STARTED === false ) {
62+ LANGUAGE_SETUP_STARTED = true ;
63+ monaco . languages . register ( { id : LANG_ID } ) ;
64+
65+ monaco . languages . setMonarchTokensProvider ( LANG_ID , monarchlanguage ) ;
66+ monaco . languages . setLanguageConfiguration ( LANG_ID , {
67+ ...languageConfiguration ,
68+ wordPattern : / ( - ? \d * \. \d \w * ) | ( [ ^ ` ~ ! # % ^ & * ( ) + \[ { \] } \\ | ; : ' , . < > \/ ? \s ] + ) / g,
69+ // Default: /(-?\d*\.\d\w*)|([^`~!#%^&*()\-=+\[{\]}\\|;:'",.<>\/?\s]+)/g
70+ // Removed `"`, `=`, and `-`, from the exclusion list, so now the completion provider can decide to overwrite any matching words, or just insert text at the cursor
71+ } ) ;
72+ }
73+ }
74+
5175const getStyles = ( theme : GrafanaTheme2 , placeholder : string ) => {
5276 return {
5377 container : css `
@@ -67,33 +91,66 @@ const getStyles = (theme: GrafanaTheme2, placeholder: string) => {
6791const MonacoQueryField = ( props : Props ) => {
6892 // we need only one instance of `overrideServices` during the lifetime of the react component
6993 const containerRef = useRef < HTMLDivElement > ( null ) ;
70- const { onBlur, onRunQuery, initialValue, placeholder, readOnly } = props ;
94+ const { onBlur, onRunQuery, initialValue, placeholder, readOnly, history , timeRange , datasource } = props ;
7195
7296 const onRunQueryRef = useLatest ( onRunQuery ) ;
7397 const onBlurRef = useLatest ( onBlur ) ;
98+ const historyRef = useLatest ( history ) ;
99+ const langProviderRef = useLatest ( datasource . languageProvider ) ;
100+ const completionDataProviderRef = useRef < CompletionDataProvider | null > ( null ) ;
101+ const autocompleteCleanupCallback = useRef < ( ( ) => void ) | null > ( null ) ;
74102
75103 const theme = useTheme2 ( ) ;
76104 const styles = getStyles ( theme , placeholder ) ;
77105
106+ useEffect ( ( ) => {
107+ // when we unmount, we unregister the autocomplete-function, if it was registered
108+ return ( ) => {
109+ autocompleteCleanupCallback . current ?.( ) ;
110+ } ;
111+ } , [ ] ) ;
112+
78113 return (
79- < div
80- aria-label = { selectors . components . QueryField . container }
81- className = { styles . container }
82- ref = { containerRef }
83- >
114+ < div aria-label = { selectors . components . QueryField . container } className = { styles . container } ref = { containerRef } >
84115 < ReactMonacoEditor
85- options = { {
86- ...options ,
87- readOnly
88- } }
89- language = "promql"
116+ options = { { ...options , readOnly } }
117+ language = { LANG_ID }
90118 value = { initialValue }
119+ beforeMount = { ( monaco ) => {
120+ ensureVictoriaLogsLogsQL ( monaco ) ;
121+ } }
91122 onMount = { ( editor , monaco ) => {
92123 // we setup on-blur
93124 editor . onDidBlurEditorWidget ( ( ) => {
94125 onBlurRef . current ( editor . getValue ( ) ) ;
95126 } ) ;
96127
128+ const dataProvider = new CompletionDataProvider ( langProviderRef . current ! , historyRef , timeRange ) ;
129+ completionDataProviderRef . current = dataProvider ;
130+ const completionProvider = getCompletionProvider ( monaco , dataProvider ) ;
131+
132+ // completion-providers in monaco are not registered directly to editor-instances,
133+ // they are registered to languages. this makes it hard for us to have
134+ // separate completion-providers for every query-field-instance
135+ // (but we need that, because they might connect to different datasources).
136+ // the trick we do is, we wrap the callback in a "proxy",
137+ // and in the proxy, the first thing is, we check if we are called from
138+ // "our editor instance", and if not, we just return nothing. if yes,
139+ // we call the completion-provider.
140+ const filteringCompletionProvider : monacoTypes . languages . CompletionItemProvider = {
141+ ...completionProvider ,
142+ provideCompletionItems : ( model , position , context , token ) => {
143+ // if the model-id does not match, then this call is from a different editor-instance,
144+ // not "our instance", so return nothing
145+ if ( editor . getModel ( ) ?. id !== model . id ) {
146+ return { suggestions : [ ] } ;
147+ }
148+ return completionProvider . provideCompletionItems ( model , position , context , token ) ;
149+ } ,
150+ } ;
151+ const { dispose } = monaco . languages . registerCompletionItemProvider ( LANG_ID , filteringCompletionProvider ) ;
152+
153+ autocompleteCleanupCallback . current = dispose ;
97154 const updateElementHeight = ( ) => {
98155 const containerDiv = containerRef . current ;
99156 if ( containerDiv !== null ) {
@@ -110,10 +167,10 @@ const MonacoQueryField = (props: Props) => {
110167
111168 // handle: shift + enter
112169 editor . addAction ( {
113- id : " execute-shift-enter" ,
114- label : " Execute" ,
170+ id : ' execute-shift-enter' ,
171+ label : ' Execute' ,
115172 keybindings : [ monaco . KeyMod . Shift | monaco . KeyCode . Enter ] ,
116- run : ( ) => onRunQueryRef . current ( editor . getValue ( ) || "" )
173+ run : ( ) => onRunQueryRef . current ( editor . getValue ( ) || '' ) ,
117174 } ) ;
118175
119176 /* Something in this configuration of monaco doesn't bubble up [mod]+K, which the
@@ -127,13 +184,8 @@ const MonacoQueryField = (props: Props) => {
127184 const placeholderDecorators = [
128185 {
129186 range : new monaco . Range ( 1 , 1 , 1 , 1 ) ,
130- contents : [
131- { value : "**bold** _italics_ regular `code`" }
132- ] ,
133- options : {
134- className : styles . placeholder ,
135- isWholeLine : true ,
136- } ,
187+ contents : [ { value : '**bold** _italics_ regular `code`' } ] ,
188+ options : { className : styles . placeholder , isWholeLine : true } ,
137189 } ,
138190 ] ;
139191
0 commit comments