@kusto/monaco-kusto
Version:
CSL, KQL plugin for the Monaco Editor
1,185 lines (1,146 loc) • 83.5 kB
JavaScript
/*!-----------------------------------------------------------------------------
* 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