UNPKG

@nteract/monaco-editor

Version:

A React component for the monaco editor, tailored for nteract

311 lines 15.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.completionProvider = void 0; const monaco = __importStar(require("monaco-editor/esm/vs/editor/editor.api")); const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); const messaging_1 = require("@nteract/messaging"); const editor_base_1 = require("../editor-base"); /** * Jupyter to Monaco completion item kinds. */ const unknownJupyterKind = "<unknown>"; const jupyterToMonacoCompletionItemKind = { [unknownJupyterKind]: monaco.languages.CompletionItemKind.Field, class: monaco.languages.CompletionItemKind.Class, function: monaco.languages.CompletionItemKind.Function, keyword: monaco.languages.CompletionItemKind.Keyword, instance: monaco.languages.CompletionItemKind.Variable, statement: monaco.languages.CompletionItemKind.Variable }; /** * Completion item provider. */ class CompletionItemProvider { constructor() { this.regexWhitespace = new RegExp(/^\s*$/); } /** * Set Channels of Jupyter kernel. */ setChannels(channels) { this.channels = channels; } /** * Whether provider is connected to Jupyter kernel. */ get isConnectedToKernel() { return !!this.channels; } /** * Additional characters to trigger completion other than Ctrl+Space. * We do not need any additional characters to trigger completion as the Monaco editor * by default triggers completion as the user types non-whitespace characters. */ get triggerCharacters() { return []; } /** * Get list of completion items at position of cursor. */ async provideCompletionItems(model, position) { // Convert to zero-based index let cursorPos = model.getOffsetAt(position); const code = model.getValue(); cursorPos = editor_base_1.js_idx_to_char_idx(cursorPos, code); // Get completions from Jupyter kernel if its Channels is connected let items = []; if (this.channels) { try { const message = editor_base_1.completionRequest(code, cursorPos); items = await this.codeCompleteObservable(this.channels, message, model).toPromise(); } catch (error) { // tslint:disable-next-line console.error(error); } } return Promise.resolve({ suggestions: items, incomplete: false }); } /** * Get list of completion items from Jupyter kernel. */ codeCompleteObservable(channels, message, model) { // Process completion response const completion$ = channels.pipe(messaging_1.childOf(message), messaging_1.ofMessageType("complete_reply"), operators_1.map((entry) => entry.content), operators_1.first(), operators_1.map((results) => this.adaptToMonacoCompletions(results, model))); // Subscribe and send completion request message return rxjs_1.Observable.create((observer) => { const subscription = completion$.subscribe(observer); channels.next(message); return subscription; }); } /** * Converts Jupyter completion result to list of Monaco completion items. */ adaptToMonacoCompletions(results, model) { var _a; // Get completion list from Jupyter let completionItems = (_a = results.matches) !== null && _a !== void 0 ? _a : []; if (results.metadata && results.metadata._jupyter_types_experimental) { completionItems = results.metadata._jupyter_types_experimental; } // Retrieve the text that is currently typed out which is used to determine completion const startPos = model.getPositionAt(results.cursor_start); const endPos = model.getPositionAt(results.cursor_end); const typedText = model.getValueInRange({ startLineNumber: startPos.lineNumber, startColumn: startPos.column, endLineNumber: endPos.lineNumber, endColumn: endPos.column }); const typedMagicPrefix = this.getMagicPrefix(typedText); const isWhitespaceFromCellStart = this.isWhitespaceFromCellStart(model, startPos); return completionItems .map((completionItem, index) => { let completionText, completionKind; if (typeof completionItem === "string") { completionText = completionItem; completionKind = unknownJupyterKind; } else { completionText = completionItem.text; completionKind = completionItem.type; } completionText = this.sanitizeText(completionText, typedText); let item = { kind: this.adaptToMonacoCompletionItemKind(completionKind), label: completionText, insertText: this.getInsertText(completionText, typedText, typedMagicPrefix), filterText: this.getFilterText(completionText, typedText), sortText: this.getSortText(index) }; const completionMagicPrefix = this.getMagicPrefix(completionText); if (completionMagicPrefix === "%%") { if (!isWhitespaceFromCellStart || startPos.column !== 1) { // Cell magic is not valid if there are non-whitespace from cell start to current position // or if it is not on the first column of a line. item = undefined; } } else if (completionMagicPrefix === "%") { if (startPos.column !== 1) { // Line magic is not valid if it is not on the first column of a line. item = undefined; } } return item; }) .filter((item) => item !== undefined); } /** * Get magic prefix from text. */ getMagicPrefix(text) { if (text.startsWith("%")) { return text.startsWith("%%") ? "%%" : "%"; } else { return undefined; } } /** * Whether all characters from cell start position up to current start position are whitespace. */ isWhitespaceFromCellStart(model, startPos) { const beforeText = model.getValueInRange({ startLineNumber: 1, startColumn: 1, endLineNumber: startPos.lineNumber, endColumn: startPos.column }); return this.regexWhitespace.test(beforeText); } /** * Converts Jupyter completion item kind to Monaco completion item kind. */ adaptToMonacoCompletionItemKind(kind) { const result = jupyterToMonacoCompletionItemKind[kind]; return result ? result : jupyterToMonacoCompletionItemKind[unknownJupyterKind]; } /** * Removes problematic prefixes based on the typed text. * * Instead of showing "some/path" we should only show "path". For paths with white space, the kernel returns * ""some/path with spaces"" which we want to change to ""path with spaces"". * * Additionally, typing "[]." should not suggest ".append" since this results in "[]..append". */ sanitizeText(completionText, typedText) { // Assumption: if the current context contains a "/" then we're currently typing a path const isPathCompletion = typedText.includes("/"); if (isPathCompletion) { // If we have whitespace within a path, the completion for it is a string wrapped in double quotes // We should return only the last part of the path, wrapped in double quotes const completionIsPathWithWhitespace = completionText.startsWith('"') && completionText.endsWith('"') && completionText.length > 2; // sanity check: not empty string if (completionIsPathWithWhitespace && completionText.substr(1).startsWith(typedText)) { // sanity check: the context is part of the suggested path const toRemove = typedText.substr(0, typedText.lastIndexOf("/") + 1); return `"${completionText.substr(toRemove.length + 1)}`; } // Otherwise, display the most specific item in the path if (completionText.startsWith(typedText)) { // sanity check: the context is part of the suggested path const toRemove = typedText.substr(0, typedText.lastIndexOf("/") + 1); return completionText.substr(toRemove.length); } } // Handle "." after paths, since those might contain "." as well. Note that we deal with this somewhat // generically, but also take a somewhat conservative approach by ensuring that the completion starts with the // current context to ensure that we aren't applying this when we shouldn't const isMemberCompletion = typedText.endsWith("."); if (isMemberCompletion && completionText.startsWith(typedText)) { const toRemove = typedText.substr(0, typedText.lastIndexOf(".") + 1); return completionText.substr(toRemove.length); } // Handle taking only the suggestion content after the last dot. There are cases that a kernel when given // "suggestion1.itemA" text and typing "." that it will suggest the full path of "suggestion.itemA.itemB" instead of // just "itemB". The logic below handles these cases. This also handles the case where given "suggestion1.itemA.it" // text and typing "e" will suggest the full path of "suggestion.itemA.itemB" instead of "itemB". // This logic also covers that scenario. const index = completionText.lastIndexOf("."); if (index > -1 && index < completionText.length - 1) { return completionText.substring(index + 1); } return completionText; } /** * Remove magics all % characters as Monaco doesn't like them for the filtering text. * Without this, completion won't show magics match items. * * Also remove quotes from the filter of a path wrapped in quotes to make sure we have * a smooth auto-complete experience. */ getFilterText(completionText, typedText) { const isPathCompletion = typedText.includes("/"); if (isPathCompletion) { const completionIsPathWithWhitespace = completionText.startsWith('"') && completionText.endsWith('"') && completionText.length > 2; // sanity check: not empty string if (completionIsPathWithWhitespace && completionText.substr(1).startsWith(typedText)) { // sanity check: the context is part of the suggested path return completionText.substr(1, completionText.length - 1); } } return completionText.replace(/%/g, ""); } /** * Get insertion text handling what to insert for the magics case depending on what * has already been typed. Also handles an edge case for file paths with "." in the name. */ getInsertText(completionText, typedText, typedMagicPrefix) { // There is an edge case for folders that have "." in the name. The default range for replacements is determined // by the "current word" but that doesn't allow "." in the string, so if you autocomplete "some." for a string // like "some.folder.name" you end up with "some.some.folder.name". const isPathCompletion = typedText.includes("/"); const isPathWithPeriodInName = isPathCompletion && completionText.includes(".") && typedText.includes("."); if (isPathWithPeriodInName) { // The text in our sanitization step has already been filtered to only include the most specific path but // our context includes the full thing, so we need to determine the substring in the most specific path. // This is then used to figure out what we should actually insert. // example 1: context = "a/path/to/some." and text = "some.folder.name" should produce "folder.name" // example 2: context = "a/path/to/some.fo" and text = "some.folder.name" should still produce "folder.name" const completionContext = typedText.substr(typedText.lastIndexOf("/") + 1); if (completionText.startsWith(completionContext)) { // sanity check: the paths match return completionText.substr(completionContext.lastIndexOf(".") + 1); } } // If the typed text starts with magics % indicator, we need to inserts the delta between what the user // typed versus what is recommended by the completion. Without this, there will be extra % insertions. // Example: // User types %%p then suggestion list will recommend %%python, if we now commit the item then the // final text in the editor becomes %%p%%python instead of %%python. This is why the tracking code // below is needed. This behavior is only specific to the magics % indicators as Monaco does not // handle % characters in their completion list well. return typedMagicPrefix ? completionText.replace(typedMagicPrefix, "") : completionText; } /** * Maps numbers to strings, such that if a>b numerically, f(a)>f(b) lexicograhically. * 1 -> "za", 26 -> "zz", 27 -> "zza", 28 -> "zzb", 52 -> "zzz", 53 ->"zzza" * @param order Number to be converted to a sorting-string. order >= 0. * @returns A string representing the order. */ getSortText(order) { order++; const numCharacters = 26; // "z" - "a" + 1; const div = Math.floor(order / numCharacters); let sortText = "z"; for (let i = 0; i < div; i++) { sortText += "z"; } const remainder = order % numCharacters; if (remainder > 0) { sortText += String.fromCharCode(96 + remainder); } return sortText; } } const completionProvider = new CompletionItemProvider(); exports.completionProvider = completionProvider; //# sourceMappingURL=completionItemProvider.js.map