import * as React from 'react' ; import AceEditorOrig from 'react-ace' ; import 'brace/ext/searchbox' ; import 'brace/mode/markdown' ; import 'brace/theme/twilight' ; import { QDLMode } from './QDLMode' ; import { AnnotationType } from '../../reducers/StateTypes' ; import Spellcheck from '../../Spellcheck' ; declare var window ; const AceEditor = AceEditorOrig as any ; const acequire = ( require ( 'brace' ) as any ) . acequire ; const { Range } = acequire ( 'ace/range' ) ; const { Search } = acequire ( 'ace/search' ) ; const mode = new QDLMode ( ) ; interface AceAnnotation { column : number ; row : number ; text : string ; type : 'error' | 'info' | 'warning' ; } interface TextViewProps extends React . Props < any > { annotations : AnnotationType [ ] ; onChange : ( text : string ) => any ; onLine : ( line : number ) => any ; onAnnotationClick : ( annotations : number [ ] ) => any ; realtime : any ; realtimeModel : any ; lastSizeChangeMillis : number ; scrollLineTarget ? : number ; scrollLineTargetTs ? : number ; showLineNumbers ? : boolean ; showSpellcheck ? : boolean ; } export default class TextView extends React . Component < TextViewProps , { } > { public ace : any ; public focused : boolean ; public lineChangeTs : number ; public spellchecker : any ; public onSelectionChange : ( ) => any ; public silentChange : boolean ; public silentSelectionChangeTimer : any ; public onRef ( ref ) { if ( this . ace && this . onSelectionChange ) { this . ace . editor . off ( 'changeSelection' , this . onSelectionChange ) ; } this . ace = ref ; this . onSelectionChange = ( ( ) => { if ( this . silentSelectionChangeTimer ) { clearTimeout ( this . silentSelectionChangeTimer ) ; } this . silentSelectionChangeTimer = setTimeout ( ( ) => { this . silentSelectionChangeTimer = null ; const selection = this . ace . editor . getSelection ( ) ; const lead = selection . selectionLead ; const anchor = selection . anchor ; if ( lead . row !== anchor . row || lead . column !== anchor . column ) { return ; } if ( this . props . onLine ) { this . props . onLine ( anchor . row ) ; } } , 50 ) ; } ) ; if ( this . ace ) { ref . editor . resize ( ) ; const session = ref . editor . getSession ( ) ; if ( ! this . spellchecker && window . dictionary && this . ace . editor . session . getDocument ( ) . getLength ( ) > 1 ) { this . spellchecker = new Spellcheck ( session , window . dictionary ) ; session . on ( 'change' , ( ) => { this . spellchecker . onChange ( ) ; } ) ; setInterval ( ( ) => { this . spellchecker . spellcheck ( ) ; } , 500 ) ; } ref . editor . $blockScrolling = Infinity ; session . $mode = mode ; session . $foldMode = mode . foldingRules ; session . getFoldWidget = ( row ) => mode . foldingRules . getFoldWidget ( session , '' , row ) ; session . getFoldWidgetRange = ( row ) => mode . foldingRules . getFoldWidgetRange ( session , '' , row ) ; ref . editor . getSession ( ) . bgTokenizer . setTokenizer ( mode . getTokenizer ( ) ) ; ref . editor . setOption ( 'tabSize' , 2 ) ; ref . editor . setOption ( 'wrapBehavioursEnabled' , true ) ; ref . editor . setOption ( 'wrap' , true ) ; ref . editor . setOption ( 'useSoftTabs' , true ) ; ref . editor . on ( 'changeSelection' , this . onSelectionChange ) ; ref . editor . on ( 'gutterclick' , ( e ) => { this . onGutterClick ( e ) ; } ) ; ref . editor . on ( 'click' , ( e ) => { if ( ! e . domEvent . altKey ) { return ; } ref . editor . commands . commands . jumpToDef . exec ( ) ; } ) ; ref . editor . commands . addCommand ( { name : 'jumpToDef' , hint : 'Jump to defintion' , bindKey : { win : 'Ctrl-I' , mac : 'Command-I' } , exec : ( ) => { const pos = ref . editor . getCursorPosition ( ) ; const token = ref . editor . session . getTokenAt ( pos . row , pos . column ) ; if ( token . type !== 'variable' ) { return false ; } const target = token . value . trim ( ) . match ( /\*\*\s*?goto\s+?(\S*?)\s*?\*\*/i ) ; if ( ! target || target . length < 2 || target [ 1 ] . length < 1 ) { return false ; } const search = new Search ( ) ; search . set ( { needle : new RegExp ( '_.*?_\\s?\\(#' + target [ 1 ] + '\\)' , 'i' ) , start : { row : 0 , column : 0 } , regExp : true , } ) ; const match = search . find ( session ) ; if ( match ) { ref . editor . scrollToLine ( match . start . row , false , false ) ; return true ; } return false ; } , } ) ; if ( this . props . annotations ) { session . setAnnotations ( this . props . annotations ) ; } } } public onGutterClick ( event ) { const path = event . domEvent . path ; if ( ! path || ! path . length ) { return ; } const ICON_CLASSES = [ 'ace_error' , 'ace_warning' , 'ace_info' ] ; let isIcon = false ; for ( const c of path [ 0 ] . classList ) { if ( ICON_CLASSES . indexOf ( c ) !== - 1 ) { isIcon = true ; break ; } } if ( ! isIcon ) { return ; } const textInt = path [ 0 ] . textContent ; if ( textInt === null ) { throw new Error ( 'Could not parse gutter index' ) ; } const rowNum = parseInt ( textInt , 10 ) - 1 ; const annotations = event . editor . session . getAnnotations ( ) . map ( ( a ) => { if ( a . row !== rowNum ) { return null ; } const m = a . text . match ( /^\w+ (\d+):/ ) ; if ( ! m ) { return null ; } return parseInt ( m [ 1 ] , 10 ) ; } ) . filter ( ( n ? ) => n ) ; this . props . onAnnotationClick ( Array . from ( new Set ( annotations ) ) ) ; event . preventDefault ( ) ; } public onTextInserted ( event ) { if ( ! this . ace || event . isLocal && ! event . isRedo && ! event . isUndo ) { return ; } const session = this . ace . editor . session ; const doc = session . getDocument ( ) ; this . silentChange = true ; doc . insert ( doc . indexToPosition ( event . index , 0 ) , event . text ) ; this . silentChange = false ; if ( event . isLocal ) { const end = doc . indexToPosition ( event . index + event . text . length ) ; this . ace . editor . gotoLine ( end . row + 1 , end . column ) ; } } public onTextDeleted ( event ) { if ( ! this . ace || event . isLocal && ! event . isRedo && ! event . isUndo ) { return ; } const session = this . ace . editor . session ; const doc = session . getDocument ( ) ; this . silentChange = true ; const start = doc . indexToPosition ( event . index , 0 ) ; const end = doc . indexToPosition ( event . index + event . text . length , 0 ) ; doc . remove ( new Range ( start . row , start . column , end . row , end . column ) ) ; this . silentChange = false ; if ( event . isLocal ) { this . ace . editor . gotoLine ( start . row + 1 , start . column ) ; } } public componentWillReceiveProps ( newProps ) { if ( this . props . realtime ) { this . props . realtime . removeAllEventListeners ( ) ; } if ( this . ace ) { const row = this . ace . editor . getSelection ( ) . anchor . row ; if ( ! this . focused && newProps . scrollLineTarget !== row && newProps . scrollLineTargetTs > ( this . lineChangeTs || - 1 ) ) { this . ace . editor . gotoLine ( newProps . scrollLineTarget + 1 , 0 , true ) ; this . lineChangeTs = newProps . scrollLineTargetTs ; } } } public onChange ( text ) { if ( this . silentChange ) { return ; } if ( this . props . realtime ) { this . props . realtime . setText ( text ) ; } if ( this . props . onChange ) { this . props . onChange ( text ) ; } } public render ( ) { let editorText = 'Loading...' ; if ( this . props . realtime ) { editorText = this . props . realtime . getText ( ) ; } const classes = [ 'ace' ] ; if ( ! this . props . showLineNumbers ) { classes . push ( 'noLineNumbers' ) ; } if ( ! this . props . showSpellcheck ) { classes . push ( 'noSpellcheck' ) ; } return ( < AceEditor className = { classes . join ( ' ' ) } ref = { this . onRef . bind ( this ) } mode = "markdown" theme = "twilight" fontSize = { 20 } onBlur = { ( ) => this . focused = false } onChange = { ( text ) => this . onChange ( text ) } onFocus = { ( ) => this . focused = true } width = "100%" height = "100%" name = { 'editor' } value = { editorText } / > ) ; } }