(null);\n\n const langProviderRef = useLatest(datasource.languageProvider);\n const historyRef = useLatest(history);\n const onRunQueryRef = useLatest(onRunQuery);\n const onBlurRef = useLatest(onBlur);\n\n const autocompleteCleanupCallback = useRef<(() => void) | null>(null);\n\n const theme = useTheme2();\n const styles = getStyles(theme, placeholder);\n\n useEffect(() => {\n // when we unmount, we unregister the autocomplete-function, if it was registered\n return () => {\n autocompleteCleanupCallback.current?.();\n };\n }, []);\n\n const setPlaceholder = (monaco: Monaco, editor: MonacoEditor) => {\n const placeholderDecorators = [\n {\n range: new monaco.Range(1, 1, 1, 1),\n options: {\n className: styles.placeholder,\n isWholeLine: true,\n },\n },\n ];\n\n let decorators: string[] = [];\n\n const checkDecorators: () => void = () => {\n const model = editor.getModel();\n\n if (!model) {\n return;\n }\n\n const newDecorators = model.getValueLength() === 0 ? placeholderDecorators : [];\n decorators = model.deltaDecorations(decorators, newDecorators);\n };\n\n checkDecorators();\n editor.onDidChangeModelContent(checkDecorators);\n };\n\n const onTypeDebounced = debounce(async (query: string) => {\n onChange(query);\n }, 1000);\n\n return (\n \n {\n ensureLogQL(monaco);\n }}\n onMount={(editor, monaco) => {\n // Monaco has a bug where it runs actions on all instances (https://github.com/microsoft/monaco-editor/issues/2947), so we ensure actions are executed on instance-level with this ContextKey.\n const isEditorFocused = editor.createContextKey('isEditorFocused' + id, false);\n // we setup on-blur\n editor.onDidBlurEditorWidget(() => {\n isEditorFocused.set(false);\n onBlurRef.current(editor.getValue());\n });\n editor.onDidChangeModelContent((e) => {\n const model = editor.getModel();\n if (!model) {\n return;\n }\n const query = model.getValue();\n const errors =\n validateQuery(\n query,\n datasource.interpolateString(query, placeHolderScopedVars),\n model.getLinesContent(),\n parser\n ) || [];\n\n const markers = errors.map(({ error, ...boundary }) => ({\n message: `${\n error ? `Error parsing \"${error}\"` : 'Parse error'\n }. The query appears to be incorrect and could fail to be executed.`,\n severity: monaco.MarkerSeverity.Error,\n ...boundary,\n }));\n\n onTypeDebounced(query);\n monaco.editor.setModelMarkers(model, 'owner', markers);\n });\n const dataProvider = new CompletionDataProvider(langProviderRef.current, historyRef, timeRange);\n const completionProvider = getCompletionProvider(monaco, dataProvider);\n\n // completion-providers in monaco are not registered directly to editor-instances,\n // they are registered to languages. this makes it hard for us to have\n // separate completion-providers for every query-field-instance\n // (but we need that, because they might connect to different datasources).\n // the trick we do is, we wrap the callback in a \"proxy\",\n // and in the proxy, the first thing is, we check if we are called from\n // \"our editor instance\", and if not, we just return nothing. if yes,\n // we call the completion-provider.\n const filteringCompletionProvider: monacoTypes.languages.CompletionItemProvider = {\n ...completionProvider,\n provideCompletionItems: (model, position, context, token) => {\n // if the model-id does not match, then this call is from a different editor-instance,\n // not \"our instance\", so return nothing\n if (editor.getModel()?.id !== model.id) {\n return { suggestions: [] };\n }\n return completionProvider.provideCompletionItems(model, position, context, token);\n },\n };\n\n const { dispose } = monaco.languages.registerCompletionItemProvider(LANG_ID, filteringCompletionProvider);\n\n autocompleteCleanupCallback.current = dispose;\n // this code makes the editor resize itself so that the content fits\n // (it will grow taller when necessary)\n // FIXME: maybe move this functionality into CodeEditor, like:\n // \n const handleResize = () => {\n const containerDiv = containerRef.current;\n if (containerDiv !== null) {\n const pixelHeight = editor.getContentHeight();\n containerDiv.style.height = `${pixelHeight + EDITOR_HEIGHT_OFFSET}px`;\n const pixelWidth = containerDiv.clientWidth;\n editor.layout({ width: pixelWidth, height: pixelHeight });\n }\n };\n\n editor.onDidContentSizeChange(handleResize);\n handleResize();\n // handle: shift + enter\n // FIXME: maybe move this functionality into CodeEditor?\n editor.addCommand(\n monaco.KeyMod.Shift | monaco.KeyCode.Enter,\n () => {\n onRunQueryRef.current(editor.getValue());\n },\n 'isEditorFocused' + id\n );\n\n editor.onDidFocusEditorText(() => {\n isEditorFocused.set(true);\n if (editor.getValue().trim() === '') {\n editor.trigger('', 'editor.action.triggerSuggest', {});\n }\n });\n\n setPlaceholder(monaco, editor);\n }}\n />\n
\n );\n};\n\n// Default export for lazy load.\nexport default MonacoQueryField;\n","import { monacoTypes } from '@grafana/ui';\n\n// this thing here is a workaround in a way.\n// what we want to achieve, is that when the autocomplete-window\n// opens, the \"second, extra popup\" with the extra help,\n// also opens automatically.\n// but there is no API to achieve it.\n// the way to do it is to implement the `storageService`\n// interface, and provide our custom implementation,\n// which will default to `true` for the correct string-key.\n// unfortunately, while the typescript-interface exists,\n// it is not exported from monaco-editor,\n// so we cannot rely on typescript to make sure\n// we do it right. all we can do is to manually\n// lookup the interface, and make sure we code our code right.\n// our code is a \"best effort\" approach,\n// i am not 100% how the `scope` and `target` things work,\n// but so far it seems to work ok.\n// i would use an another approach, if there was one available.\n\nfunction makeStorageService() {\n // we need to return an object that fulfills this interface:\n // https://github.com/microsoft/vscode/blob/ff1e16eebb93af79fd6d7af1356c4003a120c563/src/vs/platform/storage/common/storage.ts#L37\n // unfortunately it is not export from monaco-editor\n\n const strings = new Map();\n\n // we want this to be true by default\n strings.set('expandSuggestionDocs', true.toString());\n\n return {\n // we do not implement the on* handlers\n onDidChangeValue: (data: unknown): void => undefined,\n onDidChangeTarget: (data: unknown): void => undefined,\n onWillSaveState: (data: unknown): void => undefined,\n\n get: (key: string, scope: unknown, fallbackValue?: string): string | undefined => {\n return strings.get(key) ?? fallbackValue;\n },\n\n getBoolean: (key: string, scope: unknown, fallbackValue?: boolean): boolean | undefined => {\n const val = strings.get(key);\n if (val !== undefined) {\n // the interface docs say the value will be converted\n // to a boolean but do not specify how, so we improvise\n return val === 'true';\n } else {\n return fallbackValue;\n }\n },\n\n getNumber: (key: string, scope: unknown, fallbackValue?: number): number | undefined => {\n const val = strings.get(key);\n if (val !== undefined) {\n return parseInt(val, 10);\n } else {\n return fallbackValue;\n }\n },\n\n store: (\n key: string,\n value: string | boolean | number | undefined | null,\n scope: unknown,\n target: unknown\n ): void => {\n // the interface docs say if the value is nullish, it should act as delete\n if (value === null || value === undefined) {\n strings.delete(key);\n } else {\n strings.set(key, value.toString());\n }\n },\n\n remove: (key: string, scope: unknown): void => {\n strings.delete(key);\n },\n\n keys: (scope: unknown, target: unknown): string[] => {\n return Array.from(strings.keys());\n },\n\n logStorage: (): void => {\n console.log('logStorage: not implemented');\n },\n\n migrate: (): Promise => {\n // we do not implement this\n return Promise.resolve(undefined);\n },\n\n isNew: (scope: unknown): boolean => {\n // we create a new storage for every session, we do not persist it,\n // so we return `true`.\n return true;\n },\n\n flush: (reason?: unknown): Promise => {\n // we do not implement this\n return Promise.resolve(undefined);\n },\n };\n}\n\nlet overrideServices: monacoTypes.editor.IEditorOverrideServices = {\n storageService: makeStorageService(),\n};\n\nexport function getOverrideServices(): monacoTypes.editor.IEditorOverrideServices {\n // One instance of this for every query editor\n return overrideServices;\n}\n","// This helper class is used to make typescript warn you when you miss a case-block in a switch statement.\n// For example:\n//\n// const x:'A'|'B'|'C' = 'A';\n//\n// switch(x) {\n// case 'A':\n// // something\n// case 'B':\n// // something\n// default:\n// throw new NeverCaseError(x);\n// }\n//\n//\n// TypeScript detect the missing case and display an error.\n\nexport class NeverCaseError extends Error {\n constructor(value: never) {\n super(`Unexpected case in switch statement: ${JSON.stringify(value)}`);\n }\n}\n","export type LabelOperator = '=' | '!=' | '=~' | '!~';\n\nexport type Label = {\n name: string;\n value: string;\n op: LabelOperator;\n};\n\nexport type Situation =\n | {\n type: 'EMPTY';\n }\n | {\n type: 'AT_ROOT';\n }\n | {\n type: 'IN_LOGFMT';\n otherLabels: string[];\n flags: boolean;\n trailingSpace: boolean;\n trailingComma: boolean;\n logQuery: string;\n }\n | {\n type: 'IN_RANGE';\n }\n | {\n type: 'IN_AGGREGATION';\n }\n | {\n type: 'IN_GROUPING';\n logQuery: string;\n }\n | {\n type: 'IN_LABEL_SELECTOR_NO_LABEL_NAME';\n otherLabels: Label[];\n }\n | {\n type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME';\n labelName: string;\n betweenQuotes: boolean;\n otherLabels: Label[];\n }\n | {\n type: 'AFTER_SELECTOR';\n afterPipe: boolean;\n hasSpace: boolean;\n logQuery: string;\n }\n | {\n type: 'AFTER_UNWRAP';\n logQuery: string;\n }\n | {\n type: 'AFTER_KEEP_AND_DROP';\n logQuery: string;\n };\n\n/**\n * THIS METHOD IS KNOWN TO BE INCOMPLETE due to the decoupling of the Tempo datasource from Grafana core:\n * Incomplete support for LogQL autocomplete from 'public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/situation.ts';\n */\nexport const getSituation = (text: string, pos: number): Situation | null => {\n return {\n type: 'EMPTY',\n };\n};\n","import type { Monaco, monacoTypes } from '@grafana/ui';\n\nimport { CompletionDataProvider } from './CompletionDataProvider';\nimport { NeverCaseError } from './NeverCaseError';\nimport { Situation, getSituation } from './situation';\n\ntype CompletionType =\n | 'HISTORY'\n | 'FUNCTION'\n | 'DURATION'\n | 'LABEL_NAME'\n | 'LABEL_VALUE'\n | 'PATTERN'\n | 'PARSER'\n | 'LINE_FILTER'\n | 'PIPE_OPERATION';\n\ntype Completion = {\n type: CompletionType;\n label: string;\n insertText: string;\n detail?: string;\n documentation?: string;\n triggerOnInsert?: boolean;\n isSnippet?: boolean;\n};\n\nconst DURATION_COMPLETIONS: Completion[] = ['$__auto', '1m', '5m', '10m', '30m', '1h', '1d'].map((text) => ({\n type: 'DURATION',\n label: text,\n insertText: text,\n}));\nconst getCompletions = async (situation: Situation, dataProvider: CompletionDataProvider) => {\n return DURATION_COMPLETIONS;\n};\n\n// from: monacoTypes.languages.CompletionItemInsertTextRule.InsertAsSnippet\nconst INSERT_AS_SNIPPET_ENUM_VALUE = 4;\n\nexport function getSuggestOptions(): monacoTypes.editor.ISuggestOptions {\n return {\n // monaco-editor sometimes provides suggestions automatically, i am not\n // sure based on what, seems to be by analyzing the words already\n // written.\n // to try it out:\n // - enter `go_goroutines{job~`\n // - have the cursor at the end of the string\n // - press ctrl-enter\n // - you will get two suggestions\n // those were not provided by grafana, they are offered automatically.\n // i want to remove those. the only way i found is:\n // - every suggestion-item has a `kind` attribute,\n // that controls the icon to the left of the suggestion.\n // - items auto-generated by monaco have `kind` set to `text`.\n // - we make sure grafana-provided suggestions do not have `kind` set to `text`.\n // - and then we tell monaco not to show suggestions of kind `text`\n showWords: false,\n };\n}\n\nfunction getMonacoCompletionItemKind(type: CompletionType, monaco: Monaco): monacoTypes.languages.CompletionItemKind {\n switch (type) {\n case 'DURATION':\n return monaco.languages.CompletionItemKind.Unit;\n case 'FUNCTION':\n return monaco.languages.CompletionItemKind.Variable;\n case 'HISTORY':\n return monaco.languages.CompletionItemKind.Snippet;\n case 'LABEL_NAME':\n return monaco.languages.CompletionItemKind.Enum;\n case 'LABEL_VALUE':\n return monaco.languages.CompletionItemKind.EnumMember;\n case 'PATTERN':\n return monaco.languages.CompletionItemKind.Constructor;\n case 'PARSER':\n return monaco.languages.CompletionItemKind.Class;\n case 'LINE_FILTER':\n return monaco.languages.CompletionItemKind.TypeParameter;\n case 'PIPE_OPERATION':\n return monaco.languages.CompletionItemKind.Interface;\n default:\n throw new NeverCaseError(type as never);\n }\n}\n\nexport function getCompletionProvider(\n monaco: Monaco,\n dataProvider: CompletionDataProvider\n): monacoTypes.languages.CompletionItemProvider {\n const provideCompletionItems = (\n model: monacoTypes.editor.ITextModel,\n position: monacoTypes.Position\n ): monacoTypes.languages.ProviderResult => {\n const word = model.getWordAtPosition(position);\n const wordUntil = model.getWordUntilPosition(position);\n\n // documentation says `position` will be \"adjusted\" in `getOffsetAt`\n // i don't know what that means, to be sure i clone it\n const positionClone = {\n column: position.column,\n lineNumber: position.lineNumber,\n };\n const offset = model.getOffsetAt(positionClone);\n const situation = getSituation(model.getValue(), offset);\n const range = calculateRange(situation, word, wordUntil, monaco, position);\n const completionsPromise = situation != null ? getCompletions(situation, dataProvider) : Promise.resolve([]);\n return completionsPromise.then((items) => {\n // monaco by default alphabetically orders the items.\n // to stop it, we use a number-as-string sortkey,\n // so that monaco keeps the order we use\n const maxIndexDigits = items.length.toString().length;\n const suggestions: monacoTypes.languages.CompletionItem[] = items.map((item, index) => ({\n kind: getMonacoCompletionItemKind(item.type, monaco),\n label: item.label,\n insertText: item.insertText,\n insertTextRules: item.isSnippet ? INSERT_AS_SNIPPET_ENUM_VALUE : undefined,\n detail: item.detail,\n documentation: item.documentation,\n sortText: index.toString().padStart(maxIndexDigits, '0'), // to force the order we have\n range: range,\n command: item.triggerOnInsert\n ? {\n id: 'editor.action.triggerSuggest',\n title: '',\n }\n : undefined,\n }));\n return { suggestions };\n });\n };\n\n return {\n triggerCharacters: ['{', ',', '[', '(', '=', '~', ' ', '\"', '|'],\n provideCompletionItems,\n };\n}\n\nexport const calculateRange = (\n situation: Situation | null,\n word: monacoTypes.editor.IWordAtPosition | null,\n wordUntil: monacoTypes.editor.IWordAtPosition,\n monaco: Monaco,\n position: monacoTypes.Position\n): monacoTypes.Range => {\n if (\n situation &&\n situation?.type === 'IN_LABEL_SELECTOR_WITH_LABEL_NAME' &&\n 'betweenQuotes' in situation &&\n situation.betweenQuotes\n ) {\n // Word until won't have second quote if they are between quotes\n const indexOfFirstQuote = wordUntil?.word?.indexOf('\"') ?? 0;\n\n const indexOfLastQuote = word?.word?.lastIndexOf('\"') ?? 0;\n\n const indexOfEquals = word?.word.indexOf('=');\n const indexOfLastEquals = word?.word.lastIndexOf('=');\n\n // Just one equals \"=\" the cursor is somewhere within a label value\n // e.g. value=\"labe^l-value\" or value=\"^label-value\" etc\n // We want the word to include everything within the quotes, so the result from autocomplete overwrites the existing label value\n if (\n indexOfLastEquals === indexOfEquals &&\n indexOfFirstQuote !== -1 &&\n indexOfLastQuote !== -1 &&\n indexOfLastEquals !== -1\n ) {\n return word != null\n ? monaco.Range.lift({\n startLineNumber: position.lineNumber,\n endLineNumber: position.lineNumber,\n startColumn: wordUntil.startColumn + indexOfFirstQuote + 1,\n endColumn: wordUntil.startColumn + indexOfLastQuote,\n })\n : monaco.Range.fromPositions(position);\n }\n }\n\n if (situation && situation.type === 'IN_LABEL_SELECTOR_WITH_LABEL_NAME') {\n // Otherwise we want the range to be calculated as the cursor position, as we want to insert the autocomplete, instead of overwriting existing text\n // The cursor position is the length of the wordUntil\n return word != null\n ? monaco.Range.lift({\n startLineNumber: position.lineNumber,\n endLineNumber: position.lineNumber,\n startColumn: wordUntil.endColumn,\n endColumn: wordUntil.endColumn,\n })\n : monaco.Range.fromPositions(position);\n }\n\n // And for all other non-label cases, we want to use the word start and end column\n return word != null\n ? monaco.Range.lift({\n startLineNumber: position.lineNumber,\n endLineNumber: position.lineNumber,\n startColumn: word.startColumn,\n endColumn: word.endColumn,\n })\n : monaco.Range.fromPositions(position);\n};\n","import { chain } from 'lodash';\n\nimport { HistoryItem } from '@grafana/data';\n\nimport { LokiQuery, ParserAndLabelKeysResult, LanguageProvider } from '../../types';\n\nexport function escapeLabelValueInExactSelector(labelValue: string): string {\n return labelValue.replace(/\\\\/g, '\\\\\\\\').replace(/\\n/g, '\\\\n').replace(/\"/g, '\\\\\"');\n}\n\nimport { Label } from './situation';\n\ninterface HistoryRef {\n current: Array>;\n}\n\nexport class CompletionDataProvider {\n constructor(\n private languageProvider: LanguageProvider,\n private historyRef: HistoryRef = { current: [] }\n ) {\n this.queryToLabelKeysCache = new Map();\n }\n private queryToLabelKeysCache: Map;\n\n private buildSelector(labels: Label[]): string {\n const allLabelTexts = labels.map(\n (label) => `${label.name}${label.op}\"${escapeLabelValueInExactSelector(label.value)}\"`\n );\n\n return `{${allLabelTexts.join(',')}}`;\n }\n\n getHistory() {\n return chain(this.historyRef.current)\n .map((history: HistoryItem) => history.query.expr)\n .filter()\n .uniq()\n .value();\n }\n\n async getLabelNames(otherLabels: Label[] = []) {\n if (otherLabels.length === 0) {\n // if there is no filtering, we have to use a special endpoint\n return this.languageProvider.getLabelKeys();\n }\n const data = await this.getSeriesLabels(otherLabels);\n const possibleLabelNames = Object.keys(data); // all names from datasource\n const usedLabelNames = new Set(otherLabels.map((l) => l.name)); // names used in the query\n return possibleLabelNames.filter((label) => !usedLabelNames.has(label));\n }\n\n async getLabelValues(labelName: string, otherLabels: Label[]) {\n if (otherLabels.length === 0) {\n // if there is no filtering, we have to use a special endpoint\n return await this.languageProvider.fetchLabelValues(labelName);\n }\n\n const data = await this.getSeriesLabels(otherLabels);\n return data[labelName] ?? [];\n }\n\n /**\n * Runs a Loki query to extract label keys from the result.\n * The result is cached for the query string.\n *\n * Since various \"situations\" in the monaco code editor trigger this function, it is prone to being called multiple times for the same query\n * Here is a lightweight and simple cache to avoid calling the backend multiple times for the same query.\n *\n * @param logQuery\n */\n async getParserAndLabelKeys(logQuery: string): Promise {\n const EXTRACTED_LABEL_KEYS_MAX_CACHE_SIZE = 2;\n const cachedLabelKeys = this.queryToLabelKeysCache.has(logQuery) ? this.queryToLabelKeysCache.get(logQuery) : null;\n if (cachedLabelKeys) {\n // cache hit! Serve stale result from cache\n return cachedLabelKeys;\n } else {\n // If cache is larger than max size, delete the first (oldest) index\n if (this.queryToLabelKeysCache.size >= EXTRACTED_LABEL_KEYS_MAX_CACHE_SIZE) {\n // Make room in the cache for the fresh result by deleting the \"first\" index\n const keys = this.queryToLabelKeysCache.keys();\n const firstKey = keys.next().value;\n this.queryToLabelKeysCache.delete(firstKey);\n }\n // Fetch a fresh result from the backend\n const labelKeys = await this.languageProvider.getParserAndLabelKeys(logQuery);\n // Add the result to the cache\n this.queryToLabelKeysCache.set(logQuery, labelKeys);\n return labelKeys;\n }\n }\n\n async getSeriesLabels(labels: Label[]) {\n return await this.languageProvider.fetchSeriesLabels(this.buildSelector(labels)).then((data: any) => data ?? {});\n }\n}\n","import { SyntaxNode } from '@lezer/common';\nimport { LRParser } from '@lezer/lr';\n\n// import { ErrorId } from 'app/plugins/datasource/prometheus/querybuilder/shared/parsingUtils';\nconst ErrorId = 0;\n\ninterface ParserErrorBoundary {\n startLineNumber: number;\n startColumn: number;\n endLineNumber: number;\n endColumn: number;\n error: string;\n}\n\ninterface ParseError {\n text: string;\n node: SyntaxNode;\n}\n\n/**\n * Conceived to work in combination with the MonacoQueryField component.\n * Given an original query, and it's interpolated version, it will return an array of ParserErrorBoundary\n * objects containing nodes which are actual errors. The interpolated version (even with placeholder variables)\n * is required because variables look like errors for Lezer.\n * @internal\n */\nexport function validateQuery(\n query: string,\n interpolatedQuery: string,\n queryLines: string[],\n parser: LRParser\n): ParserErrorBoundary[] | false {\n if (!query) {\n return false;\n }\n\n /**\n * To provide support to variable interpolation in query validation, we run the parser in the interpolated\n * query. If there are errors there, we trace them back to the original unparsed query, so we can more\n * accurately highlight the error in the query, since it's likely that the variable name and variable value\n * have different lengths. With this, we also exclude irrelevant parser errors that are produced by\n * lezer not understanding $variables and $__variables, which usually generate 2 or 3 error SyntaxNode.\n */\n const interpolatedErrors: ParseError[] = parseQuery(interpolatedQuery, parser);\n if (!interpolatedErrors.length) {\n return false;\n }\n\n let parseErrors: ParseError[] = interpolatedErrors;\n if (query !== interpolatedQuery) {\n const queryErrors: ParseError[] = parseQuery(query, parser);\n parseErrors = interpolatedErrors.flatMap(\n (interpolatedError) =>\n queryErrors.filter((queryError) => interpolatedError.text === queryError.text) || interpolatedError\n );\n }\n\n return parseErrors.map((parseError) => findErrorBoundary(query, queryLines, parseError)).filter(isErrorBoundary);\n}\n\nfunction parseQuery(query: string, parser: LRParser) {\n const parseErrors: ParseError[] = [];\n const tree = parser.parse(query);\n tree.iterate({\n enter: (nodeRef): false | void => {\n if (nodeRef.type.id === ErrorId) {\n const node = nodeRef.node;\n parseErrors.push({\n node: node,\n text: query.substring(node.from, node.to),\n });\n }\n },\n });\n return parseErrors;\n}\n\nfunction findErrorBoundary(query: string, queryLines: string[], parseError: ParseError): ParserErrorBoundary | null {\n if (queryLines.length === 1) {\n const isEmptyString = parseError.node.from === parseError.node.to;\n const errorNode = isEmptyString && parseError.node.parent ? parseError.node.parent : parseError.node;\n const error = isEmptyString ? query.substring(errorNode.from, errorNode.to) : parseError.text;\n return {\n startLineNumber: 1,\n startColumn: errorNode.from + 1,\n endLineNumber: 1,\n endColumn: errorNode.to + 1,\n error,\n };\n }\n\n let startPos = 0,\n endPos = 0;\n for (let line = 0; line < queryLines.length; line++) {\n endPos = startPos + queryLines[line].length;\n\n if (parseError.node.from > endPos) {\n startPos += queryLines[line].length + 1;\n continue;\n }\n\n return {\n startLineNumber: line + 1,\n startColumn: parseError.node.from - startPos + 1,\n endLineNumber: line + 1,\n endColumn: parseError.node.to - startPos + 1,\n error: parseError.text,\n };\n }\n\n return null;\n}\n\nfunction isErrorBoundary(boundary: ParserErrorBoundary | null): boundary is ParserErrorBoundary {\n return boundary !== null;\n}\n\nexport const placeHolderScopedVars = {\n __interval: { text: '1s', value: '1s' },\n __rate_interval: { text: '1s', value: '1s' },\n __auto: { text: '1s', value: '1s' },\n __interval_ms: { text: '1000', value: 1000 },\n __range_ms: { text: '1000', value: 1000 },\n __range_s: { text: '1', value: 1 },\n __range: { text: '1s', value: '1s' },\n};\n","import { css } from '@emotion/css';\nimport { debounce } from 'lodash';\nimport React, { useRef, useEffect } from 'react';\nimport { useLatest } from 'react-use';\nimport { v4 as uuidv4 } from 'uuid';\n\nimport { GrafanaTheme2 } from '@grafana/data';\nimport { selectors } from '@grafana/e2e-selectors';\nimport { parser } from '@grafana/lezer-logql';\nimport { languageConfiguration, monarchlanguage } from '@grafana/monaco-logql';\nimport { useTheme2, ReactMonacoEditor, Monaco, monacoTypes, MonacoEditor } from '@grafana/ui';\n\nimport { Props } from './MonacoQueryFieldProps';\nimport { getOverrideServices } from './getOverrideServices';\nimport { getCompletionProvider, getSuggestOptions } from './monaco-completion-provider';\nimport { CompletionDataProvider } from './monaco-completion-provider/CompletionDataProvider';\nimport { placeHolderScopedVars, validateQuery } from './monaco-completion-provider/validation';\n\nconst options: monacoTypes.editor.IStandaloneEditorConstructionOptions = {\n codeLens: false,\n contextmenu: false,\n // we need `fixedOverflowWidgets` because otherwise in grafana-dashboards\n // the popup is clipped by the panel-visualizations.\n fixedOverflowWidgets: true,\n folding: false,\n fontSize: 14,\n lineDecorationsWidth: 8, // used as \"padding-left\"\n lineNumbers: 'off',\n minimap: { enabled: false },\n overviewRulerBorder: false,\n overviewRulerLanes: 0,\n padding: {\n // these numbers were picked so that visually this matches the previous version\n // of the query-editor the best\n top: 4,\n bottom: 5,\n },\n renderLineHighlight: 'none',\n scrollbar: {\n vertical: 'hidden',\n verticalScrollbarSize: 8, // used as \"padding-right\"\n horizontal: 'hidden',\n horizontalScrollbarSize: 0,\n alwaysConsumeMouseWheel: false,\n },\n scrollBeyondLastLine: false,\n suggest: getSuggestOptions(),\n suggestFontSize: 12,\n wordWrap: 'on',\n};\n\n// this number was chosen by testing various values. it might be necessary\n// because of the width of the border, not sure.\n//it needs to do 2 things:\n// 1. when the editor is single-line, it should make the editor height be visually correct\n// 2. when the editor is multi-line, the editor should not be \"scrollable\" (meaning,\n// you do a scroll-movement in the editor, and it will scroll the content by a couple pixels\n// up & down. this we want to avoid)\nconst EDITOR_HEIGHT_OFFSET = 2;\n\nconst LANG_ID = 'logql';\n\n// we must only run the lang-setup code once\nlet LANGUAGE_SETUP_STARTED = false;\n\nexport const defaultWordPattern = /(-?\\d*\\.\\d\\w*)|([^`~!#%^&*()\\-=+\\[{\\]}\\\\|;:'\",.<>\\/?\\s]+)/g;\n\nfunction ensureLogQL(monaco: Monaco) {\n if (LANGUAGE_SETUP_STARTED === false) {\n LANGUAGE_SETUP_STARTED = true;\n monaco.languages.register({ id: LANG_ID });\n\n monaco.languages.setMonarchTokensProvider(LANG_ID, monarchlanguage);\n monaco.languages.setLanguageConfiguration(LANG_ID, {\n ...languageConfiguration,\n wordPattern: /(-?\\d*\\.\\d\\w*)|([^`~!#%^&*()+\\[{\\]}\\\\|;:',.<>\\/?\\s]+)/g,\n // Default: /(-?\\d*\\.\\d\\w*)|([^`~!#%^&*()\\-=+\\[{\\]}\\\\|;:'\",.<>\\/?\\s]+)/g\n // 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\n });\n }\n}\n\nconst getStyles = (theme: GrafanaTheme2, placeholder: string) => {\n return {\n container: css`\n border-radius: ${theme.shape.radius.default};\n border: 1px solid ${theme.components.input.borderColor};\n width: 100%;\n .monaco-editor .suggest-widget {\n min-width: 50%;\n }\n `,\n placeholder: css`\n ::after {\n content: '${placeholder}';\n font-family: ${theme.typography.fontFamilyMonospace};\n opacity: 0.3;\n }\n `,\n };\n};\n\nconst MonacoQueryField = ({ history, onBlur, onRunQuery, initialValue, datasource, placeholder, onChange }: Props) => {\n const id = uuidv4();\n // we need only one instance of `overrideServices` during the lifetime of the react component\n const overrideServicesRef = useRef(getOverrideServices());\n const containerRef = useRef(null);\n\n const langProviderRef = useLatest(datasource.languageProvider);\n const historyRef = useLatest(history);\n const onRunQueryRef = useLatest(onRunQuery);\n const onBlurRef = useLatest(onBlur);\n\n const autocompleteCleanupCallback = useRef<(() => void) | null>(null);\n\n const theme = useTheme2();\n const styles = getStyles(theme, placeholder);\n\n useEffect(() => {\n // when we unmount, we unregister the autocomplete-function, if it was registered\n return () => {\n autocompleteCleanupCallback.current?.();\n };\n }, []);\n\n const setPlaceholder = (monaco: Monaco, editor: MonacoEditor) => {\n const placeholderDecorators = [\n {\n range: new monaco.Range(1, 1, 1, 1),\n options: {\n className: styles.placeholder,\n isWholeLine: true,\n },\n },\n ];\n\n let decorators: string[] = [];\n\n const checkDecorators: () => void = () => {\n const model = editor.getModel();\n\n if (!model) {\n return;\n }\n\n const newDecorators = model.getValueLength() === 0 ? placeholderDecorators : [];\n decorators = model.deltaDecorations(decorators, newDecorators);\n };\n\n checkDecorators();\n editor.onDidChangeModelContent(checkDecorators);\n };\n\n const onTypeDebounced = debounce(async (query: string) => {\n onChange(query);\n }, 1000);\n\n return (\n \n {\n ensureLogQL(monaco);\n }}\n onMount={(editor, monaco) => {\n // Monaco has a bug where it runs actions on all instances (https://github.com/microsoft/monaco-editor/issues/2947), so we ensure actions are executed on instance-level with this ContextKey.\n const isEditorFocused = editor.createContextKey('isEditorFocused' + id, false);\n // we setup on-blur\n editor.onDidBlurEditorWidget(() => {\n isEditorFocused.set(false);\n onBlurRef.current(editor.getValue());\n });\n editor.onDidChangeModelContent((e) => {\n const model = editor.getModel();\n if (!model) {\n return;\n }\n const query = model.getValue();\n const errors =\n validateQuery(\n query,\n datasource.interpolateString(query, placeHolderScopedVars),\n model.getLinesContent(),\n parser\n ) || [];\n\n const markers = errors.map(({ error, ...boundary }: any) => ({\n message: `${\n error ? `Error parsing \"${error}\"` : 'Parse error'\n }. The query appears to be incorrect and could fail to be executed.`,\n severity: monaco.MarkerSeverity.Error,\n ...boundary,\n }));\n\n onTypeDebounced(query);\n monaco.editor.setModelMarkers(model, 'owner', markers);\n });\n const dataProvider = new CompletionDataProvider(langProviderRef.current, historyRef);\n const completionProvider = getCompletionProvider(monaco, dataProvider);\n\n // completion-providers in monaco are not registered directly to editor-instances,\n // they are registered to languages. this makes it hard for us to have\n // separate completion-providers for every query-field-instance\n // (but we need that, because they might connect to different datasources).\n // the trick we do is, we wrap the callback in a \"proxy\",\n // and in the proxy, the first thing is, we check if we are called from\n // \"our editor instance\", and if not, we just return nothing. if yes,\n // we call the completion-provider.\n const filteringCompletionProvider: monacoTypes.languages.CompletionItemProvider = {\n ...completionProvider,\n provideCompletionItems: (model, position, context, token) => {\n // if the model-id does not match, then this call is from a different editor-instance,\n // not \"our instance\", so return nothing\n if (editor.getModel()?.id !== model.id) {\n return { suggestions: [] };\n }\n return completionProvider.provideCompletionItems(model, position, context, token);\n },\n };\n\n const { dispose } = monaco.languages.registerCompletionItemProvider(LANG_ID, filteringCompletionProvider);\n\n autocompleteCleanupCallback.current = dispose;\n // this code makes the editor resize itself so that the content fits\n // (it will grow taller when necessary)\n // FIXME: maybe move this functionality into CodeEditor, like:\n // \n const handleResize = () => {\n const containerDiv = containerRef.current;\n if (containerDiv !== null) {\n const pixelHeight = editor.getContentHeight();\n containerDiv.style.height = `${pixelHeight + EDITOR_HEIGHT_OFFSET}px`;\n const pixelWidth = containerDiv.clientWidth;\n editor.layout({ width: pixelWidth, height: pixelHeight });\n }\n };\n\n editor.onDidContentSizeChange(handleResize);\n handleResize();\n // handle: shift + enter\n // FIXME: maybe move this functionality into CodeEditor?\n editor.addCommand(\n monaco.KeyMod.Shift | monaco.KeyCode.Enter,\n () => {\n onRunQueryRef.current(editor.getValue());\n },\n 'isEditorFocused' + id\n );\n\n editor.onDidFocusEditorText(() => {\n isEditorFocused.set(true);\n if (editor.getValue().trim() === '') {\n editor.trigger('', 'editor.action.triggerSuggest', {});\n }\n });\n\n setPlaceholder(monaco, editor);\n }}\n />\n
\n );\n};\n\n// Default export for lazy load.\nexport default MonacoQueryField;\n","import { useRef } from 'react';\nvar useLatest = function (value) {\n var ref = useRef(value);\n ref.current = value;\n return ref;\n};\nexport default useLatest;\n"],"names":["exports","aggregations","parsers","format_expressions","vector_aggregations","vector_matching","vectorMatchingRegex","prev","curr","operators","keywords","makeStorageService","strings","data","key","scope","fallbackValue","val","value","target","reason","overrideServices","getOverrideServices","CompletionDataProvider","languageProvider","historyRef","timeRange","labels","label","history","otherLabels","possibleLabelNames","usedLabelNames","l","labelName","logQuery","cachedLabelKeys","firstKey","labelKeys","NeverCaseError","LOG_COMPLETIONS","AGGREGATION_COMPLETIONS","f","FUNCTION_COMPLETIONS","BUILT_IN_FUNCTIONS_COMPLETIONS","DURATION_COMPLETIONS","text","UNWRAP_FUNCTION_COMPLETIONS","LOGFMT_ARGUMENT_COMPLETIONS","LINE_FILTER_COMPLETIONS","getLineFilterCompletions","afterPipe","completion","operator","documentation","getPipeOperationsCompletions","prefix","completions","getAllHistoryCompletions","dataProvider","expr","getLabelNamesForSelectorCompletions","getInGroupingCompletions","extractedLabelKeys","PARSERS","getParserCompletions","hasJSON","hasLogfmt","hasPack","hasParserInQuery","allParsers","hasLevelInExtractedLabels","extra","parser","getAfterSelectorCompletions","hasSpace","query","structuredMetadataKeys","hasQueryParser","parserCompletions","pipeOperations","getLogfmtCompletions","flags","trailingComma","trailingSpace","labelPrefix","labelCompletions","getLabelValuesForMetricCompletions","betweenQuotes","getAfterUnwrapCompletions","unwrapLabelKeys","getAfterKeepAndDropCompletions","getCompletions","situation","move","node","direction","traverse","path","next","walk","nextTmp","current","expectedNode","getNodeText","parseStringLiteral","inside","isPathMatch","resolverPath","cursorPath","item","index","ERROR_NODE_ID","RESOLVERS","resolveSelector","resolveLogfmtParser","resolveTopLevel","resolveMatcher","resolveLabelsForGrouping","resolveLogRange","resolveDurations","resolveLogRangeFromError","resolvePipeError","resolveAfterUnwrap","resolveAfterKeepAndDrop","LABEL_OP_MAP","getLabelOp","opNode","getLabel","matcherNode","nameNode","op","valueNode","name","getLabels","selectorNode","listNode","pos","exprNode","resolveLogOrLogRange","aggrExpNode","bodyNode","inStringNode","parent","labelNameNode","firstListNode","_","cursorPosition","tree","trimRightTextLen","position","cursor","expectedNodes","inLogfmt","logExprNode","child","keepAndDropParent","resolveCursor","cursorPos","getSituation","currentNode","ids","resolver","INSERT_AS_SNIPPET_ENUM_VALUE","getSuggestOptions","getMonacoCompletionItemKind","type","monaco","getCompletionProvider","model","word","wordUntil","positionClone","offset","range","calculateRange","items","maxIndexDigits","indexOfFirstQuote","indexOfLastQuote","indexOfEquals","indexOfLastEquals","options","EDITOR_HEIGHT_OFFSET","LANG_ID","LANGUAGE_SETUP_STARTED","defaultWordPattern","ensureLogQL","getStyles","theme","placeholder","onBlur","onRunQuery","initialValue","datasource","onChange","id","overrideServicesRef","containerRef","langProviderRef","useLatest","onRunQueryRef","onBlurRef","autocompleteCleanupCallback","styles","setPlaceholder","editor","placeholderDecorators","decorators","checkDecorators","newDecorators","onTypeDebounced","selectors","isEditorFocused","e","markers","error","boundary","completionProvider","filteringCompletionProvider","context","token","dispose","handleResize","containerDiv","pixelHeight","pixelWidth","escapeLabelValueInExactSelector","labelValue","ErrorId","validateQuery","interpolatedQuery","queryLines","interpolatedErrors","parseQuery","parseErrors","queryErrors","interpolatedError","queryError","parseError","findErrorBoundary","isErrorBoundary","nodeRef","isEmptyString","errorNode","startPos","endPos","line","placeHolderScopedVars","ref"],"sourceRoot":""}