import AppScope from '@/scopes/app' ; import { NonReactive , nonReactive } from '@/utils' ; import { MalVal , MalNode , expandExp , symbolFor as S , createList as L , keywordFor as K , getName , getMeta , isFunc , MalType , MalError , cloneExp , isSeq , isList , isSymbolFor , MalSeq , getEvaluated , getType , isNode , symbolFor , isVector , } from '@/mal/types' ; import { getMapValue , getFnInfo , applyParamModifier , copyDelimiters , replaceExp , getUIOuterInfo , getUIAnnotationExp , deleteExp , generateExpAbsPath , getExpByPath , } from '@/mal/utils' ; import { readStr } from '@/mal' ; import { toSketchCode } from '../utils' ; import printExp from '@/mal/printer' ; import ViewScope from '@/scopes/view' ; import { reconstructTree } from '@/mal/reader' ; export default function useAppCommands ( data , callbacks ) { AppScope . def ( 'expand-selected' , ( ) => { if ( ! data . activeExp ) { return false ; } const expanded = expandExp ( data . activeExp . value ) ; if ( expanded === undefined ) { return false ; } replaceExp ( data . activeExp . value , expanded ) ; return true ; } ) ; AppScope . def ( 'insert-item' , ( exp ) => { let activeExp = data . exp . value ; if ( data . activeExp ) { activeExp = data . activeExp . value ; } else { activeExp = data . exp . value ; } const type = getType ( exp ) ; if ( ! isSeq ( activeExp ) ) { throw new MalError ( 'No insertable selection' ) ; } let newExp ; if ( type === MalType . String || type === MalType . Symbol ) { const fnName = getName ( exp ) ; const fn = ViewScope . var ( fnName ) ; const meta = getMeta ( fn ) ; const returnType = ( getMapValue ( meta , 'return/type' , MalType . String ) as string ) || '' ; const initialParams = ( getMapValue ( meta , 'initial-params' , MalType . Vector ) as MalSeq ) || null ; if ( ! isFunc ( fn ) || ! [ 'item' , 'path' ] . includes ( returnType ) ) { throw new MalError ( `${ fnName } is not a function that returns item/path` ) ; } if ( ! initialParams ) { throw new MalError ( `Function ${ fnName } does not have the :initial-params field` ) ; } newExp = L ( S ( fnName ) , ... initialParams ) ; } else if ( type === MalType . List ) { newExp = exp as MalSeq ; } else { throw new MalError ( 'Invalid argument' ) ; } reconstructTree ( newExp ) ; const newActiveExp = cloneExp ( activeExp ) ; newActiveExp . push ( newExp ) ; copyDelimiters ( newActiveExp , activeExp ) ; replaceExp ( activeExp , newActiveExp ) ; return generateExpAbsPath ( newExp ) ; } ) ; AppScope . def ( 'replace-item' , ( path , replaced ) => { if ( typeof path !== 'string' ) { throw new Error ( 'Path should be string' ) ; } const original = getExpByPath ( data . exp . value , path ) ; if ( ! isNode ( original ) ) { throw new MalError ( 'The original should be node' ) ; } replaceExp ( original , replaced ) ; return path ; } ) ; AppScope . def ( 'select-items' , ( paths ) => { if ( isVector ( paths ) ) { const items = [ ] ; for ( const path of paths ) { if ( typeof path !== 'string' ) { return false ; } const item = getExpByPath ( data . exp . value , path ) ; if ( ! isNode ( item ) ) { return false ; } items . push ( nonReactive ( item ) ) ; } callbacks . setSelectedExp ( items ) ; } return false ; } ) ; AppScope . def ( 'item-selected?' , ( path ) => { if ( typeof path !== 'string' ) { return false ; } const item = getExpByPath ( data . exp . value , path ) ; if ( ! isNode ( item ) ) { return false ; } const index = data . selectedExp . findIndex ( s => s . value === item ) ; return index !== - 1 ; } ) ; AppScope . def ( 'toggle-item-selection' , ( path ) => { if ( typeof path !== 'string' ) { return false ; } const item = getExpByPath ( data . exp . value , path ) ; if ( ! isNode ( item ) ) { return false ; } const index = data . selectedExp . findIndex ( s => s . value === item ) ; const items = [ ... data . selectedExp ] ; if ( index === - 1 ) { items . push ( nonReactive ( item ) ) ; } else { items . splice ( index , 1 ) ; } callbacks . setSelectedExp ( items ) ; return true ; } ) ; AppScope . def ( 'load-file' , ( url ) => { fetch ( url as string ) . then ( async ( res ) => { if ( res . ok ) { const code = await res . text ( ) ; const exp = readStr ( toSketchCode ( code ) ) as MalNode ; const nonReactiveExp = nonReactive ( exp ) ; callbacks . updateExp ( nonReactiveExp ) ; callbacks . setActiveExp ( null ) ; data . editingExp = nonReactiveExp ; } else { throw new MalError ( `Failed to load from "${ url }"` ) ; } } ) ; return null ; } ) ; AppScope . def ( 'select-outer' , ( ) => { if ( ! data . activeExp ) { throw new MalError ( 'No selection' ) ; } const [ outer ] = getUIOuterInfo ( data . activeExp . value ) ; if ( outer && outer !== data . exp ?. value ) { callbacks . setActiveExp ( nonReactive ( outer ) ) ; } return true ; } ) ; AppScope . def ( 'wrap-selected' , ( wrapper ) => { if ( ! data . activeExp ) { throw new MalError ( 'No selection' ) ; } if ( ! isList ( wrapper ) ) { throw new MalError ( `${ printExp ( wrapper ) } is not a list` ) ; } const exp = data . activeExp . value ; let shouldDuplicate = false ; const newExp = L ( ... wrapper . map ( e => { if ( isSymbolFor ( e , '%' ) ) { const ret = shouldDuplicate ? cloneExp ( exp , true ) : exp ; shouldDuplicate = true ; return ret ; } else { return e ; } } ) ) ; reconstructTree ( newExp ) ; replaceExp ( data . activeExp . value , newExp ) ; return true ; } ) ; AppScope . def ( 'transform-selected' , ( xform ) => { for ( const { value : exp } of data . selectedExp ) { if ( ! isSeq ( exp ) ) { return false ; } const fnInfo = getFnInfo ( exp ) ; if ( ! fnInfo ) { return false ; } const { meta , structType } = fnInfo ; const transformFn = getMapValue ( meta , 'transform' ) ; if ( ! isFunc ( transformFn ) ) { return false ; } const originalParams = structType ? [ exp ] : exp . slice ( 1 ) ; const payload = { [ K ( 'params' ) ] : originalParams . map ( p => getEvaluated ( p ) ) , [ K ( 'transform' ) ] : xform as MalVal , } ; const modifier = transformFn ( payload ) ; let newParams ; if ( structType ) { newParams = modifier as MalSeq ; } else { newParams = applyParamModifier ( modifier , originalParams ) ; if ( ! newParams ) { return false ; } } const newExp = structType ? newParams [ 0 ] : L ( exp [ 0 ] , ... newParams ) ; copyDelimiters ( newExp , exp ) ; replaceExp ( exp , newExp ) ; } return true ; } ) ; AppScope . def ( 'copy-selected' , ( ) => { if ( ! data . activeExp ) { throw new MalError ( 'No selection' ) ; } const code = printExp ( data . activeExp . value ) ; navigator . clipboard . writeText ( code ) ; return true ; } ) ; AppScope . def ( 'paste-from-clipboard' , ( ) => { let outer , index ; if ( ! data . activeExp ) { ; [ outer , index ] = [ data . exp . value as MalSeq , ( data . exp . value as MalSeq ) . length - 1 , ] ; } else { const [ _outer , _index ] = getUIOuterInfo ( data . activeExp . value ) ; if ( ! isSeq ( _outer ) ) { return false ; } ; [ outer , index ] = [ _outer , _index ] ; } const newOuter = cloneExp ( outer ) ; navigator . clipboard . readText ( ) . then ( ( str ) => { const exp = readStr ( str ) ; newOuter . splice ( index + 1 , 0 , exp ) ; copyDelimiters ( newOuter , outer ) ; reconstructTree ( newOuter ) ; replaceExp ( outer , newOuter ) ; callbacks . setActiveExp ( isNode ( exp ) ? nonReactive ( exp ) : null ) ; } ) ; return null ; } ) ; AppScope . def ( 'delete-selected' , ( ) => { for ( const _exp of data . selectedExp ) { const exp = getUIAnnotationExp ( _exp . value ) ; deleteExp ( exp ) ; } callbacks . setSelectedExp ( [ ] ) ; return true ; } ) ; AppScope . def ( 'group-selected' , ( ) => { if ( data . selectedExp . length === 0 ) { return false ; } const [ first , ... rest ] = data . selectedExp . map ( e => e . value ) ; for ( const exp of rest ) { deleteExp ( getUIAnnotationExp ( exp ) ) ; } const group = L ( symbolFor ( 'g' ) , { } , first , ... rest ) ; replaceExp ( first , group ) ; callbacks . setActiveExp ( nonReactive ( group ) ) ; return true ; } ) ; }