import * as vscode from 'vscode' ; import { Plugin } from '../plugin' ; import { ThemeConfig , getConfigValue , CSS_LEFT , CSS_TOP } from '../config/config' ; export type ExplosionOrder = 'random' | 'sequential' | number ; export type BackgroundMode = 'mask' | 'image' ; export type GifMode = 'continue' | 'restart' ; export interface ExplosionConfig { enableExplosions : boolean ; maxExplosions : number ; explosionSize : number ; explosionFrequency : number ; explosionOffset : number ; explosionDuration : number ; customExplosions : string [ ] ; explosionOrder : ExplosionOrder ; backgroundMode : BackgroundMode ; gifMode : GifMode ; customCss ? : { [ key ] : string ; } ; } export class CursorExploder implements Plugin { private config : ExplosionConfig = { } as ExplosionConfig ; private activeDecorations : vscode . TextEditorDecorationType [ ] = [ ] ; private keystrokeCounter = - 1 ; private explosionIndex = - 1 ; private counterTimeout : NodeJS . Timer ; constructor ( public themeConfig ) { } onThemeChanged = ( theme ) => { this . themeConfig = theme ; this . initialize ( ) ; } ; activate = ( ) => { this . initialize ( ) ; } ; dispose = ( ) => { this . onPowermodeStop ( ) ; } ; public onPowermodeStart = ( combo ? ) => { } ; public onPowermodeStop = ( combo ? ) => { while ( this . activeDecorations . length > 0 ) { this . activeDecorations . shift ( ) . dispose ( ) ; } } ; public onDidChangeTextDocument = ( combo , powermode , event ) => { if ( ! this . config . enableExplosions || ! powermode ) { return ; } const changes = event . contentChanges [ 0 ] ; const left = changes && changes . text . length === 0 ; this . explode ( left ) ; } ; public onDidChangeConfiguration = ( config ) => { const newConfig = { customExplosions : getConfigValue < string [ ] > ( 'customExplosions' , config , this . themeConfig ) , enableExplosions : getConfigValue < boolean > ( 'enableExplosions' , config , this . themeConfig ) , maxExplosions : getConfigValue < number > ( 'maxExplosions' , config , this . themeConfig ) , explosionSize : getConfigValue < number > ( 'explosionSize' , config , this . themeConfig ) , explosionFrequency : getConfigValue < number > ( 'explosionFrequency' , config , this . themeConfig ) , explosionOffset : getConfigValue < number > ( 'explosionOffset' , config , this . themeConfig ) , explosionOrder : getConfigValue < ExplosionOrder > ( 'explosionOrder' , config , this . themeConfig ) , explosionDuration : getConfigValue < number > ( 'explosionDuration' , config , this . themeConfig ) , backgroundMode : getConfigValue < BackgroundMode > ( 'backgroundMode' , config , this . themeConfig ) , gifMode : getConfigValue < GifMode > ( 'gifMode' , config , this . themeConfig ) , customCss : getConfigValue < any > ( 'customCss' , config , this . themeConfig ) , } ; let changed = false ; Object . keys ( newConfig ) . forEach ( key => { if ( this . config [ key ] !== newConfig [ key ] ) { changed = true ; } } ) ; if ( ! changed ) { return ; } this . config = newConfig ; this . initialize ( ) ; } ; public initialize = ( ) => { this . dispose ( ) ; if ( ! this . config . enableExplosions ) { return ; } this . explosionIndex = - 1 ; this . keystrokeCounter = - 1 ; } ; private getExplosionDecoration = ( position ) : vscode . TextEditorDecorationType => { let explosions = this . config . customExplosions ; const explosion = this . pickExplosion ( explosions ) ; if ( ! explosion ) { return null ; } return this . createExplosionDecorationType ( explosion , position ) ; } ; private pickExplosion ( explosions ) { if ( ! explosions ) { return null ; } switch ( typeof this . config . explosionOrder ) { case 'string' : switch ( this . config . explosionOrder ) { case 'random' : this . explosionIndex = getRandomInt ( 0 , explosions . length ) ; break ; case 'sequential' : this . explosionIndex = ( this . explosionIndex + 1 ) % explosions . length ; break ; default : this . explosionIndex = 0 ; } break ; case 'number' : this . explosionIndex = Math . min ( explosions . length - 1 , Math . floor ( Math . abs ( this . config . explosionOrder as number ) ) ) ; default : break ; } return explosions [ this . explosionIndex ] ; } returns private createExplosionDecorationType = ( explosion , editorPosition ) : vscode . TextEditorDecorationType => { const leftValue = Math . floor ( ( this . config . explosionSize - 1 ) / 2 ) ; const topValue = this . config . explosionSize * this . config . explosionOffset ; const explosionUrl = this . getExplosionUrl ( explosion ) ; const backgroundCss = this . config . backgroundMode === 'mask' ? this . getMaskCssSettings ( explosionUrl ) : this . getBackgroundCssSettings ( explosionUrl ) ; const defaultCss = { position : 'absolute' , [ CSS_LEFT ] : `-${ leftValue }ch` , [ CSS_TOP ] : `-${ topValue }rem` , width : `${ this . config . explosionSize }ch` , height : `${ this . config . explosionSize }rem` , display : `inline-block` , [ 'z-index' ] : 1 , [ 'pointer-events' ] : 'none' , } ; const backgroundCssString = this . objectToCssString ( backgroundCss ) ; const defaultCssString = this . objectToCssString ( defaultCss ) ; const customCssString = this . objectToCssString ( this . config . customCss || { } ) ; return vscode . window . createTextEditorDecorationType ( < vscode . DecorationRenderOptions > { before : { contentText : '' , textDecoration : `none; ${ defaultCssString } ${ backgroundCssString } ${ customCssString }` , } , textDecoration : `none; position: relative;` , rangeBehavior : vscode . DecorationRangeBehavior . ClosedClosed , } ) ; } ; private getExplosionUrl ( explosion ) { if ( this . config . gifMode !== 'restart' ) { return explosion ; } if ( this . isUrl ( explosion ) ) { return `${ explosion }?timestamp=${ Date . now ( ) }` ; } else { return explosion . replace ( 'base64,' , `timestamp=${ Date . now ( ) };base64,` ) ; } } private isUrl ( value ) { return value . indexOf ( 'https' ) === 0 ; } private getBackgroundCssSettings ( explosion ) { return { 'background-repeat' : 'no-repeat' , 'background-size' : 'contain' , 'background-image' : `url("${ explosion }")` , } ; } private getMaskCssSettings ( explosion ) { return { 'background-color' : 'currentColor' , '-webkit-mask-repeat' : 'no-repeat' , '-webkit-mask-size' : 'contain' , '-webkit-mask-image' : `url("${ explosion }")` , filter : 'saturate(150%)' , } ; } private objectToCssString ( settings ) { let value = '' ; const cssString = Object . keys ( settings ) . map ( setting => { value = settings [ setting ] ; if ( typeof value === 'string' || typeof value === 'number' ) { return `${ setting }: ${ value };` ; } } ) . join ( ' ' ) ; return cssString ; } param { boolean } left private explode = ( left = false ) => { clearTimeout ( this . counterTimeout ) ; this . counterTimeout = setTimeout ( ( ) => { this . keystrokeCounter = - 1 ; } , 1000 ) ; if ( ++ this . keystrokeCounter % this . config . explosionFrequency !== 0 ) { return ; } const activeEditor = vscode . window . activeTextEditor ; const cursorPosition = vscode . window . activeTextEditor . selection . active ; const delta = left ? - 2 : 1 ; const newRange = new vscode . Range ( cursorPosition . with ( cursorPosition . line , cursorPosition . character ) , cursorPosition . with ( cursorPosition . line , Math . max ( 0 , cursorPosition . character + delta ) ) ) ; while ( this . activeDecorations . length >= this . config . maxExplosions ) { this . activeDecorations . shift ( ) . dispose ( ) ; } const decoration = this . getExplosionDecoration ( newRange . start ) ; if ( ! decoration ) { return ; } this . activeDecorations . push ( decoration ) ; if ( this . config . explosionDuration !== 0 ) { setTimeout ( ( ) => { decoration . dispose ( ) ; } , this . config . explosionDuration ) ; } activeEditor . setDecorations ( decoration , [ newRange ] ) ; } ; } function getRandomInt ( min , max ) { min = Math . ceil ( min ) ; max = Math . floor ( max ) ; return Math . floor ( Math . random ( ) * ( max - min ) ) + min ; }