import { Channels } from "@nteract/messaging" ; import { CellType , CellId } from "@nteract/commutable" ; import * as monaco from "monaco-editor" ; import * as React from "react" ; import { completionProvider } from "./completions/completionItemProvider" ; import { ContentRef } from "@nteract/core" ; import { DocumentUri } from "./documentUri" ; import debounce from "lodash.debounce" ; export type IModelContentChangedEvent = monaco . editor . IModelContentChangedEvent ; /**
 * This adds an additional padded area around the editor for the mouse
 * to move around before we decide to hide the popup. This makes the
 * transition less erratic and hopefully a smoother experience.
 */ const HOVER_BOUND_DEFAULT_PADDING = 5 ; /**
 * Settings for configuring keyboard shortcuts with Monaco
 */ export interface IMonacoShortCutProps { cellType : CellType ; cellFocusDirection : string | undefined ; setCellFocusDirection : ( direction ? : string ) => void ; focusCell : ( payload : { id : CellId ; contentRef : ContentRef ; } ) => void ; focusAboveCellCommandMode : ( ) => void ; focusBelowCellCommandMode : ( ) => void ; insertCellBelow : ( contentRef : ContentRef , cellType : CellType ) => void ; executeCell : ( ) => void ; focusEditor : ( ) => void ; focusNextCellEditor : ( setPosition ? : boolean ) => void ; focusPreviousCellEditor : ( ) => void ; unfocusEditor : ( ) => void ; } /**
 * Common props passed to the editor component
 */ export interface IMonacoComponentProps { id : string ; contentRef : ContentRef ; theme : string ; readOnly ? : boolean ; channels ? : Channels | undefined ; value : string ; editorType ? : string ; editorFocused ? : boolean ; onChange ? : ( value : string , event ? : any ) => void ; onFocusChange ? : ( focus : boolean ) => void ; } /**
 * Props passed for configuring Monaco Editor
 */ export interface IMonacoConfiguration { /**
     * modelUri acts an identifier to query the editor model
     * without being tied to the UI
     * Calling the getModel(modelUri) API
     */ modelUri ? : monaco . Uri ; enableCompletion ? : boolean ; shouldRegisterDefaultCompletion ? : boolean ; onCursorPositionChange ? : ( selection : monaco . ISelection | null ) => void ; onRegisterDocumentFormattingEditProvider ? : ( languageId : string ) => void ; enableFormatting ? : boolean ; onRegisterCompletionProvider ? : ( languageId : string ) => void ; language : string ; lineNumbers ? : boolean ; /** set height of editor to fit the specified number of lines in display */ numberOfLines ? : number ; indentSize ? : number ; tabSize ? : number ; options ? : monaco . editor . IEditorOptions ; shortcutsOptions ? : IMonacoShortCutProps ; shortcutsHandler ? : ( editor : monaco . editor . IStandaloneCodeEditor , settings ? : IMonacoShortCutProps ) => void ; cursorPositionHandler ? : ( editor : monaco . editor . IStandaloneCodeEditor , settings ? : IMonacoProps ) => void ; commandHandler ? : ( editor : monaco . editor . IStandaloneCodeEditor ) => void ; onDidCreateEditor ? : ( editor : monaco . editor . IStandaloneCodeEditor ) => void ; } /**
 * Initial props for Monaco Editor received from agnostic component
 */ export type IMonacoProps = IMonacoComponentProps & IMonacoConfiguration ; /**
 * Creates a MonacoEditor instance
 */ export default class MonacoEditor extends React . Component < IMonacoProps > { editor ? : monaco . editor . IStandaloneCodeEditor ; editorContainerRef = React . createRef < HTMLDivElement > ( ) ; contentHeight ? : number ; private cursorPositionListener ? : monaco . IDisposable ; private blurEditorWidgetListener ? : monaco . IDisposable ; private mouseMoveListener ? : monaco . IDisposable ; constructor ( props ) { super ( props ) ; this . calculateHeight = this . calculateHeight . bind ( this ) ; this . onBlur = this . onBlur . bind ( this ) ; this . onDidChangeModelContent = this . onDidChangeModelContent . bind ( this ) ; this . onFocus = this . onFocus . bind ( this ) ; this . resize = this . resize . bind ( this ) ; this . hideAllOtherParameterWidgets = this . hideAllOtherParameterWidgets . bind ( this ) ; this . handleCoordsOutsideWidgetActiveRegion = debounce ( this . handleCoordsOutsideWidgetActiveRegion . bind ( this ) , 50 ) ; } onDidChangeModelContent ( e ) { if ( this . editor && this . props . onChange ) { this . props . onChange ( this . editor . getValue ( ) , e ) ; } } param height calculateHeight ( height ? ) { if ( ! this . editor ) { return ; } if ( typeof height === "undefined" ) { height = this . editor . getContentHeight ( ) ; } if ( this . editorContainerRef && this . editorContainerRef . current && ( this . contentHeight !== height ) ) { this . editorContainerRef . current . style . height = height + "px" ; /**
             * With no params, the layout method queries the DOM to get the parent container dimensions
             * This causes a forced layout by the browser
             * We pass in the expected width and height to as an optimization to avoid the forced layout
             */ this . editor . layout ( { width : this . editor . getLayoutInfo ( ) . width , height } ) ; this . contentHeight = height ; } } componentDidMount ( ) { if ( this . editorContainerRef && this . editorContainerRef . current ) { this . registerCompletionProvider ( ) ; this . registerDocumentFormatter ( ) ; const uri = this . props . modelUri ? this . props . modelUri : monaco . Uri . file ( this . props . id ) ; let model = monaco . editor . getModel ( uri ) ; if ( ! model ) { model = monaco . editor . createModel ( this . props . value , this . props . language , uri ) ; } model . updateOptions ( { indentSize : this . props . indentSize , tabSize : this . props . tabSize } ) ; this . editor = monaco . editor . create ( this . editorContainerRef . current , { autoIndent : "advanced" , fixedOverflowWidgets : true , find : { addExtraSpaceOnTop : false , seedSearchStringFromSelection : true , autoFindInSelection : "never" } , language : this . props . language , lineNumbers : this . props . lineNumbers ? "on" : "off" , minimap : { enabled : false } , model , overviewRulerLanes : 0 , padding : { top : 12 , bottom : 5 } , readOnly : this . props . readOnly , renderLineHighlight : "none" , scrollbar : { useShadows : false , verticalHasArrows : false , horizontalHasArrows : false , vertical : "hidden" , horizontal : "hidden" , verticalScrollbarSize : 0 , horizontalScrollbarSize : 0 , arrowSize : 30 } , theme : this . props . theme , value : this . props . value , ... this . props . options , scrollBeyondLastLine : false } ) ; if ( this . props . onDidCreateEditor ) { this . props . onDidCreateEditor ( this . editor ) ; } if ( this . editor && this . props . shortcutsHandler && this . props . shortcutsOptions ) { this . props . shortcutsHandler ( this . editor , this . props . shortcutsOptions ) ; } if ( this . editor && this . props . commandHandler ) { this . props . commandHandler ( this . editor ) ; } this . toggleEditorOptions ( ! ! this . props . editorFocused ) ; if ( this . props . editorFocused ) { if ( ! this . editor . hasTextFocus ( ) ) { this . editor . focus ( ) ; } this . registerCursorListener ( ) ; } window . addEventListener ( "resize" , this . resize ) ; 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 . onDidContentSizeChange ( ( info ) => { if ( info . contentHeightChanged ) { this . calculateHeight ( info . contentHeight ) ; } } ) ; this . editor . onDidChangeModelContent ( this . onDidChangeModelContent ) ; this . editor . onDidFocusEditorText ( this . onFocus ) ; this . editor . onDidBlurEditorText ( this . onBlur ) ; this . calculateHeight ( ) ; this . editor . setValue ( this . props . value ) ; if ( this . props . cursorPositionHandler ) { this . props . cursorPositionHandler ( this . editor , this . props ) ; } this . blurEditorWidgetListener = this . editor . onDidBlurEditorWidget ( ( ) => { this . hideParameterWidget ( ) ; } ) ; if ( this . editor ) { this . mouseMoveListener = this . editor . onMouseMove ( ( e ) => { this . handleCoordsOutsideWidgetActiveRegion ( e . event ?. pos ?. x , e . event ?. pos ?. y ) ; } ) ; } } } /**
     * Tells editor to check the surrounding container size and resize itself appropriately
     */ resize ( ) { if ( this . editor && this . props . editorFocused ) { this . editor . layout ( ) ; } } componentDidUpdate ( prevProps ) { if ( ! this . editor ) { return ; } const { value , language , contentRef , id , editorFocused , theme } = this . props ; if ( this . props . cursorPositionHandler ) { this . props . cursorPositionHandler ( this . editor , this . props ) ; } if ( this . editor && this . props . commandHandler ) { this . props . commandHandler ( this . editor ) ; } if ( prevProps . value !== this . props . value && this . editor . getValue ( ) !== this . props . value ) { this . editor . setValue ( this . props . value ) ; } completionProvider . setChannels ( this . props . channels ) ; this . registerCompletionProvider ( ) ; const model = this . editor . getModel ( ) ; if ( model && language && model . getModeId ( ) !== language ) { const editor = this . editor ; setTimeout ( ( ) => { const newUri = DocumentUri . createCellUri ( contentRef , id , language ) ; if ( ! monaco . editor . getModel ( newUri ) ) { const position = editor . getPosition ( ) ; editor . setModel ( monaco . editor . createModel ( value , language , newUri ) ) ; if ( position ) { editor . setPosition ( position ) ; } if ( editorFocused && ! editor . hasTextFocus ( ) ) { editor . focus ( ) ; } model . dispose ( ) ; } } , 0 ) ; } const monacoUpdateOptions = { readOnly : this . props . readOnly } ; if ( theme ) { monacoUpdateOptions . theme = theme ; } this . editor . updateOptions ( monacoUpdateOptions ) ; if ( ! this . editorContainerRef . current ?. offsetParent ) { return ; } if ( editorFocused && ! this . editor . hasTextFocus ( ) ) { this . editor . focus ( ) ; } this . editor . layout ( ) ; } componentWillUnmount ( ) { if ( this . editor ) { try { const model = this . editor . getModel ( ) ; window . removeEventListener ( "resize" , this . resize ) ; if ( model ) { model . dispose ( ) ; } this . editor . dispose ( ) ; } catch ( err ) { console . error ( `Error occurs in disposing editor: ${ JSON . stringify ( err ) }` ) ; } } if ( this . blurEditorWidgetListener ) { this . blurEditorWidgetListener . dispose ( ) ; } if ( this . mouseMoveListener ) { this . mouseMoveListener . dispose ( ) ; } } render ( ) { return ( < div className = "monaco-container" > < div ref = { this . editorContainerRef } id = { `editor-${ this . props . id }` } /> < /div>); } param language registerDefaultCompletionProvider ( language ) { monaco . languages . onLanguage ( language , ( ) => { monaco . languages . registerCompletionItemProvider ( language , completionProvider ) ; } ) ; } private onFocus ( ) { if ( this . props . onFocusChange ) { this . props . onFocusChange ( true ) ; } this . toggleEditorOptions ( true ) ; this . registerCursorListener ( ) ; } private onBlur ( ) { if ( this . props . onFocusChange ) { 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 ; } } private toggleEditorOptions ( isActive ) { if ( this . editor ) { this . editor . updateOptions ( { matchBrackets : isActive ? "always" : "never" , occurrencesHighlight : isActive , renderIndentGuides : isActive } ) ; } } private registerCompletionProvider ( ) { const { enableCompletion , language , onRegisterCompletionProvider , shouldRegisterDefaultCompletion } = this . props ; if ( enableCompletion && language ) { if ( onRegisterCompletionProvider ) { onRegisterCompletionProvider ( language ) ; } else if ( shouldRegisterDefaultCompletion ) { this . registerDefaultCompletionProvider ( language ) ; } } } private registerDocumentFormatter ( ) { const { enableFormatting , language , onRegisterDocumentFormattingEditProvider } = this . props ; if ( enableFormatting && language ) { if ( onRegisterDocumentFormattingEditProvider ) { onRegisterDocumentFormattingEditProvider ( language ) ; } } } private hideParameterWidget ( ) { if ( ! this . editor || ! this . editor . getDomNode ( ) || ! this . editorContainerRef . current ) { return ; } const hoverElements = Array . prototype . slice . call ( document . querySelectorAll ( ':hover' ) ) ; const parameterWidgetClasses = [ 'editor-widget' , 'parameter-hints-widget' , 'visible' ] ; let isParameterWidgetHovered = hoverElements . find ( item => { if ( typeof item . className !== 'string' ) { return false ; } const classes = item . className . split ( ' ' ) ; if ( ! parameterWidgetClasses . every ( cls => classes . indexOf ( cls ) >= 0 ) ) { return false ; } return this . editorContainerRef . current ?. contains ( item ) ; } ) ; if ( isParameterWidgetHovered ) { return ; } if ( this . editor . hasWidgetFocus ( ) ) { return ; } this . hideWidgets ( this . editorContainerRef . current , [ '.parameter-hints-widget' ] ) ; } private hideWidgets ( widgetParent , selectors ) { for ( const selector of selectors ) { for ( const widget of Array . from < HTMLDivElement > ( widgetParent . querySelectorAll ( selector ) ) ) { widget . setAttribute ( 'class' , widget . className . split ( ' ' ) . filter ( ( cls ) => cls !== 'visible' ) . join ( ' ' ) ) ; if ( widget . style . visibility !== 'hidden' ) { widget . style . visibility = 'hidden' ; } } } } private hideAllOtherParameterWidgets ( ) { if ( ! this . editorContainerRef . current ) { return ; } const widgetParents = Array . prototype . slice . call ( document . querySelectorAll ( 'div.monaco-container' ) ) ; widgetParents . filter ( widgetParent => widgetParent !== this . editorContainerRef . current ?. parentElement ) . forEach ( widgetParent => this . hideWidgets ( widgetParent , [ '.parameter-hints-widget' ] ) ) ; } private coordsInsideElement ( element , x , y , padding = HOVER_BOUND_DEFAULT_PADDING ) { if ( ! element ) return false ; const clientRect = element . getBoundingClientRect ( ) ; return ( x >= clientRect . left - padding && x <= clientRect . right + padding && y >= clientRect . top - padding && y <= clientRect . bottom + padding ) ; } private handleCoordsOutsideWidgetActiveRegion ( x , y ) { let widget = document . querySelector ( '.parameter-hints-widget' ) ; if ( widget != null && ! this . coordsInsideElement ( widget , x , y ) ) { this . hideAllOtherParameterWidgets ( ) ; } } }