import _ from 'lodash' ; export interface SuggestionController { _model : any ; } export default class KustoCodeEditor { codeEditor : monaco . editor . IStandaloneCodeEditor ; completionItemProvider : monaco . IDisposable ; signatureHelpProvider : monaco . IDisposable ; splitWithNewLineRegex = /[^\n]+\n?|\n/g ; newLineRegex = /\r?\n/ ; startsWithKustoPipeRegex = /^\|\s*/g ; kustoPipeRegexStrict = /^\|\s*$/g ; constructor ( private containerDiv , private defaultTimeField , private getSchema , private config ) { } initMonaco ( scope ) { const themeName = this . config . bootData . user . lightTheme ? 'grafana-light' : 'vs-dark' ; monaco . editor . defineTheme ( 'grafana-light' , { base : 'vs' , inherit : true , rules : [ { token : 'comment' , foreground : '008000' } , { token : 'variable.predefined' , foreground : '800080' } , { token : 'function' , foreground : '0000FF' } , { token : 'operator.sql' , foreground : 'FF4500' } , { token : 'string' , foreground : 'B22222' } , { token : 'operator.scss' , foreground : '0000FF' } , { token : 'variable' , foreground : 'C71585' } , { token : 'variable.parameter' , foreground : '9932CC' } , { token : '' , foreground : '000000' } , { token : 'type' , foreground : '0000FF' } , { token : 'tag' , foreground : '0000FF' } , { token : 'annotation' , foreground : '2B91AF' } , { token : 'keyword' , foreground : '0000FF' } , { token : 'number' , foreground : '191970' } , { token : 'annotation' , foreground : '9400D3' } , { token : 'invalid' , background : 'cd3131' } , ] , colors : { 'textCodeBlock.background' : '#FFFFFF' , } , } ) ; monaco . languages [ 'kusto' ] . kustoDefaults . setLanguageSettings ( { includeControlCommands : true , newlineAfterPipe : true , useIntellisenseV2 : false , } ) ; this . codeEditor = monaco . editor . create ( this . containerDiv , { value : scope . content || 'Write your query here' , language : 'kusto' , selectionHighlight : false , theme : themeName , folding : true , lineNumbers : 'off' , lineHeight : 16 , suggestFontSize : 13 , dragAndDrop : false , occurrencesHighlight : false , minimap : { enabled : false , } , renderIndentGuides : false , wordWrap : 'on' , } ) ; this . codeEditor . layout ( ) ; if ( monaco . editor . getModels ( ) . length === 1 ) { this . completionItemProvider = monaco . languages . registerCompletionItemProvider ( 'kusto' , { triggerCharacters : [ '.' , ' ' ] , provideCompletionItems : this . getCompletionItems . bind ( this ) , } ) ; this . signatureHelpProvider = monaco . languages . registerSignatureHelpProvider ( 'kusto' , { signatureHelpTriggerCharacters : [ '(' , ')' ] , provideSignatureHelp : this . getSignatureHelp . bind ( this ) , } ) ; } this . codeEditor . createContextKey ( 'readyToExecute' , true ) ; this . codeEditor . onDidChangeCursorSelection ( event => { this . onDidChangeCursorSelection ( event ) ; } ) ; this . getSchema ( ) . then ( schema => { if ( ! schema ) { return ; } monaco . languages [ 'kusto' ] . getKustoWorker ( ) . then ( workerAccessor => { const model = this . codeEditor . getModel ( ) ; workerAccessor ( model . uri ) . then ( worker => { const dbName = Object . keys ( schema . Databases ) . length > 0 ? Object . keys ( schema . Databases ) [ 0 ] : '' ; worker . setSchemaFromShowSchema ( schema , 'https://help.kusto.windows.net' , dbName ) ; this . codeEditor . layout ( ) ; } ) ; } ) ; } ) ; } setOnDidChangeModelContent ( listener ) { this . codeEditor . onDidChangeModelContent ( listener ) ; } disposeMonaco ( ) { if ( this . completionItemProvider ) { try { this . completionItemProvider . dispose ( ) ; } catch ( e ) { console . error ( 'Failed to dispose the completion item provider.' , e ) ; } } if ( this . signatureHelpProvider ) { try { this . signatureHelpProvider . dispose ( ) ; } catch ( e ) { console . error ( 'Failed to dispose the signature help provider.' , e ) ; } } if ( this . codeEditor ) { try { this . codeEditor . dispose ( ) ; } catch ( e ) { console . error ( 'Failed to dispose the editor component.' , e ) ; } } } addCommand ( keybinding , commandFunc ) { this . codeEditor . addCommand ( keybinding , commandFunc , 'readyToExecute' ) ; } getValue ( ) { return this . codeEditor . getValue ( ) ; } toSuggestionController ( srv ) { return < any > srv ; } setEditorContent ( value ) { this . codeEditor . setValue ( value ) ; } getCompletionItems ( model , position ) { const timeFilterDocs = '##### Macro that uses the selected timerange in Grafana to filter the query.\n\n' + '- `$__timeFilter()` -> Uses the ' + this . defaultTimeField + ' column\n\n' + '- `$__timeFilter(datetimeColumn)` ->  Uses the specified datetime column to build the query.' ; var textUntilPosition = model . getValueInRange ( { startLineNumber : 1 , startColumn : 1 , endLineNumber : position . lineNumber , endColumn : position . column , } ) ; if ( ! _ . includes ( textUntilPosition , '|' ) ) { return [ ] ; } if ( ! _ . includes ( textUntilPosition . toLowerCase ( ) , 'where' ) ) { return [ { label : 'where $__timeFilter(timeColumn)' , kind : monaco . languages . CompletionItemKind . Keyword , insertText : { value : 'where \\$__timeFilter(${0:' + this . defaultTimeField + '})' , } , documentation : { value : timeFilterDocs , } , } , ] ; } if ( _ . includes ( model . getLineContent ( position . lineNumber ) . toLowerCase ( ) , 'where' ) ) { return [ { label : '$__timeFilter(timeColumn)' , kind : monaco . languages . CompletionItemKind . Keyword , insertText : { value : '\\$__timeFilter(${0:' + this . defaultTimeField + '})' , } , documentation : { value : timeFilterDocs , } , } , { label : '$__from' , kind : monaco . languages . CompletionItemKind . Keyword , insertText : { value : `\\$__from` , } , documentation : { value : 'Built-in variable that returns the from value of the selected timerange in Grafana.\n\n' + 'Example: `where ' + this . defaultTimeField + ' > $__from` ' , } , } , { label : '$__to' , kind : monaco . languages . CompletionItemKind . Keyword , insertText : { value : `\\$__to` , } , documentation : { value : 'Built-in variable that returns the to value of the selected timerange in Grafana.\n\n' + 'Example: `where ' + this . defaultTimeField + ' < $__to` ' , } , } , { label : '$__interval' , kind : monaco . languages . CompletionItemKind . Keyword , insertText : { value : `\\$__interval` , } , documentation : { value : '##### Built-in variable that returns an automatic time grain suitable for the current timerange.\n\n' + 'Used with the bin() function - `bin(' + this . defaultTimeField + ', $__interval)` \n\n' + '[Grafana docs](http://docs.grafana.org/reference/templating/#the-interval-variable)' , } , } , ] ; } return [ ] ; } getSignatureHelp ( model , position , token ) { var textUntilPosition = model . getValueInRange ( { startLineNumber : position . lineNumber , startColumn : position . column - 14 , endLineNumber : position . lineNumber , endColumn : position . column , } ) ; if ( textUntilPosition !== '$__timeFilter(' ) { return < monaco . languages . SignatureHelp > { } ; } const signature = { activeParameter : 0 , activeSignature : 0 , signatures : [ { label : '$__timeFilter(timeColumn)' , parameters : [ { label : 'timeColumn' , documentation : 'Default is ' + this . defaultTimeField + ' column. Datetime column to filter data using the selected date range. ' , } , ] , } , ] , } ; return signature ; } onDidChangeCursorSelection ( event ) { if ( event . source !== 'modelChange' || event . reason !== monaco . editor . CursorChangeReason . RecoverFromMarkers ) { return ; } const lastChar = this . getCharAt ( event . selection . positionLineNumber , event . selection . positionColumn - 1 ) ; if ( lastChar !== ' ' ) { return ; } this . triggerSuggestions ( ) ; } triggerSuggestions ( ) { const suggestController = this . codeEditor . getContribution ( 'editor.contrib.suggestController' ) ; if ( ! suggestController ) { return ; } const convertedController = this . toSuggestionController ( suggestController ) ; convertedController . _model . cancel ( ) ; setTimeout ( function ( ) { convertedController . _model . trigger ( true ) ; } , 10 ) ; } getCharAt ( lineNumber , column ) { const model = this . codeEditor . getModel ( ) ; if ( model . getLineCount ( ) === 0 || model . getLineCount ( ) < lineNumber ) { return '' ; } const line = model . getLineContent ( lineNumber ) ; if ( line . length < column || column < 1 ) { return '' ; } return line [ column - 1 ] ; } }