mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 19:01:28 +00:00
Compare commits
8 Commits
fixed-pric
...
replace-co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77f895d343 | ||
|
|
3bc2701356 | ||
|
|
35dbaeea96 | ||
|
|
18745a9ae6 | ||
|
|
5be6f982f9 | ||
|
|
4fc9393b76 | ||
|
|
ee51e873b8 | ||
|
|
206a8ef93b |
464
src/Explorer/Notebook/MonacoEditor/MonacoEditor.tsx
Normal file
464
src/Explorer/Notebook/MonacoEditor/MonacoEditor.tsx
Normal file
@@ -0,0 +1,464 @@
|
|||||||
|
import { Channels } from "@nteract/messaging";
|
||||||
|
import * as monaco from "./monaco";
|
||||||
|
import * as React from "react";
|
||||||
|
import { completionProvider } from "./completions/completionItemProvider";
|
||||||
|
import { AppState, ContentRef } from "@nteract/core";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import "./styles.css";
|
||||||
|
import { LightThemeName, HCLightThemeName, DarkThemeName } from "./theme";
|
||||||
|
// import { logger } from "src/common/localLogger";
|
||||||
|
import { getCellMonacoLanguage } from "./selectors";
|
||||||
|
// import { DocumentUri } from "./documentUri";
|
||||||
|
|
||||||
|
export type IModelContentChangedEvent = monaco.editor.IModelContentChangedEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial props for Monaco received from agnostic component
|
||||||
|
*/
|
||||||
|
export interface IMonacoProps {
|
||||||
|
id: string;
|
||||||
|
contentRef: ContentRef;
|
||||||
|
modelUri?: monaco.Uri;
|
||||||
|
theme: monaco.editor.IStandaloneThemeData | monaco.editor.BuiltinTheme | string;
|
||||||
|
cellLanguageOverride?: string;
|
||||||
|
notebookLanguageOverride?: string;
|
||||||
|
readOnly?: boolean;
|
||||||
|
channels: Channels | undefined;
|
||||||
|
enableCompletion: boolean;
|
||||||
|
shouldRegisterDefaultCompletion?: boolean;
|
||||||
|
onChange: (value: string, event?: unknown) => void;
|
||||||
|
onFocusChange: (focus: boolean) => void;
|
||||||
|
onCursorPositionChange?: (selection: monaco.ISelection | null) => void;
|
||||||
|
onRegisterCompletionProvider?: (languageId: string) => void;
|
||||||
|
value: string;
|
||||||
|
editorFocused: boolean;
|
||||||
|
lineNumbers: boolean;
|
||||||
|
|
||||||
|
/** set height of editor to fit the specified number of lines in display */
|
||||||
|
numberOfLines?: number;
|
||||||
|
|
||||||
|
options?: monaco.editor.IEditorOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monaco specific props derived from State
|
||||||
|
*/
|
||||||
|
interface IMonacoStateProps {
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the custom theme data to avoid repeatly defining the custom theme
|
||||||
|
let customThemeData: monaco.editor.IStandaloneThemeData;
|
||||||
|
|
||||||
|
function getMonacoTheme(theme: monaco.editor.IStandaloneThemeData | monaco.editor.BuiltinTheme | string) {
|
||||||
|
if (typeof theme === "string") {
|
||||||
|
switch (theme) {
|
||||||
|
case "vs-dark":
|
||||||
|
return DarkThemeName;
|
||||||
|
case "hc-black":
|
||||||
|
return "hc-black";
|
||||||
|
case "vs":
|
||||||
|
return LightThemeName;
|
||||||
|
case "hc-light":
|
||||||
|
return HCLightThemeName;
|
||||||
|
default:
|
||||||
|
return LightThemeName;
|
||||||
|
}
|
||||||
|
} else if (theme === undefined || typeof theme === "undefined") {
|
||||||
|
return LightThemeName;
|
||||||
|
} else {
|
||||||
|
const themeName = "custom-vs";
|
||||||
|
|
||||||
|
// Skip redefining the same custom theme if it is the same theme data.
|
||||||
|
if (customThemeData !== theme) {
|
||||||
|
monaco.editor.defineTheme(themeName, theme);
|
||||||
|
customThemeData = theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
return themeName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeMapStateToProps = (initialState: AppState, initialProps: IMonacoProps) => {
|
||||||
|
const { id, contentRef } = initialProps;
|
||||||
|
const mapStateToProps = (state: AppState, ownProps: IMonacoProps & IMonacoStateProps) => {
|
||||||
|
return {
|
||||||
|
language: getCellMonacoLanguage(
|
||||||
|
state,
|
||||||
|
contentRef,
|
||||||
|
id,
|
||||||
|
ownProps.cellLanguageOverride,
|
||||||
|
ownProps.notebookLanguageOverride
|
||||||
|
)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return mapStateToProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a MonacoEditor instance within the MonacoContainer div
|
||||||
|
*/
|
||||||
|
export class MonacoEditor extends React.Component<IMonacoProps & IMonacoStateProps> {
|
||||||
|
editor?: monaco.editor.IStandaloneCodeEditor;
|
||||||
|
editorContainerRef = React.createRef<HTMLDivElement>();
|
||||||
|
contentHeight?: number;
|
||||||
|
private cursorPositionListener?: monaco.IDisposable;
|
||||||
|
|
||||||
|
constructor(props: IMonacoProps & IMonacoStateProps) {
|
||||||
|
super(props);
|
||||||
|
this.onFocus = this.onFocus.bind(this);
|
||||||
|
this.onBlur = this.onBlur.bind(this);
|
||||||
|
this.calculateHeight = this.calculateHeight.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDidChangeModelContent(e: monaco.editor.IModelContentChangedEvent): void {
|
||||||
|
if (this.editor) {
|
||||||
|
if (this.props.onChange) {
|
||||||
|
this.props.onChange(this.editor.getValue(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.calculateHeight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjust the height of editor
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* The way to determine how many lines we should display in editor:
|
||||||
|
* If numberOfLines is not set or set to 0, we adjust the height to fit the content
|
||||||
|
* If numberOfLines is specified we respect that setting
|
||||||
|
*/
|
||||||
|
calculateHeight(): void {
|
||||||
|
// Make sure we have an editor
|
||||||
|
if (!this.editor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we have a model
|
||||||
|
const model = this.editor.getModel();
|
||||||
|
if (!model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.editorContainerRef && this.editorContainerRef.current) {
|
||||||
|
const expectedLines = this.props.numberOfLines || model.getLineCount();
|
||||||
|
// The find & replace menu takes up 2 lines, that is why 2 line is set as the minimum number of lines
|
||||||
|
// TODO: we should either disable the find/replace menu or auto expand the editor when find/replace is triggerred.
|
||||||
|
const finalizedLines = Math.max(expectedLines, 1) + 1;
|
||||||
|
const lineHeight = this.editor.getConfiguration().lineHeight;
|
||||||
|
|
||||||
|
const contentHeight = finalizedLines * lineHeight;
|
||||||
|
if (this.contentHeight !== contentHeight) {
|
||||||
|
this.editorContainerRef.current.style.height = contentHeight + "px";
|
||||||
|
this.editor.layout();
|
||||||
|
this.contentHeight = contentHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(): void {
|
||||||
|
if (this.editorContainerRef && this.editorContainerRef.current) {
|
||||||
|
// Register Jupyter completion provider if needed
|
||||||
|
this.registerCompletionProvider();
|
||||||
|
|
||||||
|
// Use Monaco model uri if provided. Otherwise, create a new model uri using editor id.
|
||||||
|
const uri = this.props.modelUri ? this.props.modelUri : monaco.Uri.file(this.props.id);
|
||||||
|
|
||||||
|
// Only create a new model if it does not exist. For example, when we double click on a markdown cell,
|
||||||
|
// an editor model is created for it. Once we go back to markdown preview mode that doesn't use the editor,
|
||||||
|
// double clicking on the markdown cell will again instantiate a monaco editor. In that case, we should
|
||||||
|
// rebind the previously created editor model for the markdown instead of recreating one. Monaco does not
|
||||||
|
// allow models to be recreated with the same uri.
|
||||||
|
let model = monaco.editor.getModel(uri);
|
||||||
|
if (!model) {
|
||||||
|
model = monaco.editor.createModel(this.props.value, this.props.language, uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Monaco editor backed by a Monaco model.
|
||||||
|
this.editor = monaco.editor.create(this.editorContainerRef.current, {
|
||||||
|
// Following are the default settings
|
||||||
|
minimap: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
autoIndent: true,
|
||||||
|
overviewRulerLanes: 1,
|
||||||
|
scrollbar: {
|
||||||
|
useShadows: false,
|
||||||
|
verticalHasArrows: false,
|
||||||
|
horizontalHasArrows: false,
|
||||||
|
vertical: "hidden",
|
||||||
|
horizontal: "hidden",
|
||||||
|
verticalScrollbarSize: 0,
|
||||||
|
horizontalScrollbarSize: 0,
|
||||||
|
arrowSize: 30
|
||||||
|
},
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
find: {
|
||||||
|
// TODO Need this?
|
||||||
|
// addExtraSpaceOnTop: false, // pops the editor out of alignment if turned on
|
||||||
|
seedSearchStringFromSelection: true, // default is true
|
||||||
|
autoFindInSelection: false // default is false
|
||||||
|
},
|
||||||
|
// Disable highlight current line, too much visual noise with it on.
|
||||||
|
// VS Code also has it disabled for their notebook experience.
|
||||||
|
renderLineHighlight: "none",
|
||||||
|
|
||||||
|
// Allow editor pop up widgets such as context menus, signature help, hover tips to be able to be
|
||||||
|
// displayed outside of the editor. Without this, the pop up widgets can be clipped.
|
||||||
|
fixedOverflowWidgets: true,
|
||||||
|
|
||||||
|
// Apply custom settings from configuration
|
||||||
|
...this.props.options,
|
||||||
|
|
||||||
|
// Apply specific settings passed-in as direct props
|
||||||
|
model,
|
||||||
|
value: this.props.value,
|
||||||
|
language: this.props.language,
|
||||||
|
readOnly: this.props.readOnly,
|
||||||
|
lineNumbers: this.props.lineNumbers ? "on" : "off",
|
||||||
|
theme: getMonacoTheme(this.props.theme)
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addEditorTopMargin();
|
||||||
|
|
||||||
|
// Ignore Ctrl + Enter
|
||||||
|
// tslint:disable-next-line no-bitwise
|
||||||
|
this.editor.addCommand(
|
||||||
|
monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
|
||||||
|
() => {
|
||||||
|
// Do nothing. This is handled elsewhere, we just don't want the editor to put the newline.
|
||||||
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
// TODO Add right context
|
||||||
|
|
||||||
|
this.toggleEditorOptions(this.props.editorFocused);
|
||||||
|
|
||||||
|
if (this.props.editorFocused) {
|
||||||
|
if (!this.editor.hasTextFocus()) {
|
||||||
|
// Bring browser focus to the editor if text not already in focus
|
||||||
|
this.editor.focus();
|
||||||
|
}
|
||||||
|
this.registerCursorListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Need to remove the event listener when the editor is disposed, or we have a memory leak here.
|
||||||
|
// The same applies to the other event listeners below
|
||||||
|
// Adds listener under the resize window event which calls the resize method
|
||||||
|
window.addEventListener("resize", this.resize.bind(this));
|
||||||
|
|
||||||
|
// Adds listeners for undo and redo actions emitted from the toolbar
|
||||||
|
this.editorContainerRef.current.addEventListener("undo", () => {
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.trigger("undo-event", "undo", {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.editorContainerRef.current.addEventListener("redo", () => {
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.trigger("redo-event", "redo", {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.editor.onDidChangeModelContent(this.onDidChangeModelContent.bind(this));
|
||||||
|
this.editor.onDidFocusEditorText(this.onFocus);
|
||||||
|
this.editor.onDidBlurEditorText(this.onBlur);
|
||||||
|
this.calculateHeight();
|
||||||
|
|
||||||
|
// FIXME: This might need further investigation as the props value should be respected in construction
|
||||||
|
// The following is a mitigation measure till that time
|
||||||
|
// Ensures that the source contents of the editor (value) is consistent with the state of the editor
|
||||||
|
this.editor.setValue(this.props.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addEditorTopMargin(): void {
|
||||||
|
if (this.editor) {
|
||||||
|
// Monaco editor doesn't have margins
|
||||||
|
// https://github.com/notable/notable/issues/551
|
||||||
|
// This is a workaround to add an editor area 12px padding at the top
|
||||||
|
// so that cursors rendered by collab decorators could be visible without being cut.
|
||||||
|
this.editor.changeViewZones(changeAccessor => {
|
||||||
|
const domNode = document.createElement("div");
|
||||||
|
changeAccessor.addZone({
|
||||||
|
afterLineNumber: 0,
|
||||||
|
heightInPx: 12,
|
||||||
|
domNode
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells editor to check the surrounding container size and resize itself appropriately
|
||||||
|
*/
|
||||||
|
resize(): void {
|
||||||
|
if (this.editor && this.props.editorFocused) {
|
||||||
|
this.editor.layout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(): void {
|
||||||
|
if (!this.editor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { value, channels, /* language, contentRef, id,*/ editorFocused, theme } = this.props;
|
||||||
|
|
||||||
|
// Ensures that the source contents of the editor (value) is consistent with the state of the editor
|
||||||
|
if (this.editor.getValue() !== value) {
|
||||||
|
this.editor.setValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
completionProvider.setChannels(channels);
|
||||||
|
|
||||||
|
// Register Jupyter completion provider if needed
|
||||||
|
this.registerCompletionProvider();
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Apply new model to the editor when the language is changed.
|
||||||
|
const model = this.editor.getModel();
|
||||||
|
if (model && language && model.getModeId() !== language) {
|
||||||
|
const newUri = DocumentUri.createCellUri(contentRef, id, language);
|
||||||
|
if (!monaco.editor.getModel(newUri)) {
|
||||||
|
// Save the cursor position before we set new model.
|
||||||
|
const position = this.editor.getPosition();
|
||||||
|
|
||||||
|
// Set new model targeting the changed language.
|
||||||
|
this.editor.setModel(monaco.editor.createModel(value, language, newUri));
|
||||||
|
this.addEditorTopMargin();
|
||||||
|
|
||||||
|
// Restore cursor position to new model.
|
||||||
|
if (position) {
|
||||||
|
this.editor.setPosition(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispose of the old model in a seperate event. We cannot dispose of the model within the
|
||||||
|
// componentDidUpdate method or else the editor will throw an exception. Zero in the timeout field
|
||||||
|
// means execute immediately but in a seperate next event.
|
||||||
|
setTimeout(() => model.dispose(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (theme) {
|
||||||
|
monaco.editor.setTheme(getMonacoTheme(theme));
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the multi-tabs scenario, when the notebook is hidden by setting "display:none",
|
||||||
|
// Any state update propagated here would cause a UI re-layout, monaco-editor will then recalculate
|
||||||
|
// and set its height to 5px.
|
||||||
|
// To work around that issue, we skip updating the UI when parent element's offsetParent is null (which
|
||||||
|
// indicate an ancient element is hidden by display set to none)
|
||||||
|
// We may revisit this when we get to refactor for multi-notebooks.
|
||||||
|
if (!this.editorContainerRef.current?.offsetParent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set focus
|
||||||
|
if (editorFocused && !this.editor.hasTextFocus()) {
|
||||||
|
this.editor.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tells the editor pane to check if its container has changed size and fill appropriately
|
||||||
|
this.editor.layout();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(): void {
|
||||||
|
if (this.editor) {
|
||||||
|
try {
|
||||||
|
const model = this.editor.getModel();
|
||||||
|
if (model) {
|
||||||
|
model.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editor.dispose();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error occurs in disposing editor: ${JSON.stringify(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="monaco-container">
|
||||||
|
<div ref={this.editorContainerRef} id={`editor-${this.props.id}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register default kernel-based completion provider.
|
||||||
|
* @param language Language
|
||||||
|
*/
|
||||||
|
registerDefaultCompletionProvider(language: string): void {
|
||||||
|
// onLanguage event is emitted only once per language when language is first time needed.
|
||||||
|
monaco.languages.onLanguage(language, () => {
|
||||||
|
monaco.languages.registerCompletionItemProvider(language, completionProvider);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onFocus() {
|
||||||
|
this.props.onFocusChange(true);
|
||||||
|
this.toggleEditorOptions(true);
|
||||||
|
this.registerCursorListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
private onBlur() {
|
||||||
|
this.props.onFocusChange(false);
|
||||||
|
this.toggleEditorOptions(false);
|
||||||
|
this.unregisterCursorListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerCursorListener() {
|
||||||
|
if (this.editor && this.props.onCursorPositionChange) {
|
||||||
|
const selection = this.editor.getSelection();
|
||||||
|
this.props.onCursorPositionChange(selection);
|
||||||
|
|
||||||
|
if (!this.cursorPositionListener) {
|
||||||
|
this.cursorPositionListener = this.editor.onDidChangeCursorSelection(event =>
|
||||||
|
this.props.onCursorPositionChange!(event.selection)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unregisterCursorListener() {
|
||||||
|
if (this.cursorPositionListener) {
|
||||||
|
this.cursorPositionListener.dispose();
|
||||||
|
this.cursorPositionListener = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle editor options based on if the editor is in active state (i.e. focused).
|
||||||
|
* When the editor is not active, we want to deactivate some of the visual noise.
|
||||||
|
* @param isActive Whether editor is active.
|
||||||
|
*/
|
||||||
|
private toggleEditorOptions(isActive: boolean) {
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.updateOptions({
|
||||||
|
matchBrackets: isActive,
|
||||||
|
occurrencesHighlight: isActive,
|
||||||
|
renderIndentGuides: isActive
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register language features for target language. Call before setting language type to model.
|
||||||
|
*/
|
||||||
|
private registerCompletionProvider() {
|
||||||
|
const { enableCompletion, language, onRegisterCompletionProvider, shouldRegisterDefaultCompletion } = this.props;
|
||||||
|
|
||||||
|
if (enableCompletion && language) {
|
||||||
|
if (onRegisterCompletionProvider) {
|
||||||
|
onRegisterCompletionProvider(language);
|
||||||
|
} else if (shouldRegisterDefaultCompletion) {
|
||||||
|
this.registerDefaultCompletionProvider(language);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect<IMonacoStateProps, void, IMonacoProps, AppState>(makeMapStateToProps)(MonacoEditor);
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
|
||||||
|
// import * as monaco from "../monaco";
|
||||||
|
import { Observable, Observer } from "rxjs";
|
||||||
|
import { first, map } from "rxjs/operators";
|
||||||
|
import { childOf, JupyterMessage, ofMessageType, Channels } from "@nteract/messaging";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: import from nteract when the changes under editor-base.ts are ported to nteract.
|
||||||
|
*/
|
||||||
|
import { CompletionResults, CompletionMatch, completionRequest, js_idx_to_char_idx } from "../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 implements monaco.languages.CompletionItemProvider {
|
||||||
|
private channels: Channels | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set Channels of Jupyter kernel.
|
||||||
|
* @param channels Channels of Jupyter kernel.
|
||||||
|
*/
|
||||||
|
setChannels(channels: Channels | undefined) {
|
||||||
|
this.channels = channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether provider is connected to Jupyter kernel.
|
||||||
|
*/
|
||||||
|
get isConnectedToKernel() {
|
||||||
|
return !!this.channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional characters to trigger completion other than Ctrl+Space.
|
||||||
|
*/
|
||||||
|
get triggerCharacters() {
|
||||||
|
return [" ", "<", "/", ".", "="];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of completion items at position of cursor.
|
||||||
|
* @param model Monaco editor text model.
|
||||||
|
* @param position Position of cursor.
|
||||||
|
*/
|
||||||
|
async provideCompletionItems(model: monaco.editor.ITextModel, position: monaco.Position) {
|
||||||
|
// Convert to zero-based index
|
||||||
|
let cursorPos = model.getOffsetAt(position);
|
||||||
|
const code = model.getValue();
|
||||||
|
cursorPos = 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 = completionRequest(code, cursorPos);
|
||||||
|
items = await this.codeCompleteObservable(this.channels, message, model).toPromise();
|
||||||
|
} catch (error) {
|
||||||
|
// Temporary log error to console until we settle on how we log in V3
|
||||||
|
// tslint:disable-next-line
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve<monaco.languages.CompletionList>({
|
||||||
|
suggestions: items,
|
||||||
|
incomplete: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of completion items from Jupyter kernel.
|
||||||
|
* @param channels Channels of Jupyter kernel.
|
||||||
|
* @param message Jupyter message for completion request.
|
||||||
|
* @param model Text model.
|
||||||
|
*/
|
||||||
|
private codeCompleteObservable(channels: Channels, message: JupyterMessage, model: monaco.editor.ITextModel) {
|
||||||
|
// Process completion response
|
||||||
|
const completion$ = channels.pipe(
|
||||||
|
childOf(message),
|
||||||
|
ofMessageType("complete_reply"),
|
||||||
|
map(entry => entry.content),
|
||||||
|
first(),
|
||||||
|
map(results => this.adaptToMonacoCompletions(results, model))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Subscribe and send completion request message
|
||||||
|
return Observable.create((observer: Observer<unknown>) => {
|
||||||
|
const subscription = completion$.subscribe(observer);
|
||||||
|
channels.next(message);
|
||||||
|
return subscription;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts Jupyter completion result to list of Monaco completion items.
|
||||||
|
*/
|
||||||
|
private adaptToMonacoCompletions(results: CompletionResults, model: monaco.editor.ITextModel) {
|
||||||
|
let range: monaco.IRange;
|
||||||
|
let percentCount = 0;
|
||||||
|
let matches = results ? results.matches : [];
|
||||||
|
if (results.metadata && results.metadata._jupyter_types_experimental) {
|
||||||
|
matches = results.metadata._jupyter_types_experimental as CompletionMatch[];
|
||||||
|
}
|
||||||
|
return matches.map((match: CompletionMatch, index: number) => {
|
||||||
|
if (typeof match === "string") {
|
||||||
|
const text = this.sanitizeText(match);
|
||||||
|
const filtered = this.getFilterText(text);
|
||||||
|
return {
|
||||||
|
kind: this.adaptToMonacoCompletionItemKind(unknownJupyterKind),
|
||||||
|
label: text,
|
||||||
|
insertText: text,
|
||||||
|
filterText: filtered,
|
||||||
|
sortText: this.getSortText(index)
|
||||||
|
} as monaco.languages.CompletionItem;
|
||||||
|
} else {
|
||||||
|
// We only need to get the range once as the range is the same for all completion items in the list.
|
||||||
|
if (!range) {
|
||||||
|
const start = model.getPositionAt(match.start);
|
||||||
|
const end = model.getPositionAt(match.end);
|
||||||
|
range = {
|
||||||
|
startLineNumber: start.lineNumber,
|
||||||
|
startColumn: start.column,
|
||||||
|
endLineNumber: end.lineNumber,
|
||||||
|
endColumn: end.column
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the range representing the text before the completion action was invoked.
|
||||||
|
// If the text starts with magics % indicator, we need to track how many of these indicators exist
|
||||||
|
// so that we ensure the insertion text only 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.
|
||||||
|
const rangeText = model.getValueInRange(range);
|
||||||
|
if (rangeText.startsWith("%%")) {
|
||||||
|
percentCount = 2;
|
||||||
|
} else if (rangeText.startsWith("%")) {
|
||||||
|
percentCount = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = this.sanitizeText(match.text);
|
||||||
|
const filtered = this.getFilterText(text);
|
||||||
|
const insert = this.getInsertText(text, percentCount);
|
||||||
|
return {
|
||||||
|
kind: this.adaptToMonacoCompletionItemKind(match.type as keyof typeof jupyterToMonacoCompletionItemKind),
|
||||||
|
label: text,
|
||||||
|
insertText: percentCount > 0 ? insert : text,
|
||||||
|
filterText: filtered,
|
||||||
|
sortText: this.getSortText(index)
|
||||||
|
} as monaco.languages.CompletionItem;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts Jupyter completion item kind to Monaco completion item kind.
|
||||||
|
* @param kind Jupyter completion item kind.
|
||||||
|
*/
|
||||||
|
private adaptToMonacoCompletionItemKind(kind: keyof typeof jupyterToMonacoCompletionItemKind) {
|
||||||
|
const result = jupyterToMonacoCompletionItemKind[kind];
|
||||||
|
return result ? result : jupyterToMonacoCompletionItemKind[unknownJupyterKind];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove everything before a dot. Jupyter completion results like to include all characters before
|
||||||
|
* the trigger character. For example, if user types "myarray.", we expect the completion results to
|
||||||
|
* show "append", "pop", etc. but for the actual case, it will show "myarray.append", "myarray.pop",
|
||||||
|
* etc. so we are going to sanitize the text.
|
||||||
|
* @param text Text of Jupyter completion item
|
||||||
|
*/
|
||||||
|
private sanitizeText(text: string) {
|
||||||
|
const index = text.lastIndexOf(".");
|
||||||
|
return index > -1 && index < text.length - 1 ? text.substring(index + 1) : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove magics all % characters as Monaco doesn't like them for the filtering text.
|
||||||
|
* Without this, completion won't show magics match items.
|
||||||
|
* @param text Text of Jupyter completion item.
|
||||||
|
*/
|
||||||
|
private getFilterText(text: string) {
|
||||||
|
return text.replace(/%/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get insertion text handling what to insert for the magics case depending on what
|
||||||
|
* has already been typed.
|
||||||
|
* @param text Text of Jupyter completion item.
|
||||||
|
* @param percentCount Number of percent characters to remove
|
||||||
|
*/
|
||||||
|
private getInsertText(text: string, percentCount: number) {
|
||||||
|
for (let i = 0; i < percentCount; i++) {
|
||||||
|
text = text.replace("%", "");
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
private getSortText(order: number): string {
|
||||||
|
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();
|
||||||
|
export { completionProvider };
|
||||||
44
src/Explorer/Notebook/MonacoEditor/converter.ts
Normal file
44
src/Explorer/Notebook/MonacoEditor/converter.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import Immutable from "immutable";
|
||||||
|
import * as monaco from "./monaco";
|
||||||
|
/**
|
||||||
|
* Code Mirror to Monaco constants.
|
||||||
|
*/
|
||||||
|
export enum Mode {
|
||||||
|
markdown = "markdown",
|
||||||
|
raw = "plaintext",
|
||||||
|
python = "python",
|
||||||
|
csharp = "csharp"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps Code Mirror mode to a valid Monaco Editor supported langauge
|
||||||
|
* defaults to plaintext if map not found.
|
||||||
|
* @param mode Code Mirror mode
|
||||||
|
* @returns Monaco language
|
||||||
|
*/
|
||||||
|
export function mapCodeMirrorModeToMonaco(mode: string | { name: string }): string {
|
||||||
|
let language = "";
|
||||||
|
|
||||||
|
// Parse codemirror mode object
|
||||||
|
if (typeof mode === "string") {
|
||||||
|
language = mode;
|
||||||
|
}
|
||||||
|
// Vanilla object
|
||||||
|
else if (typeof mode === "object" && mode.name) {
|
||||||
|
language = mode.name;
|
||||||
|
}
|
||||||
|
// Immutable Map
|
||||||
|
else if (Immutable.Map.isMap(mode) && mode.has("name")) {
|
||||||
|
language = mode.get("name");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need to handle "ipython" as a special case since it is not a registered language
|
||||||
|
if (language === "ipython") {
|
||||||
|
return Mode.python;
|
||||||
|
} else if (language === "text/x-csharp") {
|
||||||
|
return Mode.csharp;
|
||||||
|
} else if (monaco.languages.getEncodedLanguageId(language) > 0) {
|
||||||
|
return language;
|
||||||
|
}
|
||||||
|
return Mode.raw;
|
||||||
|
}
|
||||||
76
src/Explorer/Notebook/MonacoEditor/editor-base.ts
Normal file
76
src/Explorer/Notebook/MonacoEditor/editor-base.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// Disable linting on file since we will be moving the code below to nteract which have different rules configured.
|
||||||
|
// tslint:disable:variable-name
|
||||||
|
// tslint:disable:interface-name
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Create new editor-base package in nteract repo and move all code below to new package.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createMessage } from "@nteract/messaging";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jupyter messaging protocol's _jupyter_types_experimental completion result.
|
||||||
|
*/
|
||||||
|
interface CompletionResult {
|
||||||
|
end: number;
|
||||||
|
start: number;
|
||||||
|
type: string;
|
||||||
|
text: string;
|
||||||
|
displayText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Juptyer completion match item.
|
||||||
|
*/
|
||||||
|
export type CompletionMatch = string | CompletionResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jupyter messaging protocol's complete_reply response.
|
||||||
|
*/
|
||||||
|
export interface CompletionResults {
|
||||||
|
status: string;
|
||||||
|
cursor_start: number;
|
||||||
|
cursor_end: number;
|
||||||
|
matches: CompletionMatch[];
|
||||||
|
metadata?: {
|
||||||
|
_jupyter_types_experimental?: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Jupyter messaging protocol's complete_request message.
|
||||||
|
* @param code Code of editor.
|
||||||
|
* @param cursorPos cursor position represented in the Jupyter messaging protocol (character position)
|
||||||
|
*/
|
||||||
|
export const completionRequest = (code: string, cursorPos: number) =>
|
||||||
|
createMessage("complete_request", {
|
||||||
|
content: {
|
||||||
|
code,
|
||||||
|
cursor_pos: cursorPos
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JavaScript stores text as utf16 and string indices use "code units",
|
||||||
|
* which stores high-codepoint characters as "surrogate pairs",
|
||||||
|
* which occupy two indices in the JavaScript string.
|
||||||
|
* We need to translate cursor_pos in the protocol (in characters)
|
||||||
|
* to js offset (with surrogate pairs taking two spots).
|
||||||
|
* @param js_idx JavaScript index
|
||||||
|
* @param text Text
|
||||||
|
*/
|
||||||
|
export const js_idx_to_char_idx: (js_idx: number, text: string) => number = (js_idx: number, text: string): number => {
|
||||||
|
let char_idx: number = js_idx;
|
||||||
|
for (let i = 0; i + 1 < text.length && i < js_idx; i++) {
|
||||||
|
const char_code: number = text.charCodeAt(i);
|
||||||
|
// check for surrogate pair
|
||||||
|
if (char_code >= 0xd800 && char_code <= 0xdbff) {
|
||||||
|
const next_char_code: number = text.charCodeAt(i + 1);
|
||||||
|
if (next_char_code >= 0xdc00 && next_char_code <= 0xdfff) {
|
||||||
|
char_idx--;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return char_idx;
|
||||||
|
};
|
||||||
10
src/Explorer/Notebook/MonacoEditor/monaco.ts
Normal file
10
src/Explorer/Notebook/MonacoEditor/monaco.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export * from "monaco-editor/esm/vs/editor/editor.api";
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Set the custom worker url to workaround the cross-domain issue with creating web worker
|
||||||
|
// * See https://github.com/microsoft/monaco-editor/blob/master/docs/integrate-amd-cross.md for more details
|
||||||
|
// * This step has to be executed after a importing of monaco-editor once per chunk to make sure
|
||||||
|
// * the custom worker url overwrites the one from monaco-editor module itself.
|
||||||
|
// */
|
||||||
|
// import { setMonacoWorkerUrl } from "./workerUrl";
|
||||||
|
// setMonacoWorkerUrl();
|
||||||
67
src/Explorer/Notebook/MonacoEditor/selectors.ts
Normal file
67
src/Explorer/Notebook/MonacoEditor/selectors.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { AppState, ContentRef, selectors as nteractSelectors } from "@nteract/core";
|
||||||
|
import { CellId } from "@nteract/commutable";
|
||||||
|
import { Mode, mapCodeMirrorModeToMonaco } from "./converter";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the language to use for syntax highlighting and autocompletion in the Monaco Editor for a given cell, falling back to the notebook language if one for the cell is not defined.
|
||||||
|
*/
|
||||||
|
export const getCellMonacoLanguage = (
|
||||||
|
state: AppState,
|
||||||
|
contentRef: ContentRef,
|
||||||
|
cellId: CellId,
|
||||||
|
cellLanguageOverride?: string,
|
||||||
|
notebookLanguageOverride?: string
|
||||||
|
): string => {
|
||||||
|
const model = nteractSelectors.model(state, { contentRef });
|
||||||
|
if (!model || model.type !== "notebook") {
|
||||||
|
throw new Error("Connected Editor components should not be used with non-notebook models");
|
||||||
|
}
|
||||||
|
|
||||||
|
const cell = nteractSelectors.notebook.cellById(model, { id: cellId });
|
||||||
|
if (!cell) {
|
||||||
|
throw new Error("Invalid cell id");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (cell.cell_type) {
|
||||||
|
case "markdown":
|
||||||
|
return Mode.markdown;
|
||||||
|
case "raw":
|
||||||
|
return Mode.raw;
|
||||||
|
case "code":
|
||||||
|
if (cellLanguageOverride) {
|
||||||
|
return mapCodeMirrorModeToMonaco(cellLanguageOverride);
|
||||||
|
} else {
|
||||||
|
// Fall back to notebook language if cell language isn't present.
|
||||||
|
return getNotebookMonacoLanguage(state, contentRef, notebookLanguageOverride);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the language to use for syntax highlighting and autocompletion in the Monaco Editor for a given notebook.
|
||||||
|
*/
|
||||||
|
export const getNotebookMonacoLanguage = (
|
||||||
|
state: AppState,
|
||||||
|
contentRef: ContentRef,
|
||||||
|
notebookLanguageOverride?: string
|
||||||
|
): string => {
|
||||||
|
const model = nteractSelectors.model(state, { contentRef });
|
||||||
|
if (!model || model.type !== "notebook") {
|
||||||
|
throw new Error("Connected Editor components should not be used with non-notebook models");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notebookLanguageOverride) {
|
||||||
|
return mapCodeMirrorModeToMonaco(notebookLanguageOverride);
|
||||||
|
}
|
||||||
|
|
||||||
|
const kernelRef = model.kernelRef;
|
||||||
|
let codeMirrorMode;
|
||||||
|
// Try to get the CodeMirror mode from the kernel.
|
||||||
|
if (kernelRef) {
|
||||||
|
codeMirrorMode = nteractSelectors.kernel(state, { kernelRef })?.info?.codemirrorMode;
|
||||||
|
}
|
||||||
|
// As a fallback, get the CodeMirror mode from the notebook itself.
|
||||||
|
codeMirrorMode = codeMirrorMode ?? nteractSelectors.notebook.codeMirrorMode(model);
|
||||||
|
|
||||||
|
return mapCodeMirrorModeToMonaco(codeMirrorMode);
|
||||||
|
};
|
||||||
22
src/Explorer/Notebook/MonacoEditor/styles.css
Normal file
22
src/Explorer/Notebook/MonacoEditor/styles.css
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
For the following components, we use the inherited width values from monaco-container.
|
||||||
|
On resizing the browser, the width of monaco-container will be calculated
|
||||||
|
and we just use the calculated width for the following components
|
||||||
|
So we don't need to use Monaco editor's layout() function which is expensive operation and causes performance issues on resizing.
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
TODO: These styles below are added for resizing perf improvement.
|
||||||
|
Once the virtualization is implemented, we will revisit this later.
|
||||||
|
*/
|
||||||
|
.monaco-container .monaco-editor {
|
||||||
|
width: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monaco-container .monaco-editor .overflow-guard {
|
||||||
|
width: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 26px is the left margin for .monaco-scrollable-element */
|
||||||
|
.monaco-container .monaco-editor .monaco-scrollable-element.editor-scrollable.vs {
|
||||||
|
width: calc(100% - 26px) !important;
|
||||||
|
}
|
||||||
75
src/Explorer/Notebook/MonacoEditor/theme.ts
Normal file
75
src/Explorer/Notebook/MonacoEditor/theme.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import * as monaco from "./monaco";
|
||||||
|
|
||||||
|
// TODO: move defineTheme calls to an initialization function
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default light theme with customized background
|
||||||
|
*/
|
||||||
|
export const LightThemeName = "vs-light";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default monaco theme for light theme
|
||||||
|
*/
|
||||||
|
export const customMonacoLightTheme: monaco.editor.IStandaloneThemeData = {
|
||||||
|
base: "vs", // Derive from default light theme of Monaco
|
||||||
|
inherit: true,
|
||||||
|
rules: [],
|
||||||
|
colors: {
|
||||||
|
// We want Monaco background to use the same background for our themes.
|
||||||
|
// Without this, the Monaco light theme has a yellowish tone.
|
||||||
|
// Verified with UX that white meets all the accessbility requirements for light
|
||||||
|
// and high contrast light theme.
|
||||||
|
"editor.background": "#FFFFFF"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
monaco.editor.defineTheme(LightThemeName, customMonacoLightTheme);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default dark theme with customized background
|
||||||
|
*/
|
||||||
|
export const DarkThemeName = "aznb-dark";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default monaco theme for dark theme
|
||||||
|
*/
|
||||||
|
export const customMonacoDarkTheme: monaco.editor.IStandaloneThemeData = {
|
||||||
|
base: "vs-dark", // Derive from default dark theme of Monaco
|
||||||
|
inherit: true,
|
||||||
|
rules: [],
|
||||||
|
colors: {
|
||||||
|
"editor.background": "#1b1a19"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
monaco.editor.defineTheme(DarkThemeName, customMonacoDarkTheme);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The custom high contrast light theme with customized background
|
||||||
|
*/
|
||||||
|
export const HCLightThemeName = "hc-light";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default monaco theme for light high contrast mode
|
||||||
|
*/
|
||||||
|
export const customMonacoHCLightTheme: monaco.editor.IStandaloneThemeData = {
|
||||||
|
base: "vs", // Derive from default light theme of Monaco; change all grey colors to black to comply with highcontrast rules
|
||||||
|
inherit: true,
|
||||||
|
rules: [
|
||||||
|
{ token: "annotation", foreground: "000000" },
|
||||||
|
{ token: "delimiter.html", foreground: "000000" },
|
||||||
|
{ token: "operator.scss", foreground: "000000" },
|
||||||
|
{ token: "operator.sql", foreground: "000000" },
|
||||||
|
{ token: "operator.swift", foreground: "000000" },
|
||||||
|
{ token: "predefined.sql", foreground: "000000" }
|
||||||
|
],
|
||||||
|
colors: {
|
||||||
|
// We want Monaco background to use the same background for our themes.
|
||||||
|
// Without this, the Monaco light theme has a yellowish tone.
|
||||||
|
// Verified with UX that white meets all the accessbility requirements for light
|
||||||
|
// and high contrast light theme.
|
||||||
|
"editor.background": "#FFFFFF"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
monaco.editor.defineTheme(HCLightThemeName, customMonacoHCLightTheme);
|
||||||
@@ -30,6 +30,7 @@ import { CellType } from "@nteract/commutable/src";
|
|||||||
import "./NotebookRenderer.less";
|
import "./NotebookRenderer.less";
|
||||||
import HoverableCell from "./decorators/HoverableCell";
|
import HoverableCell from "./decorators/HoverableCell";
|
||||||
import CellLabeler from "./decorators/CellLabeler";
|
import CellLabeler from "./decorators/CellLabeler";
|
||||||
|
import MonacoEditor from "../MonacoEditor/MonacoEditor";
|
||||||
import * as cdbActions from "../NotebookComponent/actions";
|
import * as cdbActions from "../NotebookComponent/actions";
|
||||||
|
|
||||||
export interface NotebookRendererBaseProps {
|
export interface NotebookRendererBaseProps {
|
||||||
@@ -116,7 +117,12 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
|
|||||||
{{
|
{{
|
||||||
editor: {
|
editor: {
|
||||||
codemirror: (props: PassedEditorProps) => (
|
codemirror: (props: PassedEditorProps) => (
|
||||||
<CodeMirrorEditor {...props} lineNumbers={true} />
|
<MonacoEditor
|
||||||
|
{...props}
|
||||||
|
lineNumbers={true}
|
||||||
|
enableCompletion={true}
|
||||||
|
shouldRegisterDefaultCompletion={true}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
prompt: ({ id, contentRef }: { id: CellId; contentRef: ContentRef }) => (
|
prompt: ({ id, contentRef }: { id: CellId; contentRef: ContentRef }) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user