import * as actionTypes from "@nteract/actions" ; import { CellId , createFrozenMediaBundle , createImmutableOutput , deleteCell , emptyCodeCell , emptyMarkdownCell , emptyNotebook , ImmutableCell , ImmutableCodeCell , ImmutableNotebook , ImmutableOutput , insertCellAfter , insertCellAt , makeCodeCell , makeMarkdownCell , makeRawCell , markCellDeleting , markCellNotDeleting , OnDiskDisplayData , OnDiskExecuteResult , OnDiskOutput , OnDiskStreamOutput , } from "@nteract/commutable" ; import { UpdateDisplayDataContent } from "@nteract/messaging" ; import { DocumentRecordProps , makeDocumentRecord , NotebookModel , PayloadMessage , } from "@nteract/types" ; import { escapeCarriageReturnSafe } from "escape-carriage" ; import { fromJS , List , Map , RecordOf , Set } from "immutable" ; import has from "lodash.has" ; import { v4 as uuid } from "uuid" ; type KeyPath = List < string | number > ; type KeyPaths = List < KeyPath > ; param { Object } outputs param { Object } output return { List < Object > } export function reduceOutputs ( outputs = List ( ) , output ) { const last = outputs . last ( null ) ; if ( ! last || ! last . output_type ) { return outputs . push ( createImmutableOutput ( output ) ) ; } if ( output . output_type !== "stream" || last . output_type !== "stream" ) { return outputs . push ( createImmutableOutput ( output ) ) ; } const streamOutput = output ; if ( typeof streamOutput . name === "undefined" ) { return outputs . push ( createImmutableOutput ( streamOutput ) ) ; } function appendText ( text ) { if ( typeof streamOutput . text === "string" ) { return escapeCarriageReturnSafe ( text + streamOutput . text ) ; } return text ; } if ( last . name === streamOutput . name ) { return outputs . updateIn ( [ outputs . size - 1 , "text" ] , appendText ) ; } const nextToLast = outputs . butLast ( ) . last ( null ) ; if ( nextToLast && nextToLast . output_type === "stream" && nextToLast . name === streamOutput . name ) { return outputs . updateIn ( [ outputs . size - 2 , "text" ] , appendText ) ; } return outputs . push ( createImmutableOutput ( streamOutput ) ) ; } export function cleanCellTransient ( state , id ) { return state . setIn ( [ "cellPagers" , id ] , List ( ) ) . updateIn ( [ "transient" , "keyPathsForDisplays" ] , ( kpfd ) => ( kpfd || Map ( ) ) . map ( ( keyPaths ) => keyPaths . filter ( ( keyPath ) => keyPath . get ( 2 ) !== id ) ) ) . setIn ( [ "transient" , "cellMap" , id ] , Map ( ) ) ; } function setNotebookCheckpoint ( state ) { return state . set ( "savedNotebook" , state . get ( "notebook" ) ) ; } function focusCell ( state , action ) { return state . set ( "cellFocused" , action . payload . id ) ; } function clearOutputs ( state , action ) { const id = action . payload . id ? action . payload . id : state . cellFocused ; if ( ! id ) { return state ; } const type = state . getIn ( [ "notebook" , "cellMap" , id , "cell_type" ] ) ; const cleanedState = cleanCellTransient ( state , id ) ; if ( type === "code" ) { return cleanedState . setIn ( [ "notebook" , "cellMap" , id , "outputs" ] , List ( ) ) . setIn ( [ "notebook" , "cellMap" , id , "execution_count" ] , null ) . setIn ( [ "cellPrompts" , id ] , List ( ) ) ; } return cleanedState ; } function toggleTagInCell ( state , action ) { const { id , tag } = action . payload ; return state . updateIn ( [ "notebook" , "cellMap" , id , "metadata" , "tags" ] , ( tags ) => { if ( tags ) { return tags . has ( tag ) ? tags . remove ( tag ) : tags . add ( tag ) ; } else { return Set ( [ tag ] ) ; } } ) ; } function clearAllOutputs ( state , action ) { if ( action . type === actionTypes . RESTART_KERNEL && action . payload . outputHandling !== "Clear All" ) { return state ; } const cellMap = state . getIn ( [ "notebook" , "cellMap" ] ) . map ( ( cell ) => { if ( ( cell as any ) . get ( "cell_type" ) === "code" ) { return ( cell as ImmutableCodeCell ) . merge ( { outputs : List ( ) , execution_count : null , } ) ; } return cell ; } ) ; const transient = Map ( { keyPathsForDisplays : Map ( ) , cellMap : cellMap . map ( ( ) => Map ( ) ) , } ) ; return state . setIn ( [ "notebook" , "cellMap" ] , cellMap ) . set ( "transient" , transient ) . set ( "cellPrompts" , Map ( ) ) ; } type UpdatableOutputContent = OnDiskExecuteResult | OnDiskDisplayData | UpdateDisplayDataContent ; function updateAllDisplaysWithID ( state , content ) { if ( ! content || ! content . transient || ! content . transient . display_id ) { return state ; } const keyPaths = state . getIn ( [ "transient" , "keyPathsForDisplays" , content . transient . display_id , ] ) || List ( ) ; const updateOutput = ( output ) => { if ( output ) { return output . merge ( { data : createFrozenMediaBundle ( content . data ) , metadata : fromJS ( content . metadata || { } ) , } ) ; } else if ( content . output_type === "update_display_data" ) { return { data : createFrozenMediaBundle ( content . data ) , metadata : fromJS ( content . metadata || { } ) , output_type : "display_data" , } ; } else { return createImmutableOutput ( content ) ; } } ; const updateOneDisplay = ( currState , keyPath ) => currState . updateIn ( keyPath , updateOutput ) ; return keyPaths . reduce ( updateOneDisplay , state ) ; } function appendOutput ( state , action ) { const output = action . payload . output ; const cellId = action . payload . id ; /**
     * If it is not a display_data or execute_result with
     * a display_id, then treat it as a normal output and don't
     * add its index to the keyPaths.
     */ if ( ( output . output_type !== "execute_result" && output . output_type !== "display_data" ) || ! has ( output , "transient.display_id" ) ) { return state . updateIn ( [ "notebook" , "cellMap" , cellId , "outputs" ] , ( outputs ) : List < ImmutableOutput > => reduceOutputs ( outputs , output ) ) ; } const displayID = output . transient ! . display_id ; if ( ! displayID || typeof displayID !== "string" ) { return state ; } const outputIndex = state . getIn ( [ "notebook" , "cellMap" , cellId , "outputs" ] ) . count ( ) ; const keyPath = List ( [ "notebook" , "cellMap" , cellId , "outputs" , outputIndex , ] ) ; const keyPaths = ( state . getIn ( [ "transient" , "keyPathsForDisplays" , displayID ] ) || List ( ) ) . push ( keyPath ) ; return updateAllDisplaysWithID ( state . setIn ( [ "transient" , "keyPathsForDisplays" , displayID ] , keyPaths ) , output ) ; } function updateDisplay ( state , action ) { return updateAllDisplaysWithID ( state , action . payload . content ) ; } function focusNextCell ( state , action ) { const cellOrder = state . getIn ( [ "notebook" , "cellOrder" ] ) ; const id = action . payload . id ? action . payload . id : state . get ( "cellFocused" ) ; if ( ! id ) { return state ; } const curIndex = cellOrder . findIndex ( ( foundId ) => id === foundId ) ; const curCellType = state . getIn ( [ "notebook" , "cellMap" , id , "cell_type" ] ) ; const nextIndex = curIndex + 1 ; if ( nextIndex >= cellOrder . size ) { if ( ! action . payload . createCellIfUndefined ) { return state ; } const cellId = uuid ( ) ; const cell = curCellType === "code" ? emptyCodeCell : emptyMarkdownCell ; const notebook = state . get ( "notebook" ) ; return state . set ( "cellFocused" , cellId ) . set ( "notebook" , insertCellAt ( notebook , cell , cellId , nextIndex ) ) ; } return state . set ( "cellFocused" , cellOrder . get ( nextIndex ) ) ; } function focusPreviousCell ( state , action ) { const cellOrder = state . getIn ( [ "notebook" , "cellOrder" ] ) ; const curIndex = cellOrder . findIndex ( ( id ) => id === action . payload . id ) ; const nextIndex = Math . max ( 0 , curIndex - 1 ) ; return state . set ( "cellFocused" , cellOrder . get ( nextIndex ) ) ; } function focusCellEditor ( state , action ) { return state . set ( "editorFocused" , action . payload . id ) ; } function focusNextCellEditor ( state , action ) { const cellOrder = state . getIn ( [ "notebook" , "cellOrder" ] ) ; const id = action . payload . id ? action . payload . id : state . get ( "editorFocused" ) ; if ( ! id ) { return state ; } const curIndex = cellOrder . findIndex ( ( foundId ) => id === foundId ) ; const nextIndex = curIndex + 1 ; return state . set ( "editorFocused" , cellOrder . get ( nextIndex ) ) ; } function focusPreviousCellEditor ( state , action ) { const cellOrder = state . getIn ( [ "notebook" , "cellOrder" ] ) ; const curIndex = cellOrder . findIndex ( ( id ) => id === action . payload . id ) ; const nextIndex = Math . max ( 0 , curIndex - 1 ) ; return state . set ( "editorFocused" , cellOrder . get ( nextIndex ) ) ; } function moveCell ( state , action ) { return state . updateIn ( [ "notebook" , "cellOrder" ] , ( cellOrder ) => { const oldIndex = cellOrder . findIndex ( ( id ) => id === action . payload . id ) ; const newIndex = cellOrder . findIndex ( ( id ) => id === action . payload . destinationId ) + ( action . payload . above ? 0 : 1 ) ; if ( oldIndex === newIndex ) { return cellOrder ; } return cellOrder . splice ( oldIndex , 1 ) . splice ( newIndex - ( oldIndex < newIndex ? 1 : 0 ) , 0 , action . payload . id ) ; } ) ; } function markCellAsDeleting ( state , action ) { const id = action . payload . id ? action . payload . id : state . cellFocused ; if ( ! id ) { return state ; } return state . update ( "notebook" , ( notebook ) => markCellDeleting ( notebook , id ) ) ; } function unmarkCellAsDeleting ( state , action ) { const id = action . payload . id ? action . payload . id : state . cellFocused ; if ( ! id ) { return state ; } return state . update ( "notebook" , ( notebook ) => markCellNotDeleting ( notebook , id ) ) ; } function deleteCellFromState ( state , action ) { const id = action . payload . id ? action . payload . id : state . cellFocused ; if ( ! id ) { return state ; } return cleanCellTransient ( state . update ( "notebook" , ( notebook ) => deleteCell ( notebook , id ) ) , id ) ; } function createCellBelow ( state , action ) { const id = action . payload . id ? action . payload . id : state . cellFocused ; if ( ! id ) { return state ; } const { cellType } = action . payload ; let cell = cellType === "markdown" ? emptyMarkdownCell : emptyCodeCell ; if ( action . payload . cell ) { cell = action . payload . cell ; } const cellId = uuid ( ) ; return state . update ( "notebook" , ( notebook ) => { const index = notebook . get ( "cellOrder" , List ( ) ) . indexOf ( id ) + 1 ; return insertCellAt ( notebook , cell , cellId , index ) ; } ) ; } function createCellAbove ( state , action ) { const id = action . payload . id ? action . payload . id : state . cellFocused ; if ( ! id ) { return state ; } const { cellType } = action . payload ; let cell = cellType === "markdown" ? emptyMarkdownCell : emptyCodeCell ; if ( action . payload . cell ) { cell = action . payload . cell ; } const cellId = uuid ( ) ; return state . update ( "notebook" , ( notebook ) => { const cellOrder = notebook . get ( "cellOrder" , List ( ) ) ; const index = cellOrder . indexOf ( id ) ; return insertCellAt ( notebook , cell , cellId , index ) ; } ) ; } function createCellAppend ( state , action ) { const { cellType } = action . payload ; const notebook = state . get ( "notebook" ) ; const cellOrder = notebook . get ( "cellOrder" , List ( ) ) ; const cell = cellType === "markdown" ? emptyMarkdownCell : emptyCodeCell ; const index = cellOrder . count ( ) ; const cellId = uuid ( ) ; return state . set ( "notebook" , insertCellAt ( notebook , cell , cellId , index ) ) ; } function acceptPayloadMessage ( state , action ) { const id = action . payload . id ; const payload = action . payload . payload ; if ( payload . source === "page" ) { return state . updateIn ( [ "cellPagers" , id ] , ( l ) => ( l || List ( ) ) . push ( payload . data ) ) ; } else if ( payload . source === "set_next_input" ) { if ( payload . replace ) { return state . setIn ( [ "notebook" , "cellMap" , id , "source" ] , payload . text ) ; } else { return createCellBelow ( state , { type : actionTypes . CREATE_CELL_BELOW , payload : { cellType : "code" , cell : emptyCodeCell . setIn ( "source" , payload . text || "" ) , id , contentRef : action . payload . contentRef , } , } ) ; } } return state ; } function setInCell ( state , action ) { return state . setIn ( [ "notebook" , "cellMap" , action . payload . id ] . concat ( action . payload . path ) , action . payload . value ) ; } function toggleCellOutputVisibility ( state , action ) { const id = action . payload . id ? action . payload . id : state . cellFocused ; if ( ! id ) { return state ; } return state . setIn ( [ "notebook" , "cellMap" , id , "metadata" , "jupyter" , "outputs_hidden" ] , ! state . getIn ( [ "notebook" , "cellMap" , id , "metadata" , "jupyter" , "outputs_hidden" , ] ) ) ; } function unhideAll ( state , action ) { const { outputHidden , inputHidden } = action . payload ; let metadataMixin = Map < string , boolean > ( ) ; if ( outputHidden !== undefined ) { metadataMixin = metadataMixin . set ( "outputs_hidden" , outputHidden ) ; } if ( inputHidden !== undefined ) { metadataMixin = metadataMixin . set ( "source_hidden" , inputHidden ) ; } return state . updateIn ( [ "notebook" , "cellMap" ] , ( cellMap ) => cellMap . map ( ( cell ) => { if ( ( cell as any ) . get ( "cell_type" ) === "code" ) { return cell . mergeIn ( [ "metadata" , "jupyter" ] , metadataMixin ) ; } return cell ; } ) ) ; } function toggleCellInputVisibility ( state , action ) { const id = action . payload . id ? action . payload . id : state . cellFocused ; if ( ! id ) { return state ; } return state . setIn ( [ "notebook" , "cellMap" , id , "metadata" , "jupyter" , "source_hidden" ] , ! state . getIn ( [ "notebook" , "cellMap" , id , "metadata" , "jupyter" , "source_hidden" , ] ) ) ; } function updateCellStatus ( state , action ) { const { id , status } = action . payload ; return state . setIn ( [ "transient" , "cellMap" , id , "status" ] , status ) ; } function updateOutputMetadata ( state , action ) { const { id , metadata , index , mediaType } = action . payload ; const currentOutputs = state . getIn ( [ "notebook" , "cellMap" , id , "outputs" ] ) ; const updatedOutputs = currentOutputs . update ( index , ( item ) => item . set ( "metadata" , fromJS ( { [ mediaType ] : metadata , } ) ) ) ; return state . setIn ( [ "notebook" , "cellMap" , id , "outputs" ] , updatedOutputs ) ; } function setLanguageInfo ( state , action ) { const langInfo = fromJS ( action . payload . langInfo ) ; return state . setIn ( [ "notebook" , "metadata" , "language_info" ] , langInfo ) ; } function setKernelMetadata ( state , action ) { const { kernelInfo } = action . payload ; if ( kernelInfo ) { return state . setIn ( [ "notebook" , "metadata" , "kernelspec" ] , fromJS ( { name : kernelInfo . name , language : kernelInfo . language , display_name : kernelInfo . displayName , } ) ) . setIn ( [ "notebook" , "metadata" , "kernel_info" , "name" ] , kernelInfo . name ) ; } return state ; } function overwriteMetadataField ( state , action ) { const { field , value } = action . payload ; return state . setIn ( [ "notebook" , "metadata" , field ] , fromJS ( value ) ) ; } function deleteMetadataField ( state , action ) { const { field } = action . payload ; return state . deleteIn ( [ "notebook" , "metadata" , field ] ) ; } function copyCell ( state , action ) { const id = action . payload . id || state . cellFocused ; const cell = state . getIn ( [ "notebook" , "cellMap" , id ] ) ; if ( ! cell ) { return state ; } return state . set ( "copied" , cell ) ; } function cutCell ( state , action ) { const id = action . payload . id ? action . payload . id : state . cellFocused ; if ( ! id ) { return state ; } const cell = state . getIn ( [ "notebook" , "cellMap" , id ] ) ; if ( ! cell ) { return state ; } return state . set ( "copied" , cell ) . update ( "notebook" , ( notebook ) => deleteCell ( notebook , id ) ) ; } function pasteCell ( state ) { const copiedCell = state . get ( "copied" ) ; const pasteAfter = state . cellFocused ; if ( ! copiedCell || ! pasteAfter ) { return state ; } const id = uuid ( ) ; return state . update ( "notebook" , ( notebook ) => insertCellAfter ( notebook , copiedCell , id , pasteAfter ) ) ; } function changeCellType ( state , action ) { const id = action . payload . id ? action . payload . id : state . cellFocused ; if ( ! id ) { return state ; } const { to } = action . payload ; const cell = state . getIn ( [ "notebook" , "cellMap" , id ] ) ; const from = cell . cell_type ; if ( from === to ) { return state ; } let nextState = state ; if ( from === "code" ) { nextState = cleanCellTransient ( state . deleteIn ( [ "notebook" , "cellMap" , id , "execution_count" ] ) . deleteIn ( [ "notebook" , "cellMap" , id , "outputs" ] ) , id ) ; } switch ( to ) { case "code" : return nextState . setIn ( [ "notebook" , "cellMap" , id ] , makeCodeCell ( { source : cell . source , } ) ) ; case "markdown" : return nextState . setIn ( [ "notebook" , "cellMap" , id ] , makeMarkdownCell ( { source : cell . source , } ) ) ; case "raw" : return nextState . setIn ( [ "notebook" , "cellMap" , id ] , makeRawCell ( { source : cell . source , } ) ) ; } return state ; } function toggleOutputExpansion ( state , action ) { const id = action . payload . id ? action . payload . id : state . cellFocused ; if ( ! id ) { return state ; } return state . updateIn ( [ "notebook" , "cellMap" ] , ( cells ) => { const scrolled = cells . getIn ( [ id , "metadata" , "scrolled" ] ) ; const isCurrentlyScrolled = scrolled !== false ; return cells . setIn ( [ id , "metadata" , "scrolled" ] , ! isCurrentlyScrolled ) ; } ) ; } function promptInputRequest ( state , action ) { const { id , password , prompt } = action . payload ; return state . updateIn ( [ "cellPrompts" , id ] , ( prompts ) => prompts . push ( { prompt , password , } ) ) ; } function interruptKernelSuccessful ( state , action ) { return state . updateIn ( [ "transient" , "cellMap" ] , ( cells ) => cells . map ( ( cell ) => { if ( cell . get ( "status" ) === "queued" || cell . get ( "status" ) === "running" ) { return cell . set ( "status" , "" ) ; } return cell ; } ) ) ; } type DocumentAction = actionTypes . ToggleTagInCell | actionTypes . FocusPreviousCellEditor | actionTypes . FocusPreviousCell | actionTypes . FocusNextCellEditor | actionTypes . FocusNextCell | actionTypes . FocusCellEditor | actionTypes . FocusCell | actionTypes . ClearOutputs | actionTypes . AppendOutput | actionTypes . UpdateDisplay | actionTypes . MoveCell | actionTypes . MarkCellAsDeleting | actionTypes . UnmarkCellAsDeleting | actionTypes . DeleteCell | actionTypes . CreateCellBelow | actionTypes . CreateCellAbove | actionTypes . CreateCellAppend | actionTypes . ToggleCellOutputVisibility | actionTypes . ToggleCellInputVisibility | actionTypes . UpdateCellStatus | actionTypes . UpdateOutputMetadata | actionTypes . SetLanguageInfo | actionTypes . SetKernelMetadata | actionTypes . OverwriteMetadataField | actionTypes . DeleteMetadataField | actionTypes . CopyCell | actionTypes . CutCell | actionTypes . PasteCell | actionTypes . ChangeCellType | actionTypes . ToggleCellExpansion | actionTypes . AcceptPayloadMessage | actionTypes . SendExecuteRequest | actionTypes . SaveFulfilled | actionTypes . RestartKernel | actionTypes . ClearAllOutputs | actionTypes . SetInCell < any > | actionTypes . UnhideAll | actionTypes . PromptInputRequest | actionTypes . InterruptKernelSuccessful ; const defaultDocument = makeDocumentRecord ( { notebook : emptyNotebook , } ) ; export function notebook ( state = defaultDocument , action ) { switch ( action . type ) { case actionTypes . TOGGLE_TAG_IN_CELL : return toggleTagInCell ( state , action ) ; case actionTypes . SAVE_FULFILLED : return setNotebookCheckpoint ( state ) ; case actionTypes . FOCUS_CELL : return focusCell ( state , action ) ; case actionTypes . CLEAR_OUTPUTS : return clearOutputs ( state , action ) ; case actionTypes . CLEAR_ALL_OUTPUTS : case actionTypes . RESTART_KERNEL : return clearAllOutputs ( state , action ) ; case actionTypes . APPEND_OUTPUT : return appendOutput ( state , action ) ; case actionTypes . UPDATE_DISPLAY : return updateDisplay ( state , action ) ; case actionTypes . FOCUS_NEXT_CELL : return focusNextCell ( state , action ) ; case actionTypes . FOCUS_PREVIOUS_CELL : return focusPreviousCell ( state , action ) ; case actionTypes . FOCUS_CELL_EDITOR : return focusCellEditor ( state , action ) ; case actionTypes . FOCUS_NEXT_CELL_EDITOR : return focusNextCellEditor ( state , action ) ; case actionTypes . FOCUS_PREVIOUS_CELL_EDITOR : return focusPreviousCellEditor ( state , action ) ; case actionTypes . SET_IN_CELL : return setInCell ( state , action ) ; case actionTypes . MOVE_CELL : return moveCell ( state , action ) ; case actionTypes . MARK_CELL_AS_DELETING : return markCellAsDeleting ( state , action ) ; case actionTypes . UNMARK_CELL_AS_DELETING : return unmarkCellAsDeleting ( state , action ) ; case actionTypes . DELETE_CELL : return deleteCellFromState ( state , action ) ; case actionTypes . CREATE_CELL_BELOW : return createCellBelow ( state , action ) ; case actionTypes . CREATE_CELL_ABOVE : return createCellAbove ( state , action ) ; case actionTypes . CREATE_CELL_APPEND : return createCellAppend ( state , action ) ; case actionTypes . TOGGLE_CELL_OUTPUT_VISIBILITY : return toggleCellOutputVisibility ( state , action ) ; case actionTypes . TOGGLE_CELL_INPUT_VISIBILITY : return toggleCellInputVisibility ( state , action ) ; case actionTypes . ACCEPT_PAYLOAD_MESSAGE : return acceptPayloadMessage ( state , action ) ; case actionTypes . UPDATE_CELL_STATUS : return updateCellStatus ( state , action ) ; case actionTypes . UPDATE_OUTPUT_METADATA : return updateOutputMetadata ( state , action ) ; case actionTypes . SET_LANGUAGE_INFO : return setLanguageInfo ( state , action ) ; case actionTypes . SET_KERNEL_METADATA : return setKernelMetadata ( state , action ) ; case actionTypes . OVERWRITE_METADATA_FIELD : return overwriteMetadataField ( state , action ) ; case actionTypes . DELETE_METADATA_FIELD : return deleteMetadataField ( state , action ) ; case actionTypes . COPY_CELL : return copyCell ( state , action ) ; case actionTypes . CUT_CELL : return cutCell ( state , action ) ; case actionTypes . PASTE_CELL : return pasteCell ( state ) ; case actionTypes . CHANGE_CELL_TYPE : return changeCellType ( state , action ) ; case actionTypes . TOGGLE_OUTPUT_EXPANSION : return toggleOutputExpansion ( state , action ) ; case actionTypes . UNHIDE_ALL : return unhideAll ( state , action ) ; case actionTypes . PROMPT_INPUT_REQUEST : return promptInputRequest ( state , action ) ; case actionTypes . INTERRUPT_KERNEL_SUCCESSFUL : return interruptKernelSuccessful ( state , action ) ; default : return state ; } }