@kusto/monaco-kusto
Version:
CSL, KQL plugin for the Monaco Editor
590 lines (570 loc) • 20 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 monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { editor } from 'monaco-editor/esm/vs/editor/editor.api';
import { T as Token, L as LANGUAGE_ID } from './globals-ab5bacd5.js';
import { g as getCslTypeNameFromClrType, a as getCallName, b as getExpression, c as getInputParametersAsCslString, d as getEntityDataTypeFromCslType } from './schema-c46b688b.js';
export { s as showSchema } from './schema-c46b688b.js';
function getCurrentCommandRange(editor, cursorPosition) {
const zeroBasedCursorLineNumber = cursorPosition.lineNumber - 1;
const lines = editor.getModel().getLinesContent();
let commandOrdinal = 0;
const linesWithCommandOrdinal = [];
for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) {
let isEmptyLine = lines[lineNumber].trim() === '';
if (isEmptyLine) {
// increase commandCounter - we'll be starting a new command.
linesWithCommandOrdinal.push({
commandOrdinal: commandOrdinal++,
lineNumber
});
} else {
linesWithCommandOrdinal.push({
commandOrdinal: commandOrdinal,
lineNumber
});
}
// No need to keep scanning if we're past our line and we've seen an empty line.
if (lineNumber > zeroBasedCursorLineNumber && commandOrdinal > linesWithCommandOrdinal[zeroBasedCursorLineNumber].commandOrdinal) {
break;
}
}
const currentCommandOrdinal = linesWithCommandOrdinal[zeroBasedCursorLineNumber].commandOrdinal;
const currentCommandLines = linesWithCommandOrdinal.filter(line => line.commandOrdinal === currentCommandOrdinal);
const currentCommandStartLine = currentCommandLines[0].lineNumber + 1;
const currentCommandEndLine = currentCommandLines[currentCommandLines.length - 1].lineNumber + 1;
// End-column of 1 means no characters will be highlighted - since columns are 1-based in monaco apis.
// Start-column of 1 and End column of 2 means 1st character is selected.
// Thus if a line has n column and we need to provide n+1 so that the entire line will be highlighted.
const commandEndColumn = lines[currentCommandEndLine - 1].length + 1;
return new monaco.Range(currentCommandStartLine, 1, currentCommandEndLine, commandEndColumn);
}
/**
* Extending ICode editor to contain additional kusto-specific methods.
* note that the extend method needs to be called at least once to take affect, otherwise this here code is useless.
*/
function extend(editor) {
const proto = Object.getPrototypeOf(editor);
proto.getCurrentCommandRange = function (cursorPosition) {
getCurrentCommandRange(this, cursorPosition);
};
}
/**
* Highlights the command that surround cursor location
*/
class KustoCommandHighlighter {
static ID = 'editor.contrib.kustoCommandHighlighter';
static CURRENT_COMMAND_HIGHLIGHT = {
className: 'selectionHighlight'
};
disposables = [];
decorations = [];
/**
* Register to cursor movement and selection events.
* @param editor monaco editor instance
*/
constructor(editor) {
this.editor = editor;
// Note that selection update is triggered not only for selection changes, but also just when no text selection is occurring and cursor just moves around.
// This case is counted as a 0-length selection starting and ending on the cursor position.
this.editor.onDidChangeCursorSelection(changeEvent => {
if (this.editor.getModel().getLanguageId() !== 'kusto') {
return;
}
this.highlightCommandUnderCursor(changeEvent);
});
}
getId() {
return KustoCommandHighlighter.ID;
}
dispose() {
this.disposables.forEach(d => d.dispose());
}
highlightCommandUnderCursor(changeEvent) {
// Looks like the user selected a bunch of text. we don't want to highlight the entire command in this case - since highlighting
// the text is more helpful.
if (!changeEvent.selection.isEmpty()) {
this.decorations = this.editor.deltaDecorations(this.decorations, []);
return;
}
const commandRange = getCurrentCommandRange(this.editor, changeEvent.selection.getStartPosition());
const decorations = [{
range: commandRange,
options: KustoCommandHighlighter.CURRENT_COMMAND_HIGHLIGHT
}];
this.decorations = this.editor.deltaDecorations(this.decorations, decorations);
}
}
class KustoCommandFormatter {
actionAdded = false;
constructor(editor) {
this.editor = editor;
// selection also represents no selection - for example the event gets triggered when moving cursor from point
// a to point b. in the case start position will equal end position.
editor.onDidChangeCursorSelection(changeEvent => {
if (this.editor.getModel().getLanguageId() !== 'kusto') {
return;
}
// Theoretically you would expect this code to run only once in onDidCreateEditor.
// Turns out that onDidCreateEditor is fired before the IStandaloneEditor is completely created (it is emitted by
// the super ctor before the child ctor was able to fully run).
// Thus we don't have a key binding provided yet when onDidCreateEditor is run, which is essential to call addAction.
// By adding the action here in onDidChangeCursorSelection we're making sure that the editor has a key binding provider,
// and we just need to make sure that this happens only once.
if (!this.actionAdded) {
editor.addAction({
id: 'editor.action.kusto.formatCurrentCommand',
label: 'Format Command Under Cursor',
keybindings: [monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyK, monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyF)],
run: ed => {
editor.trigger('KustoCommandFormatter', 'editor.action.formatSelection', null);
},
contextMenuGroupId: '1_modification'
});
this.actionAdded = true;
}
});
}
}
function dateStringWrapper(editor) {
editor.onDidPaste(event => {
const {
range
} = event;
if (!range) return;
const model = editor.getModel();
const pasted = model.getValueInRange(range);
if (!isBareIsoDate(pasted)) return;
const wrapped = `datetime(${pasted})`;
const edit = {
range,
text: wrapped,
forceMoveMarkers: true
};
const cursorStateComputer = () => {
const startOffset = model.getOffsetAt(range.getStartPosition());
const endPos = model.getPositionAt(startOffset + wrapped.length);
return [new monaco.Selection(endPos.lineNumber, endPos.column, endPos.lineNumber, endPos.column)];
};
editor.executeEdits('paste-date', [edit], cursorStateComputer);
});
}
function isBareIsoDate(text) {
const s = text.trim();
return /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}Z)?$/.test(s);
}
let ThemeName = /*#__PURE__*/function (ThemeName) {
ThemeName["light"] = "kusto-light";
ThemeName["dark"] = "kusto-dark";
return ThemeName;
}({});
const colors = {
white: '#DCDCDC',
lightGoldenrodYellow: '#FAFAD2',
softGold: '#D7BA7D',
paleChestnut: '#D69D85',
paleVioletRed: '#DB7093',
firebrick: '#B22222',
orangeRed: '#CE3600',
mediumVioletRed: '#C71585',
magenta: '#FF00FF',
// for debugging
darkOrchid: '#9932CC',
darkViolet: '#9400D3',
midnightBlue: '#191970',
blue: '#0000FF',
blueSapphire: '#004E8C',
tealBlue: '#2B91AF',
skyBlue: '#569CD6',
lightSkyBlue: '#92CAF4',
mediumTurquoise: '#4EC9B0',
oliveDrab: '#608B4E',
green: '#008000',
jetBlack: '#1B1A19',
black: '#000000'
};
const light = {
base: 'vs',
inherit: true,
rules: [{
token: '',
foreground: colors.black
}, {
token: Token.PlainText,
foreground: colors.black
}, {
token: Token.Comment,
foreground: colors.green
}, {
token: Token.Punctuation,
foreground: colors.black
}, {
token: Token.Directive,
foreground: colors.darkViolet
}, {
token: Token.Literal,
foreground: colors.black
}, {
token: Token.StringLiteral,
foreground: colors.firebrick
}, {
token: Token.Type,
foreground: colors.blue
}, {
token: Token.Column,
foreground: colors.mediumVioletRed
}, {
token: Token.Table,
foreground: colors.darkOrchid
}, {
token: Token.Database,
foreground: colors.darkOrchid
}, {
token: Token.Function,
foreground: colors.blue
}, {
token: Token.Parameter,
foreground: colors.midnightBlue
}, {
token: Token.Variable,
foreground: colors.midnightBlue
}, {
token: Token.Identifier,
foreground: colors.black
}, {
token: Token.ClientParameter,
foreground: colors.tealBlue
}, {
token: Token.QueryParameter,
foreground: colors.tealBlue
}, {
token: Token.ScalarParameter,
foreground: colors.blue
}, {
token: Token.MathOperator,
foreground: colors.black
}, {
token: Token.QueryOperator,
foreground: colors.orangeRed
}, {
token: Token.Command,
foreground: colors.blue
}, {
token: Token.Keyword,
foreground: colors.blue
}, {
token: Token.MaterializedView,
foreground: colors.darkOrchid
}, {
token: Token.SchemaMember,
foreground: colors.black
}, {
token: Token.SignatureParameter,
foreground: colors.black
}, {
token: Token.Option,
foreground: colors.black
}],
colors: {}
};
const dark = {
base: 'vs-dark',
inherit: true,
rules: [{
token: '',
foreground: colors.white
}, {
token: Token.PlainText,
foreground: colors.white
}, {
token: Token.Comment,
foreground: colors.oliveDrab
}, {
token: Token.Punctuation,
foreground: colors.white
}, {
token: Token.Directive,
foreground: colors.lightGoldenrodYellow
}, {
token: Token.Literal,
foreground: colors.white
}, {
token: Token.StringLiteral,
foreground: colors.paleChestnut
}, {
token: Token.Type,
foreground: colors.skyBlue
}, {
token: Token.Column,
foreground: colors.paleVioletRed
}, {
token: Token.Table,
foreground: colors.softGold
}, {
token: Token.Database,
foreground: colors.softGold
}, {
token: Token.Function,
foreground: colors.skyBlue
}, {
token: Token.Parameter,
foreground: colors.lightSkyBlue
}, {
token: Token.Variable,
foreground: colors.lightSkyBlue
}, {
token: Token.Identifier,
foreground: colors.white
}, {
token: Token.ClientParameter,
foreground: colors.tealBlue
}, {
token: Token.QueryParameter,
foreground: colors.tealBlue
}, {
token: Token.ScalarParameter,
foreground: colors.skyBlue
}, {
token: Token.MathOperator,
foreground: colors.white
}, {
token: Token.QueryOperator,
foreground: colors.mediumTurquoise
}, {
token: Token.Command,
foreground: colors.skyBlue
}, {
token: Token.Keyword,
foreground: colors.skyBlue
}, {
token: Token.MaterializedView,
foreground: colors.softGold
}, {
token: Token.SchemaMember,
foreground: colors.white
}, {
token: Token.SignatureParameter,
foreground: colors.white
}, {
token: Token.Option,
foreground: colors.white
}],
colors: {
'editor.background': colors.jetBlack,
'editorSuggestWidget.selectedBackground': colors.blueSapphire
}
};
const themes = [{
name: ThemeName.light,
data: light
}, {
name: ThemeName.dark,
data: dark
}];
function getRangeHtml(model, range) {
const {
startLineNumber,
endLineNumber,
endColumn
} = range;
const isLastLineEmpty = endColumn === 1;
const actualLastLine = isLastLineEmpty ? endLineNumber - 1 : endLineNumber;
const totalLines = actualLastLine - startLineNumber + 1;
const colorizedLines = new Array(totalLines).fill(undefined).map((_, index) => editor.colorizeModelLine(model, startLineNumber + index));
return colorizedLines.join('<br/>');
}
/**
* Registers a Kusto-specific action to close the IntelliSense suggestions popup.
*
* Note:
* We register the action on the first cursor movement, not on editor creation,
* because Monaco fires 'onDidCreateEditor' before the keybinding service is fully initialized.
* Waiting for a cursor event guarantees that the editor is fully ready
* and allows safe registration of actions with keybindings.
*/
class CaseInvertor {
actionsRegistered = false;
constructor(editor) {
this.editor = editor;
this.ctrlKeyMod = this.isMac() ? monaco.KeyMod.WinCtrl : monaco.KeyMod.CtrlCmd;
this.editor.onDidChangeCursorSelection(() => {
if (this.editor.getModel()?.getLanguageId() !== 'kusto') {
return;
}
if (!this.actionsRegistered) {
this.registerUpperCaseHandler();
this.registerLowerCaseHandler();
this.actionsRegistered = true;
}
});
}
registerUpperCaseHandler() {
this.editor.addAction({
id: 'kusto.toUpperCase',
label: 'To Upper Case',
keybindings: [this.ctrlKeyMod | monaco.KeyMod.Shift | monaco.KeyCode.KeyU],
run: editor => {
const selectedRange = editor.getSelection();
const selectedText = editor.getModel().getValueInRange(selectedRange);
const upperCaseText = selectedText.toUpperCase();
editor.executeEdits('toUpperCase', [{
range: selectedRange,
text: upperCaseText
}]);
}
});
}
registerLowerCaseHandler() {
this.editor.addAction({
id: 'kusto.toLowerCase',
label: 'To Lower Case',
keybindings: [this.ctrlKeyMod | monaco.KeyMod.Shift | monaco.KeyCode.KeyL],
run: editor => {
const selectedRange = editor.getSelection();
const selectedText = editor.getModel().getValueInRange(selectedRange);
const lowerCaseText = selectedText.toLowerCase();
editor.executeEdits('toLowerCase', [{
range: selectedRange,
text: lowerCaseText
}]);
}
});
}
isMac() {
const uaData = navigator.userAgentData;
return uaData ? uaData.platform === 'macOS' : /Mac|iPod|iPhone|iPad/.test(navigator.platform);
}
}
// --- Kusto configuration and defaults ---------
class LanguageServiceDefaultsImpl {
_onDidChange = new monaco.Emitter();
// in milliseconds. For example - this is 2 minutes 2 * 60 * 1000
constructor(languageSettings) {
this.setLanguageSettings(languageSettings);
// default to never kill worker when idle.
// reason: when killing worker - schema gets lost. We transmit the schema back to main process when killing
// the worker, but in some extreme cases web worker runs out of memory while stringifying the schema.
// This stems from the fact that web workers have much more limited memory that the main process.
// An alternative solution (not currently implemented) is to just save the schema in the main process whenever calling
// setSchema. That way we don't need to stringify the schema on the worker side when killing the web worker.
this._workerMaxIdleTime = 0;
}
get onDidChange() {
return this._onDidChange.event;
}
get languageSettings() {
return this._languageSettings;
}
setLanguageSettings(options) {
this._languageSettings = options || Object.create(null);
this._onDidChange.fire(this);
}
setMaximumWorkerIdleTime(value) {
// doesn't fire an event since no
// worker restart is required here
this._workerMaxIdleTime = value;
}
getWorkerMaxIdleTime() {
return this._workerMaxIdleTime;
}
}
const defaultLanguageSettings = {
includeControlCommands: true,
newlineAfterPipe: true,
openSuggestionDialogAfterPreviousSuggestionAccepted: true,
enableHover: true,
formatter: {
indentationSize: 4,
pipeOperatorStyle: 'Smart'
},
syntaxErrorAsMarkDown: {
enableSyntaxErrorAsMarkDown: false
},
enableQueryWarnings: false,
enableQuerySuggestions: false,
disabledDiagnosticCodes: [],
quickFixCodeActions: ['Change to', 'FixAll'],
enableQuickFixes: false,
completionOptions: {
includeExtendedSyntax: false
}
};
function getKustoWorker() {
return new Promise((resolve, reject) => {
withMode(mode => {
mode.getKustoWorker().then(resolve, reject);
});
});
}
function withMode(callback) {
return import('./kustoMode.js').then(callback);
}
const kustoDefaults = new LanguageServiceDefaultsImpl(defaultLanguageSettings);
monaco.languages.onLanguage('kusto', async () => {
await withMode(mode => mode.setupMode(kustoDefaults, monaco));
});
monaco.languages.register({
id: LANGUAGE_ID,
extensions: ['.csl', '.kql']
});
themes.forEach(({
name,
data
}) => monaco.editor.defineTheme(name, data));
// Initialize kusto specific language features that don't currently have a natural way to extend using existing apis.
// Most other language features are initialized in kustoMode.ts
monaco.editor.onDidCreateEditor(editor => {
if (window.MonacoEnvironment?.globalAPI) {
// hook up extension methods to editor.
extend(editor);
}
// TODO: asked if there's a cleaner way to register an editor contribution. looks like monaco has an internal contribution registrar but it's no exposed in the API.
// https://stackoverflow.com/questions/46700245/how-to-add-an-ieditorcontribution-to-monaco-editor
new KustoCommandHighlighter(editor);
if (isStandaloneCodeEditor(editor)) {
new KustoCommandFormatter(editor);
new CaseInvertor(editor);
}
triggerSuggestDialogWhenCompletionItemSelected(editor);
dateStringWrapper(editor);
});
function triggerSuggestDialogWhenCompletionItemSelected(editor) {
editor.onDidChangeCursorSelection(event => {
// checking the condition inside the event makes sure we will stay up to date when kusto configuration changes at runtime.
if (kustoDefaults && kustoDefaults.languageSettings && kustoDefaults.languageSettings.openSuggestionDialogAfterPreviousSuggestionAccepted) {
var didAcceptSuggestion = event.source === 'snippet' && event.reason === monaco.editor.CursorChangeReason.NotSet;
// If the word at the current position is not null - meaning we did not add a space after completion.
// In this case we don't want to activate the eager mode, since it will display the current selected word..
if (!didAcceptSuggestion || editor.getModel().getWordAtPosition(event.selection.getPosition()) !== null) {
return;
}
event.selection;
// OK so now we in a situation where we know a suggestion was selected, and we want to trigger another one.
// the only problem is that the suggestion widget itself listens to this same event in order to know it needs to close.
// The only problem is that we're ahead in line, so we're triggering a suggest operation that will be shut down once
// the next callback is called. This is why we're waiting here - to let all the callbacks run synchronously and be
// the 'last' subscriber to run. Granted this is hacky, but until monaco provides a specific event for suggestions,
// this is the best we have.
setTimeout(() => editor.trigger('monaco-kusto', 'editor.action.triggerSuggest', {}), 10);
}
});
}
function isStandaloneCodeEditor(editor) {
return editor.addAction !== undefined;
}
const globalApi = {
getCslTypeNameFromClrType,
getCallName,
getExpression,
getInputParametersAsCslString,
getEntityDataTypeFromCslType,
kustoDefaults,
getKustoWorker,
getRangeHtml
};
monaco.languages.kusto = globalApi;
export { getCallName, getCslTypeNameFromClrType, getEntityDataTypeFromCslType, getExpression, getInputParametersAsCslString, getKustoWorker, getRangeHtml, kustoDefaults };