UNPKG

@kusto/monaco-kusto

Version:

CSL, KQL plugin for the Monaco Editor

1,185 lines (1,146 loc) 83.5 kB
/*!----------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * monaco-kusto version: 13.1.1(178105a761985a9b7c16d45b528f829e1c112ff0) * Released under the MIT license * https://https://github.com/Azure/monaco-kusto/blob/master/README.md *-----------------------------------------------------------------------------*/ import * as worker from 'monaco-editor/esm/vs/editor/editor.worker'; import * as ls from 'vscode-languageserver-types'; import XRegExp from 'xregexp'; import { d as getEntityDataTypeFromCslType, a as getCallName, b as getExpression, g as getCslTypeNameFromClrType } from './schema-c46b688b.js'; import '@kusto/language-service/bridge.min'; import '@kusto/language-service/Kusto.JavaScript.Client.min'; import '@kusto/language-service/newtonsoft.json.min'; import '@kusto/language-service-next/Kusto.Language.Bridge.min'; var k = Kusto.Data.IntelliSense; var parsing = Kusto.Language.Parsing; var k2 = Kusto.Language.Editor; var sym = Kusto.Language.Symbols; var GlobalState = Kusto.Language.GlobalState; let List = System.Collections.Generic.List$1; function assertNever(x) { throw new Error('Unexpected object: ' + x); } class ParseProperties { constructor(version, uri, rulesProvider, parseMode) { this.version = version; this.uri = uri; this.rulesProvider = rulesProvider; this.parseMode = parseMode; } isParseNeeded(document, rulesProvider, parseMode) { if (document.uri === this.uri && (!rulesProvider || rulesProvider === this.rulesProvider) && document.version <= this.version && parseMode && parseMode <= this.parseMode) { return false; } return true; } } let TokenKind = /*#__PURE__*/function (TokenKind) { TokenKind[TokenKind["TableToken"] = 2] = "TableToken"; TokenKind[TokenKind["TableColumnToken"] = 4] = "TableColumnToken"; TokenKind[TokenKind["OperatorToken"] = 8] = "OperatorToken"; TokenKind[TokenKind["SubOperatorToken"] = 16] = "SubOperatorToken"; TokenKind[TokenKind["CalculatedColumnToken"] = 32] = "CalculatedColumnToken"; TokenKind[TokenKind["StringLiteralToken"] = 64] = "StringLiteralToken"; TokenKind[TokenKind["FunctionNameToken"] = 128] = "FunctionNameToken"; TokenKind[TokenKind["UnknownToken"] = 256] = "UnknownToken"; TokenKind[TokenKind["CommentToken"] = 512] = "CommentToken"; TokenKind[TokenKind["PlainTextToken"] = 1024] = "PlainTextToken"; TokenKind[TokenKind["DataTypeToken"] = 2048] = "DataTypeToken"; TokenKind[TokenKind["ControlCommandToken"] = 4096] = "ControlCommandToken"; TokenKind[TokenKind["CommandPartToken"] = 8192] = "CommandPartToken"; TokenKind[TokenKind["QueryParametersToken"] = 16384] = "QueryParametersToken"; TokenKind[TokenKind["CslCommandToken"] = 32768] = "CslCommandToken"; TokenKind[TokenKind["LetVariablesToken"] = 65536] = "LetVariablesToken"; TokenKind[TokenKind["PluginToken"] = 131072] = "PluginToken"; TokenKind[TokenKind["BracketRangeToken"] = 262144] = "BracketRangeToken"; TokenKind[TokenKind["ClientDirectiveToken"] = 524288] = "ClientDirectiveToken"; return TokenKind; }({}); /** * A plain old javascript object that is roughly equivalent to the @kusto/language-service-next object, but without * all the Bridge.Net properties and methods. this object is being sent from web worker to main thread and turns out * that when posting the message we lose all properties (and functions), thus we use a POJO instead. * This issue started happening once upgrading to 0.20.0 from 0.15.5. */ /** * colorization data for specific line range. */ const symbolKindToName = { [sym.SymbolKind.Cluster]: 'Cluster', [sym.SymbolKind.Column]: 'Column', [sym.SymbolKind.Command]: 'Command', [sym.SymbolKind.Database]: 'Database', [sym.SymbolKind.EntityGroup]: 'EntityGroup', [sym.SymbolKind.EntityGroupElement]: 'EntityGroupElement', [sym.SymbolKind.Error]: 'Error', [sym.SymbolKind.Function]: 'Function', [sym.SymbolKind.Graph]: 'Graph', [sym.SymbolKind.Group]: 'Group', [sym.SymbolKind.MaterializedView]: 'MaterializedView', [sym.SymbolKind.None]: 'None', [sym.SymbolKind.Operator]: 'Operator', [sym.SymbolKind.Option]: 'Option', [sym.SymbolKind.Parameter]: 'Parameter', [sym.SymbolKind.Pattern]: 'Pattern', [sym.SymbolKind.QueryOperatorParameter]: 'QueryOperatorParameter', [sym.SymbolKind.Primitive]: 'Primitive', [sym.SymbolKind.Table]: 'Table', [sym.SymbolKind.Tuple]: 'Tuple', [sym.SymbolKind.Variable]: 'Variable', [sym.SymbolKind.Void]: 'Void' }; /** * Kusto Language service translates the kusto object model (transpiled from C# by Bridge.Net) * to the vscode language server types, which are used by vscode language extensions. * This should make things easier in the future to provide a vscode extension based on this translation layer. * * Further translations, if needed, to support specific editors (Atom, sublime, Etc) * should be done on top of this API, since it is (at least meant to be) a standard that is supported by multiple editors. * * Note1: Currently monaco isn't using this object model so further translation will be necessary on calling modules. * * Note2: This file is responsible for interacting with the kusto object model and exposing Microsoft language service types. * An exception to that rule is tokenization (and syntax highlighting which depends on it) - * since it's not currently part of the Microsoft language service protocol. Thus tokenize() _does_ 'leak' kusto types to the callers. */ class KustoLanguageService { /** * Taken from: * https://msazure.visualstudio.com/One/_git/Azure-Kusto-Service?path=/Src/Tools/Kusto.Explorer.Control/QueryEditors/KustoScriptEditor/KustoScriptEditorControl2.xaml.cs&version=GBdev&line=2075&lineEnd=2075&lineStartColumn=9&lineEndColumn=77&lineStyle=plain&_a=contents */ _toOptionKind = { [k2.CompletionKind.AggregateFunction]: k.OptionKind.FunctionAggregation, [k2.CompletionKind.BuiltInFunction]: k.OptionKind.FunctionScalar, [k2.CompletionKind.Cluster]: k.OptionKind.Database, [k2.CompletionKind.Column]: k.OptionKind.Column, [k2.CompletionKind.CommandPrefix]: k.OptionKind.Command, [k2.CompletionKind.Database]: k.OptionKind.Database, [k2.CompletionKind.DatabaseFunction]: k.OptionKind.FunctionServerSide, [k2.CompletionKind.Example]: k.OptionKind.Literal, [k2.CompletionKind.Identifier]: k.OptionKind.None, [k2.CompletionKind.Keyword]: k.OptionKind.Option, [k2.CompletionKind.LocalFunction]: k.OptionKind.FunctionLocal, [k2.CompletionKind.MaterialiedView]: k.OptionKind.MaterializedView, [k2.CompletionKind.Parameter]: k.OptionKind.Parameter, [k2.CompletionKind.Punctuation]: k.OptionKind.None, [k2.CompletionKind.QueryPrefix]: k.OptionKind.Operator, [k2.CompletionKind.RenderChart]: k.OptionKind.OptionRender, [k2.CompletionKind.ScalarInfix]: k.OptionKind.None, [k2.CompletionKind.ScalarPrefix]: k.OptionKind.Literal, [k2.CompletionKind.ScalarType]: k.OptionKind.DataType, [k2.CompletionKind.Syntax]: k.OptionKind.None, [k2.CompletionKind.Table]: k.OptionKind.Table, [k2.CompletionKind.TabularPrefix]: k.OptionKind.None, [k2.CompletionKind.TabularSuffix]: k.OptionKind.None, [k2.CompletionKind.Unknown]: k.OptionKind.None, [k2.CompletionKind.Variable]: k.OptionKind.Parameter, [k2.CompletionKind.Option]: k.OptionKind.Option, [k2.CompletionKind.Graph]: k.OptionKind.Graph, [k2.CompletionKind.EntityGroup]: k.OptionKind.EntityGroup, [k2.CompletionKind.StoredQueryResult]: k.OptionKind.StoredQueryResult }; constructor(schema, languageSettings) { this._schemaCache = {}; this._kustoJsSchema = KustoLanguageService.convertToKustoJsSchema(schema); this.__kustoJsSchemaV2 = this.convertToKustoJsSchemaV2(schema); this._schema = schema; this._clustersSetInGlobalState = new Set(); this._nonEmptyDatabaseSetInGlobalState = new Set(); // used to remove clusters that are already in the global state this.configure(languageSettings); this._newlineAppendPipePolicy = new Kusto.Data.IntelliSense.ApplyPolicy(); this._newlineAppendPipePolicy.Text = '\n| '; } createDatabaseUniqueName(clusterName, databaseName) { return `${clusterName}_${databaseName}`; } /** * A setter for _kustoJsSchemaV2. After a schema (global state) is set, create 2 sets of cluster and database names. */ set _kustoJsSchemaV2(globalState) { this.__kustoJsSchemaV2 = globalState; this._clustersSetInGlobalState.clear(); this._nonEmptyDatabaseSetInGlobalState.clear(); // create 2 Sets with cluster names and database names based on the updated Global State. for (let i = 0; i < globalState.Clusters.Count; i++) { const clusterSymbol = this._kustoJsSchemaV2.Clusters.getItem(i); this._clustersSetInGlobalState.add(clusterSymbol.Name); for (let i2 = 0; i2 < clusterSymbol.Databases.Count; i2++) { const databaseSymbol = clusterSymbol.Databases.getItem(i2); if (databaseSymbol.Tables.Count > 0) { // only include database with tables this._nonEmptyDatabaseSetInGlobalState.add(this.createDatabaseUniqueName(clusterSymbol.Name, databaseSymbol.Name)); } } } } /** * A getter for __kustoJsSchemaV2 */ get _kustoJsSchemaV2() { return this.__kustoJsSchemaV2; } configure(languageSettings) { this._languageSettings = languageSettings; const { includeExtendedSyntax } = this._languageSettings.completionOptions; this._completionOptions = Kusto.Language.Editor.CompletionOptions.Default.WithIncludeExtendedSyntax(includeExtendedSyntax).WithIncludePunctuationOnlySyntax(false); // Since we're still reverting to V1 intellisense for control commands, we need to update the rules provider // (which is a notion of V1 intellisense). this.createRulesProvider(this._kustoJsSchema, this._schema.clusterType); } doComplete(document, position) { return this.doCompleteV2(document, position); } disabledCompletionItemsV2 = { // render charts ladderchart: k2.CompletionKind.RenderChart, pivotchart: k2.CompletionKind.RenderChart, timeline: k2.CompletionKind.RenderChart, timepivot: k2.CompletionKind.RenderChart, '3Dchart': k2.CompletionKind.RenderChart, list: k2.CompletionKind.RenderChart }; /** * important: Only use during development to test Global State. * Prints clusters, databases and tables that are currently in the GlobalState. */ debugGlobalState(globals) { // iterate over clusters console.log(`globals.Clusters.Count: ${globals.Clusters.Count}`); for (let i = 0; i < globals.Clusters.Count; i++) { const cluster = globals.Clusters.getItem(i); console.log(`cluster: ${cluster.Name}`); // iterate over databases console.log(`cluster.Databases.Count: ${cluster.Databases.Count}`); for (let i2 = 0; i2 < cluster.Databases.Count; i2++) { const database = cluster.Databases.getItem(i2); console.log(`cluster.database: [${cluster.Name}].[${database.Name}]`); // iterate over tables console.log(`cluster.Databases.Tables.Count: ${database.Tables.Count}`); for (let i3 = 0; i3 < database.Tables.Count; i3++) { const table = database.Tables.getItem(i3); console.log(`cluster.database.table: [${cluster.Name}].[${database.Name}].[${table.Name}]`); } } } } /** * Prepending the doc of the actual topic at the top */ formatHelpTopic(helpTopic) { return `**${helpTopic.Name} [(view online)](${helpTopic.Url})**\n\n${helpTopic.LongDescription}`; } doCompleteV2(document, position) { if (!document) { return Promise.resolve(ls.CompletionList.create([])); } const script = this.parseDocumentV2(document); // print cluster/database/tables from CodeScript.Globals // this.debugGlobalState(script.Globals); // get current command const cursorOffset = document.offsetAt(position); let currentCommand = script.GetBlockAtPosition(cursorOffset); const completionItems = currentCommand.Service.GetCompletionItems(cursorOffset, this._completionOptions); let disabledItems = this.disabledCompletionItemsV2; if (this._languageSettings.disabledCompletionItems) { this._languageSettings.disabledCompletionItems.map(item => { // logic will treat unknown as a '*' wildcard, meaning that if the key is in the object // the completion item will be suppressed. disabledItems[item] = k2.CompletionKind.Unknown; }); } const itemsAsArray = this.toArray(completionItems.Items); let items = itemsAsArray.filter(item => !(item && item.MatchText && disabledItems[item.MatchText] !== undefined && (disabledItems[item.MatchText] === k2.CompletionKind.Unknown || disabledItems[item.MatchText] === item.Kind))).map((kItem, index) => { const v1CompletionOption = new k.CompletionOption(this._toOptionKind[kItem.Kind] || k.OptionKind.None, kItem.DisplayText); const helpTopic = this.getTopic(v1CompletionOption); // If we have AfterText it means that the cursor should not be placed at end of suggested text. // In that case we switch to snippet format and represent the point where the cursor should be as // as '\$0' const { textToInsert, format } = kItem.AfterText && kItem.AfterText.length > 0 ? { // Need to escape dollar sign since it is used as a placeholder in snippet. // Usually dollar sign is not a valid character in a function name, but grafana uses macros that start with dollars. textToInsert: `${kItem.EditText.replace('$', '\\$')}$0${kItem.AfterText}`, format: ls.InsertTextFormat.Snippet } : { textToInsert: kItem.EditText, format: ls.InsertTextFormat.PlainText }; const lsItem = ls.CompletionItem.create(kItem.DisplayText); const startPosition = document.positionAt(completionItems.EditStart); const endPosition = document.positionAt(completionItems.EditStart + completionItems.EditLength); lsItem.textEdit = ls.TextEdit.replace(ls.Range.create(startPosition, endPosition), textToInsert); // Changing the first letter to be lower case, to ignore case-sensitive matching lsItem.filterText = kItem.MatchText.charAt(0).toLowerCase() + kItem.MatchText.slice(1); lsItem.kind = this.kustoKindToLsKindV2(kItem.Kind); lsItem.sortText = kItem.OrderText; lsItem.insertTextFormat = format; lsItem.detail = helpTopic ? helpTopic.ShortDescription : undefined; lsItem.documentation = helpTopic ? { value: this.formatHelpTopic(helpTopic), kind: ls.MarkupKind.Markdown } : undefined; return lsItem; }); return Promise.resolve(ls.CompletionList.create(items)); } /** * when trying to get a topic we need the function name (abs, toLower, ETC). * The problem is that the 'Value' string also contains the arguments (e.g abs(number)), which means that we are * not able to correlate the option with its documentation. * This piece of code tries to strip this hwne getting topic. * @param completionOption the Completion option */ getTopic(completionOption) { if (completionOption.Kind == k.OptionKind.FunctionScalar || completionOption.Kind == k.OptionKind.FunctionAggregation) { // from a value like 'abs(number)' remove the '(number)' so that only 'abs' will remain const indexOfParen = completionOption.Value.indexOf('('); if (indexOfParen >= 0) { completionOption = new k.CompletionOption(completionOption.Kind, completionOption.Value.substring(0, indexOfParen)); } } return k.CslDocumentation.Instance.GetTopic(completionOption); } doRangeFormat(document, range) { if (!document) { return Promise.resolve([]); } const rangeStartOffset = document.offsetAt(range.start); const rangeEndOffset = document.offsetAt(range.end); const commands = this.getFormattedCommandsInDocumentV2(document, rangeStartOffset, rangeEndOffset); if (!commands.originalRange || commands.formattedCommands.length === 0) { return Promise.resolve([]); } return Promise.resolve([ls.TextEdit.replace(commands.originalRange, commands.formattedCommands.join(''))]); } doDocumentFormat(document) { if (!document) { return Promise.resolve([]); } const startPos = document.positionAt(0); const endPos = document.positionAt(document.getText().length); const fullDocRange = ls.Range.create(startPos, endPos); const formattedDoc = this.getFormattedCommandsInDocumentV2(document).formattedCommands.join(''); return Promise.resolve([ls.TextEdit.replace(fullDocRange, formattedDoc)]); } // Method is not triggered, instead doRangeFormat is invoked with the range of the caret's line. doCurrentCommandFormat(document, caretPosition) { const offset = document.offsetAt(caretPosition); const range = this.createRange(document, offset - 1, offset + 1); return this.doRangeFormat(document, range); } doFolding(document) { if (!document) { return Promise.resolve([]); } return this.getCommandsInDocument(document).then(commands => { return commands.map(command => { // don't count the last empty line as part of the folded range (consider linux, mac, pc newlines) if (command.text.endsWith('\r\n')) { command.absoluteEnd -= 2; } else if (command.text.endsWith('\r') || command.text.endsWith('\n')) { --command.absoluteEnd; } const startPosition = document.positionAt(command.absoluteStart); const endPosition = document.positionAt(command.absoluteEnd); return { startLine: startPosition.line, startCharacter: startPosition.character, endLine: endPosition.line, endCharacter: endPosition.character }; }); }); } getClusterReferences(document, cursorOffset) { const script = this.parseDocumentV2(document); const currentBlock = this.getCurrentCommandV2(script, cursorOffset); let clusterReferences = currentBlock?.Service?.GetClusterReferences(); if (!clusterReferences) { return Promise.resolve([]); } const newClustersReferencesSet = new Set(); // used to remove duplicates // Keep only unique clusters that aren't already exist in the Global State for (let i = 0; i < clusterReferences.Count; i++) { const clusterReference = clusterReferences.getItem(i); // Because the engine client adds suffix anyway // const clusterName = Kusto.Language.KustoFacts.GetFullHostName(clusterReference.Cluster, Kusto.Language.KustoFacts.KustoWindowsNet); const clusterName = Kusto.Language.KustoFacts.GetFullHostName(clusterReference.Cluster, null); if (!this._clustersSetInGlobalState.has(clusterName)) { newClustersReferencesSet.add(clusterName); } } return Promise.resolve(Array.from(newClustersReferencesSet).map(clusterName => ({ clusterName }))); } getDatabaseReferences(document, cursorOffset) { const script = this.parseDocumentV2(document); const currentBlock = this.getCurrentCommandV2(script, cursorOffset); let databasesReferences = currentBlock?.Service?.GetDatabaseReferences(); if (!databasesReferences) { return Promise.resolve([]); } let newDatabasesReferences = []; let newDatabasesReferencesSet = new Set(); for (let i1 = 0; i1 < databasesReferences.Count; i1++) { const databaseReference = databasesReferences.getItem(i1); const clusterHostName = Kusto.Language.KustoFacts.GetFullHostName(databaseReference.Cluster, null); // ignore duplicates const databaseReferenceUniqueId = this.createDatabaseUniqueName(clusterHostName, databaseReference.Database); if (newDatabasesReferencesSet.has(databaseReferenceUniqueId)) { continue; } newDatabasesReferencesSet.add(databaseReferenceUniqueId); // ignore references that are already in the GlobalState. let foundInGlobalState = this._nonEmptyDatabaseSetInGlobalState.has(databaseReferenceUniqueId); if (!foundInGlobalState) { newDatabasesReferences.push({ databaseName: databaseReference.Database, clusterName: clusterHostName }); } } return Promise.resolve(newDatabasesReferences); } doValidation(document, changeIntervals, includeWarnings, includeSuggestions) { // didn't implement validation for v1. if (!document) { return Promise.resolve([]); } const script = this.parseDocumentV2(document); let blocks = this.toArray(script.Blocks); if (changeIntervals.length > 0) { blocks = this.getAffectedBlocks(blocks, changeIntervals); } const diagnostics = blocks.map(block => { // GetDiagnostics returns the errors in the block let diagnostics = this.toArray(block.Service.GetDiagnostics()); const enableWarnings = includeWarnings ?? this._languageSettings.enableQueryWarnings; const enableSuggestions = includeSuggestions ?? this._languageSettings.enableQuerySuggestions; if (enableWarnings || enableSuggestions) { // Concat Warnings and suggestions to the diagnostics const warningAndSuggestionDiagnostics = block.Service.GetAnalyzerDiagnostics(true); const filterredDiagnostics = this.toArray(warningAndSuggestionDiagnostics).filter(d => { const allowSeverity = enableWarnings && d.Severity === 'Warning' || enableSuggestions && d.Severity === 'Suggestion'; const allowCode = !this._languageSettings.disabledDiagnosticCodes?.includes(d.Code); return allowSeverity && allowCode; }); diagnostics = diagnostics.concat(filterredDiagnostics); } return diagnostics; }).reduce((prev, curr) => prev.concat(curr), []); const lsDiagnostics = this.toLsDiagnostics(diagnostics, document); return Promise.resolve(lsDiagnostics); } getApplyCodeActions(document, start, end) { const script = this.parseDocumentV2(document); let block = this.getAffectedBlocks(this.toArray(script.Blocks), [{ start, end }])[0]; const codeActionInfo = block.Service.GetCodeActions(start, start, 0, null, true, null, new Kusto.Language.Utils.CancellationToken()); const codeActions = this.toArray(codeActionInfo.Actions); // Some code actions are of type "MenuAction". We want to flat them out, to show them seperately. let flatCodeActions = []; for (let i = 0; i < codeActions.length; i++) { flatCodeActions.push(...this.flattenCodeActions(codeActions[i], null)); } return flatCodeActions; } getResultActions(document, start, end) { const script = this.parseDocumentV2(document); let block = this.getAffectedBlocks(this.toArray(script.Blocks), [{ start, end }])[0]; const applyCodeActions = this.getApplyCodeActions(document, start, end); const resultActionsMap = applyCodeActions.map(applyCodeAction => { let changes = []; const codeActionResults = this.toArray(block.Service.ApplyCodeAction(applyCodeAction, start).Actions); const changeTextAction = codeActionResults.find(c => c instanceof Kusto.Language.Editor.ChangeTextAction); if (changeTextAction) { changes = this.toArray(changeTextAction.Changes).map(change => ({ start: change.Start + block.Start, deleteLength: change.DeleteLength, insertText: change.InsertText })); } return { title: applyCodeAction.Title, changes, kind: applyCodeAction.Kind }; }).filter(resultAction => resultAction.changes.length); return Promise.resolve(resultActionsMap); } transformCodeActionTitle(currentActionTitle, parentActionTitle) { let title = currentActionTitle; switch (title) { case 'Apply': title = 'Apply once'; break; case 'Fix All': title = 'Apply to all'; break; case 'Extract Value': title = 'Extract value'; break; } if (parentActionTitle) { // We want to lower case the first character since it's going to be in brackets const parentActionTitleLowerCased = parentActionTitle.charAt(0).toUpperCase() + parentActionTitle.slice(1); title = `${title} (${parentActionTitleLowerCased})`; } return title; } flattenCodeActions(codeAction, parentTitle) { const applyActions = []; if (codeAction instanceof k2.ApplyAction) { codeAction.Title = this.transformCodeActionTitle(codeAction.Title, parentTitle); applyActions.push(codeAction); } else if (codeAction instanceof k2.MenuAction) { const nestedCodeActions = this.toArray(codeAction.Actions); for (let i = 0; i < nestedCodeActions.length; i++) { applyActions.push(...this.flattenCodeActions(nestedCodeActions[i], codeAction.Title)); } } return applyActions; } toLsDiagnostics(diagnostics, document) { return diagnostics.filter(diag => diag.HasLocation).map(diag => { const start = document.positionAt(diag.Start); const end = document.positionAt(diag.Start + diag.Length); const range = ls.Range.create(start, end); let severity; switch (diag.Severity) { case 'Suggestion': severity = ls.DiagnosticSeverity.Information; break; case 'Warning': severity = ls.DiagnosticSeverity.Warning; break; default: severity = ls.DiagnosticSeverity.Error; } return ls.Diagnostic.create(range, diag.Message, severity, diag.Code); }); } async getClassifications(document) { const codeScript = this.parseDocumentV2(document); const codeBlocks = this.toArray(codeScript.Blocks); const classificationRanges = codeBlocks.map(block => { const { Classifications } = block.Service.GetClassifications(block.Start, block.Length); return this.toArray(Classifications); }); return classificationRanges.flatMap(ranges => { return ranges.map(range => { const { line, character } = document.positionAt(range.Start); const length = range.Length; const kind = range.Kind; return { line, character, length, kind }; }); }); } getAffectedBlocks(blocks, changeIntervals) { return blocks.filter(block => // a command is affected if it intersects at least on of changed ranges. block // command can be null. we're filtering all nulls in the array. ? changeIntervals.some(({ start: changeStart, end: changeEnd }) => // both intervals intersect if either the start or the end of interval A is inside interval B. block.Start >= changeStart && block.Start <= changeEnd || changeStart >= block.Start && changeStart <= block.End + 1) : false); } addClusterToSchema(document, clusterName, databases) { let clusterNameOnly = Kusto.Language.KustoFacts.GetHostName(clusterName); let cluster = this._kustoJsSchemaV2.GetCluster$1(clusterNameOnly); if (cluster) { // add databases that are not already in the cluster. databases.filter(({ name }) => !cluster.GetDatabase(name)).forEach(({ name, alternativeName }) => { const symbol = new sym.DatabaseSymbol.$ctor3(name, alternativeName || null, undefined, false); cluster = cluster.AddDatabase(symbol); }); } if (!cluster) { const databaseSymbols = databases.map(({ name, alternativeName }) => { return new sym.DatabaseSymbol.$ctor3(name, alternativeName || null, undefined, false); }); const databaseSymbolsList = new (List(sym.DatabaseSymbol).$ctor1)(databaseSymbols); cluster = new sym.ClusterSymbol.$ctor1(clusterNameOnly, databaseSymbolsList, false); } this._kustoJsSchemaV2 = this._kustoJsSchemaV2.AddOrReplaceCluster(cluster); this._script = k2.CodeScript.From$1(document.getText(), this._kustoJsSchemaV2); return Promise.resolve(); } addDatabaseToSchema(document, clusterName, databaseSchema) { let clusterHostName = Kusto.Language.KustoFacts.GetHostName(clusterName); let cluster = this._kustoJsSchemaV2.GetCluster$1(clusterHostName); if (!cluster) { cluster = new sym.ClusterSymbol.$ctor1(clusterHostName, null, false); } const databaseSymbol = KustoLanguageService.convertToDatabaseSymbol(databaseSchema); cluster = cluster.AddOrUpdateDatabase(databaseSymbol); this._kustoJsSchemaV2 = this._kustoJsSchemaV2.AddOrReplaceCluster(cluster); this._script = k2.CodeScript.From$1(document.getText(), this._kustoJsSchemaV2); return Promise.resolve(); } setSchema(schema) { this._schema = schema; // We support intellisenseV2 only if the clusterType is "Engine", even if the setting is enabled if (schema && schema.clusterType === 'Engine') { let kustoJsSchemaV2 = this.convertToKustoJsSchemaV2(schema); this._kustoJsSchemaV2 = kustoJsSchemaV2; this._script = undefined; this._parsePropertiesV2 = undefined; } // since V2 doesn't support control commands, we're initializing V1 intellisense for both cases and we'll going to use V1 intellisense for control commands. const kustoJsSchema = schema ? KustoLanguageService.convertToKustoJsSchema(schema) : undefined; this._kustoJsSchema = kustoJsSchema; this.createRulesProvider(kustoJsSchema, schema.clusterType); return Promise.resolve(); } setParameters(scalarParameters, tabularParameters) { if (this._schema.clusterType !== 'Engine') { throw new Error('setParameters requires intellisense V2 and Engine cluster'); } this._schema.globalScalarParameters = scalarParameters; this._schema.globalTabularParameters = tabularParameters; const scalarSymbols = scalarParameters.map(param => KustoLanguageService.createParameterSymbol(param)); const tabularSymbols = tabularParameters.map(param => KustoLanguageService.createTabularParameterSymbol(param)); this._kustoJsSchemaV2 = this._kustoJsSchemaV2.WithParameters(KustoLanguageService.toBridgeList([...scalarSymbols, ...tabularSymbols])); this._script = this._script?.WithGlobals(this._kustoJsSchemaV2); // Set parameters is only working with the below code. It didn't used to need this, why does it now?!? // Copy+pasted from setSchema const kustoJsSchema = KustoLanguageService.convertToKustoJsSchema(this._schema); this._kustoJsSchema = kustoJsSchema; this.createRulesProvider(kustoJsSchema, this._schema.clusterType); return Promise.resolve(undefined); } /** * A combination of normalizeSchema and setSchema * @param schema schema json as received from .show schema as json * @param clusterConnectionString cluster connection string * @param databaseInContextName name of database in context * @param globalScalarParameters * @param globalTabularParameters * @param databaseInContextAlternateName alternate name of database in context */ setSchemaFromShowSchema(schema, clusterConnectionString, databaseInContextName, globalScalarParameters, globalTabularParameters, databaseInContextAlternateName) { const normalized = this._normalizeSchema(schema, clusterConnectionString, databaseInContextName, databaseInContextAlternateName); return this.setSchema({ ...normalized, globalScalarParameters, globalTabularParameters }); } /** * Converts the result of .show schema as json to a normalized schema used by kusto language service. * @param schema result of show schema * @param clusterConnectionString cluster connection string` * @param databaseInContextName database in context name * @param databaseInContextAlternateName database in context alternate name */ _normalizeSchema(schema, clusterConnectionString, databaseInContextName, databaseInContextAlternateName) { const databases = Object.keys(schema.Databases).map(key => schema.Databases[key]).map(({ Name, Tables, ExternalTables, MaterializedViews, Functions, EntityGroups = {}, MinorVersion, MajorVersion }) => ({ name: Name, alternateName: databaseInContextAlternateName, minorVersion: MinorVersion, majorVersion: MajorVersion, entityGroups: Object.entries(EntityGroups).map(([name, members]) => ({ name, members })), tables: [].concat(...[[Tables, 'Table'], [MaterializedViews, 'MaterializedView'], [ExternalTables, 'ExternalTable']].filter(([tableContainer]) => tableContainer).map(([tableContainer, tableEntity]) => Object.values(tableContainer).map(({ Name, OrderedColumns, DocString }) => ({ name: Name, docstring: DocString, entityType: tableEntity, columns: OrderedColumns.map(({ Name, Type, DocString, CslType, Examples }) => ({ name: Name, type: CslType, docstring: DocString, examples: Examples })) })))), functions: Object.keys(Functions).map(key => Functions[key]).map(({ Name, Body, DocString, InputParameters }) => ({ name: Name, body: Body, docstring: DocString, inputParameters: InputParameters.map(inputParam => ({ name: inputParam.Name, type: inputParam.Type, cslType: inputParam.CslType, cslDefaultValue: inputParam.CslDefaultValue, columns: inputParam.Columns ? inputParam.Columns.map(col => ({ name: col.Name, type: col.Type, cslType: col.CslType })) : inputParam.Columns })) })) })); return { clusterType: 'Engine', cluster: { connectionString: clusterConnectionString, databases: databases }, database: databases.filter(db => db.name === databaseInContextName)[0] }; } /** * Converts the result of .show schema as json to a normalized schema used by kusto language service. * @param schema result of show schema * @param clusterConnectionString cluster connection string` * @param databaseInContextName database in context name * @param databaseInContextAlternateName database in context alternate name */ normalizeSchema(schema, clusterConnectionString, databaseInContextName, databaseInContextAlternateName) { return Promise.resolve(this._normalizeSchema(schema, clusterConnectionString, databaseInContextName, databaseInContextAlternateName)); } getSchema() { return Promise.resolve(this._schema); } getCommandInContext(document, cursorOffset) { return this.getCommandInContextV2(document, cursorOffset); } getCommandAndLocationInContext(document, cursorOffset) { // We are going to remove v1 intellisense. no use to keep parity. if (!document) { return Promise.resolve(null); } const script = this.parseDocumentV2(document); const block = this.getCurrentCommandV2(script, cursorOffset); if (!block) { return Promise.resolve(null); } const start = document.positionAt(block.Start); const end = document.positionAt(block.End); const location = ls.Location.create(document.uri, ls.Range.create(start, end)); const text = block.Text; return Promise.resolve({ text, location }); } getCommandInContextV2(document, cursorOffset) { if (!document) { return Promise.resolve(null); } const script = this.parseDocumentV2(document); const block = this.getCurrentCommandV2(script, cursorOffset); if (!block) { return Promise.resolve(null); } // TODO: do we need to do tricks like V1 is doing in this.getCurrentCommand? return Promise.resolve(block.Text); } /** * Return an array of commands in document. each command contains the range and text. */ getCommandsInDocument(document) { if (!document) { return Promise.resolve([]); } return this.getCommandsInDocumentV2(document); } getCommandsInDocumentV1(document) { this.parseDocumentV1(document, k.ParseMode.CommandTokensOnly); let commands = this.toArray(this._parser.Results); return Promise.resolve(commands.map(({ AbsoluteStart, AbsoluteEnd, Text }) => ({ absoluteStart: AbsoluteStart, absoluteEnd: AbsoluteEnd, text: Text }))); } toPlacementStyle(formatterPlacementStyle) { if (!formatterPlacementStyle) { return undefined; } switch (formatterPlacementStyle) { case 'None': return k2.PlacementStyle.None; case 'NewLine': return k2.PlacementStyle.NewLine; case 'Smart': return k2.PlacementStyle.Smart; default: throw new Error('Unknown PlacementStyle'); } } getFormattedCommandsInDocumentV2(document, rangeStart, rangeEnd) { const script = this.parseDocumentV2(document); const commands = this.toArray(script.Blocks).filter(command => { if (!command.Text || command.Text.trim() == '') return false; if (rangeStart == null || rangeEnd == null) return true; // calculate command end position without \r\n. let commandEnd = command.End; const commandText = command.Text; for (let i = commandText.length - 1; i >= 0; i--) { if (commandText[i] != '\r' && commandText[i] != '\n') { break; } else { commandEnd--; } } if (command.Start > rangeStart && command.Start < rangeEnd) return true; if (commandEnd > rangeStart && commandEnd < rangeEnd) return true; if (command.Start <= rangeStart && commandEnd >= rangeEnd) return true; }); if (commands.length === 0) { return { formattedCommands: [] }; } const formattedCommands = commands.map(command => { const formatterOptions = this._languageSettings.formatter; const formatter = Kusto.Language.Editor.FormattingOptions.Default.WithIndentationSize(formatterOptions?.indentationSize ?? 4).WithInsertMissingTokens(false).WithPipeOperatorStyle(this.toPlacementStyle(formatterOptions?.pipeOperatorStyle) ?? k2.PlacementStyle.Smart).WithSemicolonStyle(Kusto.Language.Editor.PlacementStyle.None).WithBrackettingStyle(k2.BrackettingStyle.Diagonal); if (rangeStart == null || rangeEnd == null || rangeStart === command.Start && rangeEnd === command.End) { const result = command.Service.GetFormattedText(formatter); return result.Text; } return command.Service.GetFormattedText(formatter).Text; }); const originalRange = this.createRange(document, commands[0].Start, commands[commands.length - 1].End); return { formattedCommands, originalRange }; } getCommandsInDocumentV2(document) { const script = this.parseDocumentV2(document); let commands = this.toArray(script.Blocks).filter(command => command.Text.trim() != ''); return Promise.resolve(commands.map(({ Start, End, Text }) => ({ absoluteStart: Start, absoluteEnd: End, text: Text }))); } getClientDirective(text) { let outParam = { v: null }; const isClientDirective = k.CslCommandParser.IsClientDirective(text, outParam); return Promise.resolve({ isClientDirective, directiveWithoutLeadingComments: outParam.v }); } getAdminCommand(text) { let outParam = { v: null }; const isAdminCommand = k.CslCommandParser.IsAdminCommand$1(text, outParam); return Promise.resolve({ isAdminCommand, adminCommandWithoutLeadingComments: outParam.v }); } findDefinition(document, position) { if (!document) { return Promise.resolve([]); } const script = this.parseDocumentV2(document); const cursorOffset = document.offsetAt(position); let currentBlock = this.getCurrentCommandV2(script, cursorOffset); if (!currentBlock) { return Promise.resolve([]); } const relatedInfo = currentBlock.Service.GetRelatedElements(document.offsetAt(position)); const relatedElements = this.toArray(relatedInfo.Elements); const definition = relatedElements[0]; if (!definition) { return Promise.resolve([]); } const start = document.positionAt(definition.Start); const end = document.positionAt(definition.End); const range = ls.Range.create(start, end); const location = ls.Location.create(document.uri, range); return Promise.resolve([location]); } findReferences(document, position) { if (!document) { return Promise.resolve([]); } const script = this.parseDocumentV2(document); const cursorOffset = document.offsetAt(position); let currentBlock = this.getCurrentCommandV2(script, cursorOffset); if (!currentBlock) { return Promise.resolve([]); } const relatedInfo = currentBlock.Service.GetRelatedElements(document.offsetAt(position)); const relatedElements = this.toArray(relatedInfo.Elements); if (!relatedElements || relatedElements.length == 0) { return Promise.resolve([]); } const references = relatedElements.map(relatedElement => { const start = document.positionAt(relatedElement.Start); const end = document.positionAt(relatedElement.End); const range = ls.Range.create(start, end); const location = ls.Location.create(document.uri, range); return location; }); return Promise.resolve(references); } getQueryParams(document, cursorOffset) { if (!document) { return Promise.resolve([]); } const parsedAndAnalyzed = this.parseAndAnalyze(document, cursorOffset); const queryParamStatements = this.toArray(parsedAndAnalyzed.Syntax.GetDescendants(Kusto.Language.Syntax.QueryParametersStatement)); if (!queryParamStatements || queryParamStatements.length == 0) { return Promise.resolve([]); } const queryParams = []; queryParamStatements.forEach(paramStatement => { paramStatement.WalkElements(el => el.ReferencedSymbol && el.ReferencedSymbol.Type ? queryParams.push({ name: el.ReferencedSymbol.Name, type: el.ReferencedSymbol.Type.Name }) : undefined); }); return Promise.resolve(queryParams); } getRenderInfo(document, cursorOffset) { const parsedAndAnalyzed = this.parseAndAnalyze(document, cursorOffset); if (!parsedAndAnalyzed) { return Promise.resolve(undefined); } const renderStatements = this.toArray(parsedAndAnalyzed.Syntax.GetDescendants(Kusto.Language.Syntax.RenderOperator)); if (!renderStatements || renderStatements.length === 0) { return Promise.resolve(undefined); } // assuming a single render statement const renderStatement = renderStatements[0]; // Start and end relative to block start. const startOffset = renderStatement.TextStart; const endOffset = renderStatement.End; const visualization = renderStatement.ChartType.ValueText; const withClause = renderStatement.WithClause; if (!withClause) { const info = { options: { visualization }, location: { startOffset, endOffset } }; return Promise.resolve(info); } const properties = this.toArray(withClause.Properties); const props = properties.reduce((prev, property) => { const name = property.Element$1.Name.SimpleName; switch (name) { case 'xcolumn': const value = property.Element$1.Expression.ReferencedSymbol.Name; prev[name] = value; break; case 'ycolumns': case 'anomalycolumns': const nameNodes = this.toArray(property.Element$1.Expression.Names); const values = nameNodes.map(nameNode => nameNode.Element$1.SimpleName); prev[name] = values; break; case 'ymin': case 'ymax': const numericVal = parseFloat(property.Element$1.Expression.ConstantValue); prev[name] = numericVal; break; case 'title': case 'xtitle': case 'ytitle': case 'visualization': case 'series': const strVal = property.Element$1.Expression.ConstantValue; prev[name] = strVal; break; case 'xaxis': case 'yaxis': const scale = property.Element$1.Expression.ConstantValue; prev[name] = scale; break; case 'legend': const legend = property.Element$1.Expression.ConstantValue; prev[name] = legend; break; case 'ysplit': const split = property.Element$1.Expression.ConstantValue; prev[name] = split; break; case 'accumulate': const accumulate = property.Element$1.Expression.ConstantValue; prev[name] = accumulate; break; case 'kind': const val = property.Element$1.Expression.ConstantValue; prev[name] = val; break; default: assertNever(name); } return prev; }, {}); const renderOptions = { visualization, ...props }; const renderInfo = { options: renderOptions, location: { startOffset, endOffset } }; return Promise.resolve(renderInfo); } getReferencedSymbols(document, offset) { const parsedAndAnalyzed = this.parseAndAnalyze(document, offset); if (!parsedAndAnalyzed) { Promise.resolve([]); } // We take all referenced symbols in the query const referencedSymbols = this.toArray(parsedAndAnalyzed.Syntax.GetDescendants(Kusto.Language.Syntax.Expression)).filter(expression => expression.ReferencedSymbol !== null).map(x => x.ReferencedSymbol); const result = referencedSymbols.map(sym => ({ name: sym.Name, kind: symbolKindToName[sym.Kind] ?? `${sym.Kind}`, display: `${sym.Name} (${sym.AlternateName})` })); return Promise.resolve(result); } getReferencedGlobalParams(document, cursorOffset) { const parsedAndAnalyzed = this.parseAndAnalyze(document, cursorOffset); if (!parsedAndAnalyzed) { Promise.resolve([]); } // We take the ambient parameters const ambientParameters = this.toArray(this._kustoJsSchemaV2.Parameters); // We take all referenced symbols in the query const referencedSymbols = this.toArray(parsedAndAnalyzed.Syntax.GetDescendants(Kusto.Language.Syntax.Expression)).filter(expression => expression.ReferencedSymbol !== null).map(x => x.ReferencedSymbol); // The Intersection between them is the ambient parameters that are used in the query. // Note: Ideally we would use Set here (or at least array.Include), but were' compiling down to es2015. const intersection = referencedSymbols.filter(referencedSymbol => ambientParameters.filter(ambientParameter => ambientParameter === referencedSymbol).length > 0); const result = intersection.map(param => ({ name: param.Name, type: param.Type.Name })); return Promise.resolve(result); } getGlobalParams(document) { const params = this.toArray(this._kustoJsSchemaV2.Parameters); const result = params.map(param => ({ name: param.Name, type: param.Type.Name })); return Promise.resolve(result); } doRename(document, position, newName) { if (!document) { return Promise.resolve(undefined); } const script = this.parseDocumentV2(document); const cursorOffset = document.offsetAt(position); let currentBLock = this.getCurrentCommandV2(script, cursorOffset); if (!currentBLock) { return Promise.resolve(undefined); } const relatedInfo = currentBLock.Service.GetRelatedElements(document.offsetAt(position)); const relatedElements = this.toArray(relatedInfo.Elements); const declarations = relatedElements.filter(e => e.Kind == k2.RelatedElementKind.Declaration); // A declaration must be one of the elements if (!declarations || declarations.length == 0) { return Promise.resolve(undefined); } const edits = relatedElements.map(edit => { const start = document.positionAt(edit.Start); const end = document.positionAt(edit.End); const range = ls.Range.create(start, end); return ls.TextEdit.replace(range, newName); }); // create a workspace edit const workspaceEdit = { changes: { [document.uri]: edits } }; return Promise.resolve(workspaceEdit); } doHover(document, position) { if (!document) { return Promise.resolve(undefined); } const script = this.parseDocumentV2(document); const cursorOffset = document.offsetAt(position); let currentBLock = this.getCurrentCommandV2(script, cursorOffset); if (!currentBLock) { return Promise.resolve(undefined); } const isSupported = currentBLock.Service.IsFeatureSupported(k2.CodeServiceFeatures.QuickInfo, cursorOffset); if (!isSupported) { return Promise.resolve(undefined); } const quickInfo = currentBLock.Service.GetQuickInfo(cursorOffset); if (!quickInfo || !quickInfo.Items) { return Promise.resolve(undefined); } let items = this.toArray(quickInfo.Items); if (!items) { return Promise.resolve(undefined); } // Errors, Warnings and Suggestions are already shown in getDiagnostics. we don't want them in doHover. items = items.filter(item => item.Kind !== k2.QuickInfoKind.Error && item.Kind !== k2.QuickInfoKind.Suggestion && item.Kind !== k2.QuickInfoKind