return Math.ceil(width / 4) * Math.ceil(height / 4) * 16;

/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2021 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

/**
 * @ignore
 */
function GetSize (width, height, x, y, dx, dy, mult)
{
    if (mult === undefined) { mult = 16; }

    return Math.floor((width + x) / dx) * Math.floor((height + y) / dy) * mult;
}

/**
 * @ignore
 */
function PVRTC2bppSize (width, height)
{
    width = Math.max(width, 16);
    height = Math.max(height, 8);

    return width * height / 4;
}

/**
 * @ignore
 */
function PVRTC4bppSize (width, height)
{
    width = Math.max(width, 8);
    height = Math.max(height, 8);

    return width * height / 2;
}

/**
 * @ignore
 */
function DXTEtcSmallSize (width, height)
{
    return GetSize(width, height, 3, 3, 4, 4, 8);
}

/**
 * @ignore
 */
function DXTEtcAstcBigSize (width, height)
{
    return GetSize(width, height, 3, 3, 4, 4);
}

/**
 * @ignore
 */
function ATC5x4Size (width, height)
{
    return GetSize(width, height, 4, 3, 5, 4);
}

/**
 * @ignore
 */
function ATC5x5Size (width, height)
{
    return GetSize(width, height, 4, 4, 5, 5);
}

/**
 * @ignore
 */
function ATC6x5Size (width, height)
{
    return GetSize(width, height, 5, 4, 6, 5);
}

/**
 * @ignore
 */
function ATC6x6Size (width, height)
{
    return GetSize(width, height, 5, 5, 6, 6);
}

/**
 * @ignore
 */
function ATC8x5Size (width, height)
{
    return GetSize(width, height, 7, 4, 8, 5);
}

/**
 * @ignore
 */
function ATC8x6Size (width, height)
{
    return GetSize(width, height, 7, 5, 8, 6);
}

/**
 * @ignore
 */
function ATC8x8Size (width, height)
{
    return GetSize(width, height, 7, 7, 8, 8);
}

/**
 * @ignore
 */
function ATC10x5Size (width, height)
{
    return GetSize(width, height, 9, 4, 10, 5);
}

/**
 * @ignore
 */
function ATC10x6Size (width, height)
{
    return GetSize(width, height, 9, 5, 10, 6);
}

/**
 * @ignore
 */
function ATC10x8Size (width, height)
{
    return GetSize(width, height, 9, 7, 10, 8);
}

/**
 * @ignore
 */
function ATC10x10Size (width, height)
{
    return GetSize(width, height, 9, 9, 10, 10);
}

/**
 * @ignore
 */
function ATC12x10Size (width, height)
{
    return GetSize(width, height, 11, 9, 12, 10);
}

/**
 * @ignore
 */
function ATC12x12Size (width, height)
{
    return GetSize(width, height, 11, 11, 12, 12);
}

/**
 * 0: COMPRESSED_RGB_PVRTC_2BPPV1_IMG
 * 1: COMPRESSED_RGBA_PVRTC_2BPPV1_IMG
 * 2: COMPRESSED_RGB_PVRTC_4BPPV1_IMG
 * 3: COMPRESSED_RGBA_PVRTC_4BPPV1_IMG
 * 6: COMPRESSED_RGB_ETC1
 * 7: COMPRESSED_RGB_S3TC_DXT1_EXT or COMPRESSED_SRGB_S3TC_DXT1_EXT
 * 8: COMPRESSED_RGBA_S3TC_DXT1_EXT or COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT
 * 9: COMPRESSED_RGBA_S3TC_DXT3_EXT or COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT
 * 11: COMPRESSED_RGBA_S3TC_DXT5_EXT or COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT
 * 22: COMPRESSED_RGB8_ETC2
 * 23: COMPRESSED_RGBA8_ETC2_EAC
 * 24: COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2
 * 25: COMPRESSED_R11_EAC
 * 26: COMPRESSED_RG11_EAC
 * 27: COMPRESSED_RGBA_ASTC_4x4_KHR or COMPRESSED_SRGB8_ALPHA8_ASTC_4X4_KHR
 * 28: COMPRESSED_RGBA_ASTC_5x4_KHR or COMPRESSED_SRGB8_ALPHA8_ASTC_5X4_KHR
 * 29: COMPRESSED_RGBA_ASTC_5x5_KHR or COMPRESSED_SRGB8_ALPHA8_ASTC_5X5_KHR
 * 30: COMPRESSED_RGBA_ASTC_6x5_KHR or COMPRESSED_SRGB8_ALPHA8_ASTC_6X5_KHR
 * 31: COMPRESSED_RGBA_ASTC_6x6_KHR or COMPRESSED_SRGB8_ALPHA8_ASTC_6X6_KHR
 * 32: COMPRESSED_RGBA_ASTC_8x5_KHR or COMPRESSED_SRGB8_ALPHA8_ASTC_8X5_KHR
 * 33: COMPRESSED_RGBA_ASTC_8x6_KHR or COMPRESSED_SRGB8_ALPHA8_ASTC_8X6_KHR
 * 34: COMPRESSED_RGBA_ASTC_8x8_KHR or COMPRESSED_SRGB8_ALPHA8_ASTC_8X8_KHR
 * 35: COMPRESSED_RGBA_ASTC_10x5_KHR or COMPRESSED_SRGB8_ALPHA8_ASTC_10X5_KHR
 * 36: COMPRESSED_RGBA_ASTC_10x6_KHR or COMPRESSED_SRGB8_ALPHA8_ASTC_10X6_KHR
 * 37: COMPRESSED_RGBA_ASTC_10x8_KHR or COMPRESSED_SRGB8_ALPHA8_ASTC_10X8_KHR
 * 38: COMPRESSED_RGBA_ASTC_10x10_KHR or COMPRESSED_SRGB8_ALPHA8_ASTC_10X10_KHR
 * 39: COMPRESSED_RGBA_ASTC_12x10_KHR or COMPRESSED_SRGB8_ALPHA8_ASTC_12X10_KHR
 * 40: COMPRESSED_RGBA_ASTC_12x12_KHR or COMPRESSED_SRGB8_ALPHA8_ASTC_12X12_KHR
 */

/**
 * @ignore
 */
var FORMATS = {
    0: { sizeFunc: PVRTC2bppSize, glFormat: [ 0x8C01 ] },
    1: { sizeFunc: PVRTC2bppSize, glFormat: [ 0x8C03 ] },
    2: { sizeFunc: PVRTC4bppSize, glFormat: [ 0x8C00 ] },
    3: { sizeFunc: PVRTC4bppSize, glFormat: [ 0x8C02 ] },
    6: { sizeFunc: DXTEtcSmallSize , glFormat: [ 0x8D64 ] },
    7: { sizeFunc: DXTEtcSmallSize, glFormat: [ 0x83F0, 0x8C4C ] },
    8: { sizeFunc: DXTEtcAstcBigSize, glFormat: [ 0x83F1, 0x8C4D ] },
    9: { sizeFunc: DXTEtcAstcBigSize, glFormat: [ 0x83F2, 0x8C4E ] },
    11: { sizeFunc: DXTEtcAstcBigSize, glFormat: [ 0x83F3, 0x8C4F ] },
    22: { sizeFunc: DXTEtcSmallSize , glFormat: 0x9274 },
    23: { sizeFunc: DXTEtcAstcBigSize, glFormat: 0x9278 },
    22: { sizeFunc: DXTEtcSmallSize , glFormat: [ 0x9274 ] },
    23: { sizeFunc: DXTEtcAstcBigSize, glFormat: [ 0x9278 ] },
    24: { sizeFunc: DXTEtcSmallSize, glFormat: [ 0x9276 ] },
    25: { sizeFunc: DXTEtcSmallSize, glFormat: [ 0x9270 ] },
    26: { sizeFunc: DXTEtcAstcBigSize, glFormat: [ 0x9272 ] },
    27: { sizeFunc: DXTEtcAstcBigSize, glFormat: [ 0x93B0, 0x93D0 ] },
    28: { sizeFunc: ATC5x4Size, glFormat: [ 0x93B1, 0x93D1 ] },
    29: { sizeFunc: ATC5x5Size, glFormat: [ 0x93B2, 0x93D2 ] },
    30: { sizeFunc: ATC6x5Size, glFormat: [ 0x93B3, 0x93D3 ] },
    31: { sizeFunc: ATC6x6Size, glFormat: [ 0x93B4, 0x93D4 ] },
    32: { sizeFunc: ATC8x5Size, glFormat: [ 0x93B5, 0x93D5 ] },
    33: { sizeFunc: ATC8x6Size, glFormat: [ 0x93B6, 0x93D6 ] },
    34: { sizeFunc: ATC8x8Size, glFormat: [ 0x93B7, 0x93D7 ] },
    35: { sizeFunc: ATC10x5Size, glFormat: [ 0x93B8, 0x93D8 ] },
    36: { sizeFunc: ATC10x6Size, glFormat: [ 0x93B9, 0x93D9 ] },
    37: { sizeFunc: ATC10x8Size, glFormat: [ 0x93BA, 0x93DA ] },
    38: { sizeFunc: ATC10x10Size, glFormat: [ 0x93BB, 0x93DB ] },
    39: { sizeFunc: ATC12x10Size, glFormat: [ 0x93BC, 0x93DC ] },
    40: { sizeFunc: ATC12x12Size, glFormat: [ 0x93BD, 0x93DD ] }
};

/**
 * Parses a PVR format Compressed Texture file and generates texture data suitable for WebGL from it.
 *
 * @function Phaser.Textures.Parsers.PVRParser
 * @memberof Phaser.Textures.Parsers
 * @since 3.60.0
 *
 * @param {ArrayBuffer} data - The data object created by the Compressed Texture File Loader.
 *
 * @return {Phaser.Types.Textures.CompressedTextureData} The Compressed Texture data.
 */
var PVRParser = function (data)
{
    var header = new Uint32Array(data, 0, 13);

    // VERSION
    var version = header[0];
    var versionMatch = version === 0x03525650;

    //  PIXEL_FORMAT_INDEX
    var pvrFormat = versionMatch ? header[2] : header[3];

    // Colour Space
    var colorSpace = header[4];

    var internalFormat = FORMATS[pvrFormat].glFormat[colorSpace];
    var sizeFunction = FORMATS[pvrFormat].sizeFunc;

    //  MIPMAPCOUNT_INDEX
    var mipmapLevels = header[11];

    //  WIDTH_INDEX
    var width = header[7];

    //  HEIGHT_INDEX
    var height = header[6];

    //  HEADER_SIZE + METADATA_SIZE_INDEX
    var dataOffset = 52 + header[12];

    var image = new Uint8Array(data, dataOffset);

    var mipmaps = new Array(mipmapLevels);

    var offset = 0;
    var levelWidth = width;
    var levelHeight = height;

    for (var i = 0; i < mipmapLevels; i++)
    {
        var levelSize = sizeFunction(levelWidth, levelHeight);

        mipmaps[i] = {
            data: new Uint8Array(image.buffer, image.byteOffset + offset, levelSize),
            width: levelWidth,
            height: levelHeight
        };

        levelWidth = Math.max(1, levelWidth >> 1);
        levelHeight = Math.max(1, levelHeight >> 1);

        offset += levelSize;
    }

    return {
        mipmaps: mipmaps,
        width: width,
        height: height,
        internalFormat: internalFormat,
        compressed: true,
        generateMipmap: false
    };
};

module.exports = PVRParser;


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2013-2023 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

var GetLeft = require('../../bounds/GetLeft');
var GetTop = require('../../bounds/GetTop');
var SetLeft = require('../../bounds/SetLeft');
var SetTop = require('../../bounds/SetTop');

/**
 * Takes given Game Object and aligns it so that it is positioned in the top left of the other.
 *
 * @function Phaser.Display.Align.In.TopLeft
 * @since 3.0.0
 *
 * @generic {Phaser.GameObjects.GameObject} G - [gameObject,$return]
 *
 * @param {Phaser.GameObjects.GameObject} gameObject - The Game Object that will be positioned.
 * @param {Phaser.GameObjects.GameObject} alignIn - The Game Object to base the alignment position on.
 * @param {number} [offsetX=0] - Optional horizontal offset from the position.
 * @param {number} [offsetY=0] - Optional vertical offset from the position.
 *
 * @return {Phaser.GameObjects.GameObject} The Game Object that was aligned.
 */
var TopLeft = function (gameObject, alignIn, offsetX, offsetY)
{
    if (offsetX === undefined) { offsetX = 0; }
    if (offsetY === undefined) { offsetY = 0; }

    SetLeft(gameObject, GetLeft(alignIn) - offsetX);
    SetTop(gameObject, GetTop(alignIn) - offsetY);

    return gameObject;
};

module.exports = TopLeft;


/**
 * @callback Phaser.Types.Cameras.Scene2D.CameraZoomCallback
 * @since 3.11.0
 *
 * @param {Phaser.Cameras.Scene2D.Camera} camera - The camera on which the effect is running.
 * @param {number} progress - The progress of the effect. A value between 0 and 1.
 * @param {number} zoom - The Camera's new zoom value.
 */


#define SHADER_NAME PHASER_LINEAR_BLEND_FS

precision mediump float;

uniform sampler2D uMainSampler1;
uniform sampler2D uMainSampler2;
uniform float uStrength;

varying vec2 outTexCoord;

void main ()
{
    vec4 frame1 = texture2D(uMainSampler1, outTexCoord);
    vec4 frame2 = texture2D(uMainSampler2, outTexCoord);

    gl_FragColor = mix(frame1, frame2 * uStrength, 0.5);
}


return GetSize(width, height, 11, 11, 12, 12);

keep keep keep keep replace keep keep keep keep keep


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2013-2023 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

/**
 * Passes all provided Game Objects to the Input Manager to enable them for input with identical areas and callbacks.
 *
 * @see {@link Phaser.GameObjects.GameObject#setInteractive}
 *
 * @function Phaser.Actions.SetHitArea
 * @since 3.0.0
 *
 * @generic {Phaser.GameObjects.GameObject[]} G - [items,$return]
 *
 * @param {(array|Phaser.GameObjects.GameObject[])} items - An array of Game Objects. The contents of this array are updated by this Action.
 * @param {(Phaser.Types.Input.InputConfiguration|any)} [hitArea] - Either an input configuration object, or a geometric shape that defines the hit area for the Game Object. If not given it will try to create a Rectangle based on the texture frame.
 * @param {Phaser.Types.Input.HitAreaCallback} [callback] - The callback that determines if the pointer is within the Hit Area shape or not. If you provide a shape you must also provide a callback.
 *
 * @return {(array|Phaser.GameObjects.GameObject[])} The array of Game Objects that was passed to this Action.
 */
var SetHitArea = function (items, hitArea, hitAreaCallback)
{
    for (var i = 0; i < items.length; i++)
    {
        items[i].setInteractive(hitArea, hitAreaCallback);
    }

    return items;
};

module.exports = SetHitArea;


# Phaser 3 Change Log

## Version 3.7.0 - Sinon - 4th May 2018

A beta release of the 3.7.0 version. See 3.7.1 for details.


# Version 3.60.0 - Miku - 12th April 2023

Due to the size and importance of the v3.60 release we have split the Change Log up into multiple sections.

This makes it easier for you to browse and find the information you need.

## New Features

These are the headliner features in this release:

* 14 bundled [Special FX](FX.md) including Bloom, Blur, Distort, Glow, Wipe and more
* Vastly improved [Mobile Rendering Performance](MobilePerformance.md) - over 7000% faster!
* New [Timeline Sequencer](Timeline.md) for creating complex flows of events
* New [Plane Game Object](PlaneGameObject.md) for perspective distortions
* New [Nine Slice Game Object](NineSliceGameObject.md) for perfect UI scaling
* [Built-in Spector JS](Spector.md) for WebGL debugging on desktop and mobile
* Brand new [Video Game Object](VideoGameObject.md) handles videos and media streams with ease
* Brand new [Particle Emitter](ParticleEmitter.md) comes with explosive new features
* Support for [Spatial Audio](SpatialSound.md) and distance based volume
* New [Spine 4 Plugin](Spine4.md) support
* Upgraded to [Matter Physics v0.19](MatterPhysics.md)
* New [Tween Manager](TweenManager.md) with better performance and memory management
* New [Dynamic Textures](DynamicTextures.md) for rendering to textures at runtime
* New [TimeStep features and Timer Event Updates](Timestep.md) for enforcing fps rates and more
* Support for [Compressed Textures](CompressedTextures.md)
* [ESM Module Support](ESMSupport.md)

## System and Plugins

Pick any of the following sections to see the breaking changes, new features, updates, and bug fixes for that area of the API.

* [Animation System](Animation.md)
* [Arcade Physics](ArcadePhysics.md)
* [Bitmap and Geometry Masks](Masks.md)
* [Camera System](Camera.md)
* [Canvas Renderer](CanvasRenderer.md)
* [WebGL Renderer](WebGLRenderer.md)
* [Colors and Display](Colors.md)
* [Game, Device and Game Config](Game.md)
* [Geometry, Paths and Curves](Geometry.md)
* [Input System](Input.md)
* [Loader System](Loader.md)
* [Scale Manager](ScaleManager.md)
* [Scenes and Scene Manager](Scene.md)
* [Sound System](Sound.md)
* [Spine 3 Plugin](Spine3.md)
* [Texture Manager](TextureManager.md)
* [Utils, Math and Actions](Utils.md)
* [Build Config and Browser Updates](Build.md)

## Game Object Updates

Finally, here are the updates related to Game Objects:

* [Bitmap Text Game Object](BitmapTextGameObject.md)
* [Container Game Object](Container.md)
* [Graphics Game Object](GraphicsGameObject.md)
* [Mesh Game Object, Vertices and Faces](Mesh.md)
* [Text Game Object](TextGameObject.md)
* [Tilemap Game Object](Tilemap.md)
* [All other Game Object related Updates](GameObject.md)

## Examples, Documentation, Beta Testing and TypeScript

My thanks to the following for helping with the Phaser 3 Examples, Beta Testing, Docs, and TypeScript definitions, either by reporting errors, fixing them, or helping author the docs:

| 💖 | 💖 | 💖 | 💖 |
| ----- | ----- | ----- | ----- |
| @0day-oni | @201flaviosilva | @AlbertMontagutCasero | @Arcanorum |
| @arosemena | @austinlyon | @chrisl8 | @christian-post |
| @danfoster | @darrylpizarro | @DeweyHur | @drunkcat |
| @ef4 | @eltociear | @EsteFilipe | @etherealmachine |
| @EmilSV | @Fake | @florestankorp | @hacheraw |
| @hanzooo | @jerricko | @joegaffey | @jonasrundberg |
| @kainage | @kootoopas | @lolimay | @MaffDev |
| @michalfialadev | @monteiz | @necrokot | @Nero0 |
| @OdinvonDoom | @orjandh | @pavle-goloskokovic | @PhaserEditor2D |
| @Pythux | @quocsinh | @rgk | @rollinsafary-inomma |
| @rstanuwijaya | 👑 @samme 👑 | @Smirnov48 | @steveja42 |
| @sylvainpolletvillard | @twoco | @ubershmekel | @ultimoistante |
| @VanaMartin | @vforsh | @Vidminas | @x-wk |
| @xmahle | @xuxucode | @YeloPartyHat | @ZekeLu |
| FromChris | Golen | OmniOwl | and you ... |

📖 Read the [Phaser 3 API Docs](https://newdocs.phaser.io/) 💻 Browse 2000+ [Code Examples](https://labs.phaser.io) 🤝 Join the awesome [Phaser Discord](https://discord.gg/phaser)


* 8: COMPRESSED_RGBA_S3TC_DXT1_EXT or COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT

* 9: COMPRESSED_RGBA_S3TC_DXT3_EXT or COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT

* 11: COMPRESSED_RGBA_S3TC_DXT5_EXT or COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT

* 14: COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT or COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT

* 15: COMPRESSED_RGBA_BPTC_UNORM_EXT or COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2013-2023 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

/**
 * @namespace Phaser.Types.GameObjects.Shader
 */


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2013-2023 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

/**
 * The Camera Rotate Complete Event.
 *
 * This event is dispatched by a Camera instance when the Rotate Effect completes.
 *
 * Listen for it via either of the following:
 *
 * ```js
 * this.cameras.main.on('camerarotatecomplete', () => {});
 * ```
 *
 * or use the constant, to avoid having to remember the correct event string:
 *
 * ```js
 * this.cameras.main.on(Phaser.Cameras.Scene2D.Events.ROTATE_COMPLETE, () => {});
 * ```
 *
 * @event Phaser.Cameras.Scene2D.Events#ROTATE_COMPLETE
 * @type {string}
 * @since 3.23.0
 *
 * @param {Phaser.Cameras.Scene2D.Camera} camera - The camera that the effect began on.
 * @param {Phaser.Cameras.Scene2D.Effects.RotateTo} effect - A reference to the effect instance.
 */
module.exports = 'camerarotatecomplete';


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2013-2023 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

var RotateAroundXY = require('./RotateAroundXY');

/**
 * Rotate a line around a point by the given angle in radians.
 *
 * @function Phaser.Geom.Line.RotateAroundPoint
 * @since 3.0.0
 *
 * @generic {Phaser.Geom.Line} O - [line,$return]
 *
 * @param {Phaser.Geom.Line} line - The line to rotate.
 * @param {(Phaser.Geom.Point|object)} point - The point to rotate the line around.
 * @param {number} angle - The angle of rotation in radians.
 *
 * @return {Phaser.Geom.Line} The rotated line.
 */
var RotateAroundPoint = function (line, point, angle)
{
    return RotateAroundXY(line, point.x, point.y, angle);
};

module.exports = RotateAroundPoint;


6: { sizeFunc: DXTEtcSmallSize , glFormat: [ 0x8D64 ] },

7: { sizeFunc: DXTEtcSmallSize, glFormat: [ 0x83F0, 0x8C4C ] },

8: { sizeFunc: DXTEtcAstcBigSize, glFormat: [ 0x83F1, 0x8C4D ] },

9: { sizeFunc: DXTEtcAstcBigSize, glFormat: [ 0x83F2, 0x8C4E ] },

11: { sizeFunc: DXTEtcAstcBigSize, glFormat: [ 0x83F3, 0x8C4F ] },

22: { sizeFunc: DXTEtcSmallSize , glFormat: 0x9274 },

23: { sizeFunc: DXTEtcAstcBigSize, glFormat: 0x9278 },

22: { sizeFunc: DXTEtcSmallSize , glFormat: [ 0x9274 ] },

23: { sizeFunc: DXTEtcAstcBigSize, glFormat: [ 0x9278 ] },

24: { sizeFunc: DXTEtcSmallSize, glFormat: [ 0x9276 ] },

25: { sizeFunc: DXTEtcSmallSize, glFormat: [ 0x9270 ] },

keep keep keep keep keep replace replace keep keep keep keep

14: { sizeFunc: BPTCSize, glFormat: [ 0x8E8E, 0x8E8F ] },

15: { sizeFunc: BPTCSize, glFormat: [ 0x8E8C, 0x8E8D ] },


# Phaser 3.60.0 Change Log

Return to the [Change Log index](CHANGELOG-v3.60.md).

## New Feature - Built-in Special FX

![Special FX](images/fx.png)

We have decided to bundle a selection of highly flexible special effect shaders in to Phaser 3.60 and provide access to them via an easy to use set of API calls. The FX included are:

* **Barrel** - A nice pinch / bulge distortion effect.
* **Bloom** - Add bloom to any Game Object, with custom offset, blur strength, steps and color.
* **Blur** - 3 different levels of gaussian blur (low, medium and high) and custom distance and color.
* **Bokeh** / TiltShift - A bokeh and tiltshift effect, with intensity, contrast and distance settings.
* **Circle** - Add a circular ring around any Game Object, useful for masking / avatar frames, with custom color, width and background color.
* **ColorMatrix** - Add a ColorMatrix to any Game Object with access to all of its methods, such as `sepia`, `greyscale`, `lsd` and lots more.
* **Displacement** - Use a displacement texture, such as a noise texture, to drastically (or subtly!) alter the appearance of a Game Object.
* **Glow** - Add a smooth inner or outer glow, with custom distance, strength and color.
* **Gradient** - Draw a gradient between two colors across any Game Object, with optional 'chunky' mode for classic retro style games.
* **Pixelate** - Make any Game Object appear pixelated, to a varying degree.
* **Shadow** - Add a drop shadow behind a Game Object, with custom depth and color.
* **Shine** - Run a 'shine' effect across a Game Object, either additively or as part of a reveal.
* **Vignette** - Apply a vignette around a Game Object, with custom offset position, radius and color.
* **Wipe** - Set a Game Object to 'wipe' or 'reveal' with custom line width, direction and axis of the effect.

What's more, the FX can be stacked up. You could add, for example, a `Barrel` followed by a `Blur` and then topped-off with a `Circle` effect. Just by adjusting the ordering you can achieve some incredible and unique effects, very quickly.

We've worked hard to make the API as easy to use as possible, too. No more messing with pipelines or importing plugins. You can simply do:

```js
const player = this.add.sprite(x, y, texture);

player.preFX.addGlow(0xff0000, 32);
```

This will add a 32 pixel red glow around the `player` Sprite.

Each effect returns a new FX Controller instance, allowing you to easily adjust the special effects in real-time via your own code, tweens and similar:

```js
const fx = container.postFX.addWipe();

this.tweens.add({
    targets: fx,
    progress: 1
});
```

This will add a Wipe Effect to a Container instance and then tween its progress value from 0 to 1, causing the wipe to play out.

All texture-based Game Objects have access to `Pre FX` (so that includes Images, Sprites, TileSprites, Text, RenderTexture and Video). However, _all_ Game Objects have access to `Post FX`, as do cameras. The difference is just when the effect is applied. For a 'pre' effect, it is applied before the Game Object is drawn. For a 'post' effect, it's applied after it has been drawn. All of the same effects are available to both.

```js
this.cameras.main.postFX.addTiltShift();
```

For example, this will apply a Tilt Shift effect to everything being rendered by the Camera. Which is a much faster way of doing it than applying the same effect to every child in a Scene. You can also apply them to Containers, allowing more fine-grained control over the display.

The full list of new methods are as follows:

Available only to texture-based Game Objects:

* `GameObject.preFX` an instance of the FX Controller, which allows you to add and remove  Pre FX from the Game Object. It features methods such as `add`, `remove` and `clear`. Plus the following:

* `GameObject.preFX.addGlow` adds a Glow Pre FX effect to the Game Object.
* `GameObject.preFX.addShadow` adds a Shadow Pre FX effect to the Game Object.
* `GameObject.preFX.addPixelate` adds a Pixelate Pre FX effect to the Game Object.
* `GameObject.preFX.addVignette` adds a Vignette Pre FX effect to the Game Object.
* `GameObject.preFX.addShine` adds a Shine Pre FX effect to the Game Object.
* `GameObject.preFX.addBlur` adds a Blur Pre FX effect to the Game Object.
* `GameObject.preFX.addGradient` adds a Gradient Pre FX effect to the Game Object.
* `GameObject.preFX.addBloom` adds a Bloom Pre FX effect to the Game Object.
* `GameObject.preFX.addColorMatrix` adds a ColorMatrix Pre FX effect to the Game Object.
* `GameObject.preFX.addCircle` adds a Circle Pre FX effect to the Game Object.
* `GameObject.preFX.addBarrel` adds a Barrel Pre FX effect to the Game Object.
* `GameObject.preFX.addDisplacement` adds a Displacement Pre FX effect to the Game Object.
* `GameObject.preFX.addWipe` adds a Wipe Pre FX effect to the Game Object.
* `GameObject.preFX.addReveal` adds a Reveal Pre FX effect to the Game Object.
* `GameObject.preFX.addBokeh` adds a Bokeh Pre FX effect to the Game Object.
* `GameObject.preFX.addTiltShift` adds a TiltShift Pre FX effect to the Game Object.

Available to all Game Objects:

* `GameObject.clearFX` removes both Pre and Post FX from the Game Object.
* `GameObject.postFX` an instance of the FX Controller, which allows you to add and remove  Post FX from the Game Object. It features methods such as `add`, `remove` and `clear`. Plus the following:

* `GameObject.postFX.addGlow` adds a Glow Post FX effect to the Game Object.
* `GameObject.postFX.addShadow` adds a Shadow Post FX effect to the Game Object.
* `GameObject.postFX.addPixelate` adds a Pixelate Post FX effect to the Game Object.
* `GameObject.postFX.addVignette` adds a Vignette Post FX effect to the Game Object.
* `GameObject.postFX.addShine` adds a Shine Post FX effect to the Game Object.
* `GameObject.postFX.addBlur` adds a Blur Post FX effect to the Game Object.
* `GameObject.postFX.addGradient` adds a Gradient Post FX effect to the Game Object.
* `GameObject.postFX.addBloom` adds a Bloom Post FX effect to the Game Object.
* `GameObject.postFX.addColorMatrix` adds a ColorMatrix Post FX effect to the Game Object.
* `GameObject.postFX.addCircle` adds a Circle Post FX effect to the Game Object.
* `GameObject.postFX.addBarrel` adds a Barrel Post FX effect to the Game Object.
* `GameObject.postFX.addDisplacement` adds a Displacement Post FX effect to the Game Object.
* `GameObject.postFX.addWipe` adds a Wipe Post FX effect to the Game Object.
* `GameObject.postFX.addReveal` adds a Reveal Post FX effect to the Game Object.
* `GameObject.postFX.addBokeh` adds a Bokeh Post FX effect to the Game Object.
* `GameObject.postFX.addTiltShift` adds a TiltShift Post FX effect to the Game Object.

## New Feature - Pre FX Pipeline

* When defining the `renderTargets` in a WebGL Pipeline config, you can now set optional `width` and `height` properties, which will create a Render Target of that exact size, ignoring the `scale` value (if also given).
* `WebGLPipeline.isPreFX` is a new boolean property that defines if the pipeline is a Sprite FX Pipeline, or not. The default is `false`.
* `GameObjects.Components.FX` is a new component that provides access to FX specific properties and methods. The Image and Sprite Game Objects have this component by default.
* `fxPadding` and its related method `setFXPadding` allow you to set extra padding to be added to the texture the Game Object renders with. This is especially useful for Pre FX shaders that modify the sprite beyond its bounds, such as glow or shadow effects.
* The `WebGLPipeline.setShader` method has a new optional parameter `buffer` that allows you to set the vertex buffer to be bound before the shader is activated.
* The `WebGLPipeline.setVertexBuffer` method has a new optional parameter `buffer` that allows you to set the vertex buffer to be bound if you don't want to bind the default one.
* The `WebGLRenderer.createTextureFromSource` method has a new optional boolean parameter `forceClamp` that will for the clamp wrapping mode even if the texture is a power-of-two.
* `RenderTarget` will now automatically set the wrapping mode to clamp.
* `WebGLPipeline.flipProjectionMatrix` is a new method that allows you to flip the y and bottom projection matrix values via a parameter.
* `PipelineManager.renderTargets` is a new property that holds an array of `RenderTarget` objects that all `PreFX` pipelines can share, to keep texture memory as low as possible.
* `PipelineManager.maxDimension` is a new property that holds the largest possible target dimension.
* `PipelineManager.frameInc` is a new property that holds the amount the `RenderTarget`s will increase in size in each iteration. The default value is 32, meaning it will create targets of size 32, 64, 96, etc. You can control this via the pipeline config object.
* `PipelineManager.targetIndex` is a new property that holds the internal target array offset index. Treat it as read-only.
* The Pipeline Manager will now create a bunch of `RenderTarget` objects during its `boot` method. These are sized incrementally from 32px and up (use the `frameInc` value to alter this). These targets are shared by all Sprite FX Pipelines.
* `PipelineManager.getRenderTarget` is a new method that will return the a `RenderTarget` that best fits the dimensions given. This is typically called by Pre FX Pipelines, rather than directly.
* `PipelineManager.getSwapRenderTarget` is a new method that will return a 'swap' `RenderTarget` that matches the size of the main target. This is called by Pre FX pipelines and not typically called directly.
* `PipelineManager.getAltSwapRenderTarget` is a new method that will return a 'alternative swap' `RenderTarget` that matches the size of the main target. This is called by Pre FX pipelines and not typically called directly.
* The `WebGLPipeline.setTime` method has a new optional parameter `shader`, which allows you to control the shader on which the time value is set.
* If you add `#define SHADER_NAME` to the start of your shader then it will be picked up as the `WebGLShader` name during the `setShadersFromConfig` process within `WebGLPipeline`.
* Calling `setPostPipeline` on a Game Object will now pass the `pipelineData` configuration object (if provided) to the pipeline instance being created.
* `PipelineManager.getPostPipeline` now has an optional 3rd parameter, a `config` object that is passed to the pipeline instance in its constructor, which can be used by the pipeline during its set-up.

---------------------------------------

Return to the [Change Log index](CHANGELOG-v3.60.md).

📖 Read the [Phaser 3 API Docs](https://newdocs.phaser.io/) 💻 Browse 2000+ [Code Examples](https://labs.phaser.io) 🤝 Join the awesome [Phaser Discord](https://discord.gg/phaser)


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2013-2023 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

/**
 * Center a line on the given coordinates.
 *
 * @function Phaser.Geom.Line.CenterOn
 * @since 3.0.0
 *
 * @param {Phaser.Geom.Line} line - The line to center.
 * @param {number} x - The horizontal coordinate to center the line on.
 * @param {number} y - The vertical coordinate to center the line on.
 *
 * @return {Phaser.Geom.Line} The centered line.
 */
var CenterOn = function (line, x, y)
{
    var tx = x - ((line.x1 + line.x2) / 2);
    var ty = y - ((line.y1 + line.y2) / 2);

    line.x1 += tx;
    line.y1 += ty;

    line.x2 += tx;
    line.y2 += ty;

    return line;
};

module.exports = CenterOn;


switch (token.type) {
case "comment":
case "number":
case "regexp":
return false;
default:
return true;

/*
 * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, brackets, $ */

define(function (require, exports, module) {
    "use strict";

    var CodeHintManager = brackets.getModule("editor/CodeHintManager"),
        EditorManager   = brackets.getModule("editor/EditorManager"),
        DocumentManager = brackets.getModule("document/DocumentManager"),
        Commands        = brackets.getModule("command/Commands"),
        CommandManager  = brackets.getModule("command/CommandManager"),
        Menus           = brackets.getModule("command/Menus"),
        Strings         = brackets.getModule("strings"),
        AppInit         = brackets.getModule("utils/AppInit"),
        ExtensionUtils  = brackets.getModule("utils/ExtensionUtils"),
        StringUtils     = brackets.getModule("utils/StringUtils"),
        StringMatch     = brackets.getModule("utils/StringMatch"),
        LanguageManager = brackets.getModule("language/LanguageManager"),
        HintUtils       = require("HintUtils"),
        ScopeManager    = require("ScopeManager"),
        Session         = require("Session"),
        Acorn           = require("thirdparty/acorn/acorn");

    var KeyboardPrefs = JSON.parse(require("text!keyboard.json"));

    var JUMPTO_DEFINITION = "navigate.jumptoDefinition";

    var session      = null,  // object that encapsulates the current session state
        cachedCursor = null,  // last cursor of the current hinting session
        cachedHints  = null,  // sorted hints for the current hinting session
        cachedType   = null,  // describes the lookup type and the object context
        cachedToken  = null,  // the token used in the current hinting session
        matcher      = null;  // string matcher for hints

    /**
     *  Get the value of current session.
     *  Used for unit testing.
     * @returns {Session} - the current session.
     */
    function getSession() {
        return session;
    }

    /**
     * Creates a hint response object. Filters the hint list using the query
     * string, formats the hints for display, and returns a hint response
     * object according to the CodeHintManager's API for code hint providers.
     *
     * @param {Array.<Object>} hints - hints to be included in the response
     * @param {string} query - querystring with which to filter the hint list
     * @param {Object} type - the type of query, property vs. identifier
     * @return {Object} - hint response as defined by the CodeHintManager API
     */
    function getHintResponse(hints, query, type) {

        var trimmedQuery,
            formattedHints;

        /*
         * Returns a formatted list of hints with the query substring
         * highlighted.
         *
         * @param {Array.<Object>} hints - the list of hints to format
         * @param {string} query - querystring used for highlighting matched
         *      poritions of each hint
         * @return {Array.<jQuery.Object>} - array of hints formatted as jQuery
         *      objects
         */
        function formatHints(hints, query) {
            return hints.map(function (token) {
                var $hintObj    = $("<span>").addClass("brackets-js-hints");

                // level indicates either variable scope or property confidence
                if (!type.property && !token.builtin && token.depth !== undefined) {
                    switch (token.depth) {
                    case 0:
                        $hintObj.addClass("priority-high");
                        break;
                    case 1:
                        $hintObj.addClass("priority-medium");
                        break;
                    case 2:
                        $hintObj.addClass("priority-low");
                        break;
                    default:
                        $hintObj.addClass("priority-lowest");
                        break;
                    }
                }

                if (token.guess) {
                    $hintObj.addClass("guess-hint");
                }

                // is the token a keyword?
                if (token.keyword) {
                    $hintObj.addClass("keyword-hint");
                }

                if (token.literal) {
                    $hintObj.addClass("literal-hint");
                }

                // highlight the matched portion of each hint
                if (token.stringRanges) {
                    token.stringRanges.forEach(function (item) {
                        if (item.matched) {
                            $hintObj.append($("<span>")
                                .append(StringUtils.htmlEscape(item.text))
                                .addClass("matched-hint"));
                        } else {
                            $hintObj.append(StringUtils.htmlEscape(item.text));
                        }
                    });
                } else {
                    $hintObj.text(token.value);
                }

                $hintObj.data("token", token);

                return $hintObj;
            });
        }

        // trim leading and trailing string literal delimiters from the query
        if (query.indexOf(HintUtils.SINGLE_QUOTE) === 0 ||
                query.indexOf(HintUtils.DOUBLE_QUOTE) === 0) {
            trimmedQuery = query.substring(1);
            if (trimmedQuery.lastIndexOf(HintUtils.DOUBLE_QUOTE) === trimmedQuery.length - 1 ||
                    trimmedQuery.lastIndexOf(HintUtils.SINGLE_QUOTE) === trimmedQuery.length - 1) {
                trimmedQuery = trimmedQuery.substring(0, trimmedQuery.length - 1);
            }
        } else {
            trimmedQuery = query;
        }

        if (hints) {
            formattedHints = formatHints(hints, trimmedQuery);
        } else {
            formattedHints = [];
        }

        return {
            hints: formattedHints,
            match: null, // the CodeHintManager should not format the results
            selectInitial: true
        };
    }

    /**
     * @constructor
     */
    function JSHints() {
    }

    /**
     * determine if the cached hint information should be invalidated and re-calculated
     *
     * @param {Session} session - the active hinting session
     * @return {boolean} - true if the hints should be recalculated
     */
    JSHints.prototype.needNewHints = function (session) {
        var cursor  = session.getCursor(),
            type    = session.getType();

        return !cachedHints ||
                cachedCursor.line !== cursor.line ||
                type.property !== cachedType.property ||
                type.context !== cachedType.context ||
                type.showFunctionType !== cachedType.showFunctionType;
    };

    /**
     *  Have conditions have changed enough to justify closing the hints popup?
     *
     * @param {Session} session - the active hinting session
     * @return {boolean} - true if the hints popup should be closed.
     */
    JSHints.prototype.shouldCloseHints = function (session) {

        // close if the token className has changed then close the hints.
        var cursor = session.getCursor(),
            token = session.getToken(cursor),
            lastToken = cachedToken;

        // if the line has changed, then close the hints
        if (!cachedCursor || cursor.line !== cachedCursor.line) {
            return true;
        }

        if (token.type === null) {
            token = session.getNextTokenOnLine(cursor);
        }

        if (lastToken && lastToken.type === null) {
            lastToken = session.getNextTokenOnLine(cachedCursor);
        }

        // Both of the tokens should never be null (happens when token is off
        // the end of the line), so one is null then close the hints.
        if (!lastToken || !token ||
                token.type !== lastToken.type) {
            return true;
        }

        // Test if one token string is a prefix of the other.
        // If one is a prefix of the other then consider it the
        // same token and don't close the hints.
        if (token.string.length >= lastToken.string.length) {
            return token.string.indexOf(lastToken.string) !== 0;
        } else {
            return lastToken.string.indexOf(token.string) !== 0;
        }
    };

    /**
     * @return {boolean} - true if the document is a html file
     */
    function isHTMLFile(document) {
        var languageID = LanguageManager.getLanguageForPath(document.file.fullPath).getId();
        return languageID === "html";
    }

    function isInlineScript(editor) {
        return editor.getModeForSelection() === "javascript";
    }

    /**
     * Determine whether hints are available for a given editor context
     *
     * @param {Editor} editor - the current editor context
     * @param {string} key - charCode of the last pressed key
     * @return {boolean} - can the provider provide hints for this session?
     */
    JSHints.prototype.hasHints = function (editor, key) {
        if (session && HintUtils.hintableKey(key)) {

            if (isHTMLFile(session.editor.document)) {
                if (!isInlineScript(session.editor)) {
                    return false;
                }
            }
            var cursor  = session.getCursor(),
                token   = session.getToken(cursor);

            // don't autocomplete within strings or comments, etc.
            if (token && HintUtils.hintable(token)) {
                var offset = session.getOffset(),
                    type    = session.getType(),
                    query   = session.getQuery();

                if (this.needNewHints(session)) {
                    cachedCursor = null;
                    cachedHints = null;
                    cachedToken = null;
                    matcher = null;
                }
                return true;
            }
        }
        return false;
    };

    /**
      * Return a list of hints, possibly deferred, for the current editor
      * context
      *
      * @param {string} key - charCode of the last pressed key
      * @return {Object + jQuery.Deferred} - hint response (immediate or
      *     deferred) as defined by the CodeHintManager API
      */
    JSHints.prototype.getHints = function (key) {
        var cursor = session.getCursor(),
            token = session.getToken(cursor);
        if (token && HintUtils.hintableKey(key) && HintUtils.hintable(token)) {
            var type    = session.getType(),
                query   = session.getQuery();

            // If the hint context is changed and the hints are open, then
            // close the hints by returning null;
            if (CodeHintManager.isOpen() && this.shouldCloseHints(session)) {
                return null;
            }

            cachedCursor = cursor;
            cachedToken = token;

            // Compute fresh hints if none exist, or if the session
            // type has changed since the last hint computation
            if (this.needNewHints(session)) {
                var scopeResponse   = ScopeManager.requestHints(session, session.editor.document);

                if (scopeResponse.hasOwnProperty("promise")) {
                    var $deferredHints = $.Deferred();
                    scopeResponse.promise.done(function () {
                        cachedType = session.getType();

                        matcher = new StringMatch.StringMatcher();
                        cachedHints = session.getHints(query, matcher);

                        if ($deferredHints.state() === "pending") {
                            query = session.getQuery();
                            var hintResponse    = getHintResponse(cachedHints, query, type);

                            $deferredHints.resolveWith(null, [hintResponse]);
                        }
                    }).fail(function () {
                        if ($deferredHints.state() === "pending") {
                            $deferredHints.reject();
                        }
                    });

                    return $deferredHints;
                }
            }

            if (cachedHints) {
                cachedHints = session.getHints(session.getQuery(), matcher);
                return getHintResponse(cachedHints, query, type);
            }
        }

        return null;
    };

    /**
     * Inserts the hint selected by the user into the current editor.
     *
     * @param {jQuery.Object} hint - hint object to insert into current editor
     * @return {boolean} - should a new hinting session be requested
     *      immediately after insertion?
     */
    JSHints.prototype.insertHint = function ($hintObj) {
        var hint        = $hintObj.data("token"),
            completion  = hint.value,
            cursor      = session.getCursor(),
            token       = session.getToken(cursor),
            query       = session.getQuery(),
            start       = {line: cursor.line, ch: cursor.ch - query.length},
            end         = {line: cursor.line, ch: (token ? token.end : cursor.ch)},
            delimiter;

        if (token && token.string === ".") {
            var nextToken  = session.getNextTokenOnLine(cursor);

            if (nextToken && // don't replace delimiters, etc.
                    HintUtils.maybeIdentifier(nextToken.string) &&
                    HintUtils.hintable(nextToken)) {
                end.ch = nextToken.end;
            }
        }

        // If the hint is a string literal, choose a delimiter in which
        // to wrap it, preserving the existing delimiter if possible.
        if (hint.literal && hint.kind === "string") {
            if (token.string.indexOf(HintUtils.DOUBLE_QUOTE) === 0) {
                delimiter = HintUtils.DOUBLE_QUOTE;
            } else if (token.string.indexOf(HintUtils.SINGLE_QUOTE) === 0) {
                delimiter = HintUtils.SINGLE_QUOTE;
            } else {
                delimiter = hint.delimiter;
            }

            completion = completion.replace("\\", "\\\\");
            completion = completion.replace(delimiter, "\\" + delimiter);
            completion = delimiter + completion + delimiter;
        }

        if (session.getType().showFunctionType) {
            // function types show up as hints, so don't insert anything
            // if we were displaying a function type
            return false;
        }

        if (session.getType().property) {
            // if we're inserting a property name, we need to make sure the
            // hint is a valid property name.
            // to check this, run the hint through Acorns tokenizer
            // it should result in one token, and that token should either be
            // a 'name' or a 'keyword', as javascript allows keywords as property names
            var tokenizer = Acorn.tokenize(completion);
            var currentToken = tokenizer(),
                invalidPropertyName = false;

            // the name is invalid if the hint is not a 'name' or 'keyword' token
            if (currentToken.type !== Acorn.tokTypes.name && !currentToken.type.keyword) {
                invalidPropertyName = true;
            } else {
                // check for a second token - if there is one (other than 'eof')
                // then the hint isn't a valid property name either
                currentToken = tokenizer();
                if (currentToken.type !== Acorn.tokTypes.eof) {
                    invalidPropertyName = true;
                }
            }

            if (invalidPropertyName) {
                // need to walk back to the '.' and replace
                // with '["<hint>"]
                var dotCursor = session.findPreviousDot();
                if (dotCursor) {
                    completion = "[\"" + completion + "\"]";
                    start.line = dotCursor.line;
                    start.ch = dotCursor.ch - 1;
                }
            }
        }
        // Replace the current token with the completion
        // HACK (tracking adobe/brackets#1688): We talk to the private CodeMirror instance
        // directly to replace the range instead of using the Document, as we should. The
        // reason is due to a flaw in our current document synchronization architecture when
        // inline editors are open.
        session.editor._codeMirror.replaceRange(completion, start, end);

        // Return false to indicate that another hinting session is not needed
        return false;
    };

    // load the extension
    AppInit.appReady(function () {

        /*
         * When the editor is changed, reset the hinting session and cached
         * information, and reject any pending deferred requests.
         *
         * @param {Editor} editor - editor context to be initialized.
         * @param {boolean} primePump - true if the pump should be primed.
         */
        function initializeSession(editor, primePump) {
            ScopeManager.handleEditorChange(editor.document);
            session = new Session(editor);
            cachedHints = null;

            // prime pump for hints so the first user request is fast
            if (primePump) {
                ScopeManager.requestHints(session, session.editor.document, 0);
            }
        }

        /*
         * Install editor change listeners
         *
         * @param {Editor} editor - editor context on which to listen for
         *      changes
         */
        function installEditorListeners(editor) {
            // always clean up cached scope and hint info
            cachedCursor = null;
            cachedHints = null;
            cachedType = null;

            if (editor && editor.getLanguageForSelection().getId() === HintUtils.LANGUAGE_ID) {
                initializeSession(editor, true);
                $(editor)
                    .on(HintUtils.eventName("change"), function () {
                        ScopeManager.handleFileChange(editor.document);
                    });
            } else {
                session = null;
            }
        }

        /*
         * Uninstall editor change listeners
         *
         * @param {Editor} editor - editor context on which to stop listening
         *      for changes
         */
        function uninstallEditorListeners(editor) {
            $(editor)
                .off(HintUtils.eventName("change"));
        }

        /*
         * Handle the activeEditorChange event fired by EditorManager.
         * Uninstalls the change listener on the previous editor
         * and installs a change listener on the new editor.
         *
         * @param {Event} event - editor change event (ignored)
         * @param {Editor} current - the new current editor context
         * @param {Editor} previous - the previous editor context
         */
        function handleActiveEditorChange(event, current, previous) {
            uninstallEditorListeners(previous);
            installEditorListeners(current);
        }

        /*
         * Handle JumptoDefiniton menu/keyboard command.
         */
        function handleJumpToDefinition() {
            var offset     = session.getOffset(),
                response   = ScopeManager.requestJumptoDef(session, session.editor.document, offset);

            if (response.hasOwnProperty("promise")) {
                response.promise.done(function (jumpResp) {

                    if (jumpResp.resultFile) {
                        if (jumpResp.resultFile !== jumpResp.file) {
                            var resolvedPath = ScopeManager.getResolvedPath(jumpResp.resultFile);
                            if (resolvedPath) {
                                CommandManager.execute(Commands.FILE_OPEN, {fullPath: resolvedPath})
                                    .done(function () {
                                        session.editor.setSelection(jumpResp.start, jumpResp.end, true);
                                    });
                            }
                        } else {
                            session.editor.setSelection(jumpResp.start, jumpResp.end, true);
                        }
                    }

                }).fail(function () {
                    response.reject();
                });
            }
        }

        // Register command handler
        CommandManager.register(Strings.CMD_JUMPTO_DEFINITION, JUMPTO_DEFINITION, handleJumpToDefinition);

        // Add the menu item
        var menu = Menus.getMenu(Menus.AppMenuBar.NAVIGATE_MENU);
        if (menu) {
            menu.addMenuItem(JUMPTO_DEFINITION, KeyboardPrefs.jumptoDefinition, Menus.AFTER, Commands.NAVIGATE_GOTO_DEFINITION);
        }

        ExtensionUtils.loadStyleSheet(module, "styles/brackets-js-hints.css");

        // uninstall/install change listener as the active editor changes
        $(EditorManager)
            .on(HintUtils.eventName("activeEditorChange"),
                handleActiveEditorChange);

        // immediately install the current editor
        installEditorListeners(EditorManager.getActiveEditor());

        var jsHints = new JSHints();
        CodeHintManager.registerHintProvider(jsHints, [HintUtils.LANGUAGE_ID, "html"], 0);

        // for unit testing
        exports.getSession = getSession;
        exports.jsHintProvider = jsHints;
        exports.initializeSession = initializeSession;
        exports.handleJumpToDefinition = handleJumpToDefinition;
    });
});


// It is expected that, when .add() returns false, the consumer
// of the DirWriter will pause until a "drain" event occurs. Note
// that this is *almost always going to be the case*, unless the
// thing being written is some sort of unsupported type, and thus
// skipped over.

module.exports = DirWriter

var fs = require("graceful-fs")
  , fstream = require("../fstream.js")
  , Writer = require("./writer.js")
  , inherits = require("inherits")
  , mkdir = require("mkdirp")
  , path = require("path")
  , collect = require("./collect.js")

inherits(DirWriter, Writer)

function DirWriter (props) {
  var me = this
  if (!(me instanceof DirWriter)) me.error(
    "DirWriter must be called as constructor.", null, true)

  // should already be established as a Directory type
  if (props.type !== "Directory" || !props.Directory) {
    me.error("Non-directory type "+ props.type + " " +
                    JSON.stringify(props), null, true)
  }

  Writer.call(this, props)
}

DirWriter.prototype._create = function () {
  var me = this
  mkdir(me._path, Writer.dirmode, function (er) {
    if (er) return me.error(er)
    // ready to start getting entries!
    me.ready = true
    me.emit("ready")
    me._process()
  })
}

// a DirWriter has an add(entry) method, but its .write() doesn't
// do anything.  Why a no-op rather than a throw?  Because this
// leaves open the door for writing directory metadata for
// gnu/solaris style dumpdirs.
DirWriter.prototype.write = function () {
  return true
}

DirWriter.prototype.end = function () {
  this._ended = true
  this._process()
}

DirWriter.prototype.add = function (entry) {
  var me = this

  // console.error("\tadd", entry._path, "->", me._path)
  collect(entry)
  if (!me.ready || me._currentEntry) {
    me._buffer.push(entry)
    return false
  }

  // create a new writer, and pipe the incoming entry into it.
  if (me._ended) {
    return me.error("add after end")
  }

  me._buffer.push(entry)
  me._process()

  return 0 === this._buffer.length
}

DirWriter.prototype._process = function () {
  var me = this

  // console.error("DW Process p=%j", me._processing, me.basename)

  if (me._processing) return

  var entry = me._buffer.shift()
  if (!entry) {
    // console.error("DW Drain")
    me.emit("drain")
    if (me._ended) me._finish()
    return
  }

  me._processing = true
  // console.error("DW Entry", entry._path)

  me.emit("entry", entry)

  // ok, add this entry
  //
  // don't allow recursive copying
  var p = entry
  do {
    var pp = p._path || p.path
    if (pp === me.root._path || pp === me._path ||
        (pp && pp.indexOf(me._path) === 0)) {
      // console.error("DW Exit (recursive)", entry.basename, me._path)
      me._processing = false
      if (entry._collected) entry.pipe()
      return me._process()
    }
  } while (p = p.parent)

  // console.error("DW not recursive")

  // chop off the entry's root dir, replace with ours
  var props = { parent: me
              , root: me.root || me
              , type: entry.type
              , depth: me.depth + 1 }

  var p = entry._path || entry.path || entry.props.path
  if (entry.parent) {
    p = p.substr(entry.parent._path.length + 1)
  }
  // get rid of any ../../ shenanigans
  props.path = path.join(me.path, path.join("/", p))

  // if i have a filter, the child should inherit it.
  props.filter = me.filter

  // all the rest of the stuff, copy over from the source.
  Object.keys(entry.props).forEach(function (k) {
    if (!props.hasOwnProperty(k)) {
      props[k] = entry.props[k]
    }
  })

  // not sure at this point what kind of writer this is.
  var child = me._currentChild = new Writer(props)
  child.on("ready", function () {
    // console.error("DW Child Ready", child.type, child._path)
    // console.error("  resuming", entry._path)
    entry.pipe(child)
    entry.resume()
  })

  // XXX Make this work in node.
  // Long filenames should not break stuff.
  child.on("error", function (er) {
    if (child._swallowErrors) {
      me.warn(er)
      child.emit("end")
      child.emit("close")
    } else {
      me.emit("error", er)
    }
  })

  // we fire _end internally *after* end, so that we don't move on
  // until any "end" listeners have had their chance to do stuff.
  child.on("close", onend)
  var ended = false
  function onend () {
    if (ended) return
    ended = true
    // console.error("* DW Child end", child.basename)
    me._currentChild = null
    me._processing = false
    me._process()
  }
}


/*
 * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50 */
/*global $, define, require, describe, it, expect, beforeEach, afterEach, waitsFor, runs, waitsForDone */
define(function (require, exports, module) {
    'use strict';

    // Load dependent modules
    var ProjectManager,     // Load from brackets.test
        CommandManager,     // Load from brackets.test
        Commands            = require("command/Commands"),
        SpecRunnerUtils     = require("spec/SpecRunnerUtils");

    describe("ProjectManager", function () {

        this.category = "integration";

        var testPath = SpecRunnerUtils.getTestPath("/spec/ProjectManager-test-files"),
            testWindow,
            brackets;

        beforeEach(function () {
            SpecRunnerUtils.createTestWindowAndRun(this, function (w) {
                testWindow = w;

                // Load module instances from brackets.test
                brackets = testWindow.brackets;
                ProjectManager = testWindow.brackets.test.ProjectManager;
                CommandManager = testWindow.brackets.test.CommandManager;
            });
        });

        afterEach(function () {
            SpecRunnerUtils.closeTestWindow();
        });

        describe("createNewItem", function () {
            it("should create a new file with a given name", function () {
                var didCreate = false, gotError = false;

                SpecRunnerUtils.loadProjectInTestWindow(testPath);

                runs(function () {
                    // skip rename
                    ProjectManager.createNewItem(testPath, "Untitled.js", true)
                        .done(function () { didCreate = true; })
                        .fail(function () { gotError = true; });
                });
                waitsFor(function () { return didCreate && !gotError; }, "ProjectManager.createNewItem() timeout", 1000);

                var error, stat, complete = false;
                var filePath = testPath + "/Untitled.js";
                runs(function () {
                    brackets.fs.stat(filePath, function (err, _stat) {
                        error = err;
                        stat = _stat;
                        complete = true;
                    });
                });

                waitsFor(function () { return complete; }, 1000);

                var unlinkError = brackets.fs.NO_ERROR;
                runs(function () {
                    expect(error).toBeFalsy();
                    expect(stat.isFile()).toBe(true);

                    // delete the new file
                    complete = false;
                    brackets.fs.unlink(filePath, function (err) {
                        unlinkError = err;
                        complete = true;
                    });
                });
                waitsFor(
                    function () {
                        return complete && (unlinkError === brackets.fs.NO_ERROR);
                    },
                    "unlink() failed to cleanup Untitled.js, err=" + unlinkError,
                    1000
                );
            });

            it("should fail when a file already exists", function () {
                var didCreate = false, gotError = false;

                SpecRunnerUtils.loadProjectInTestWindow(testPath);

                runs(function () {
                    // skip rename
                    ProjectManager.createNewItem(testPath, "file.js", true)
                        .done(function () { didCreate = true; })
                        .fail(function () { gotError = true; });
                });
                waitsFor(function () { return !didCreate && gotError; }, "ProjectManager.createNewItem() timeout", 1000);

                runs(function () {
                    expect(gotError).toBeTruthy();
                    expect(didCreate).toBeFalsy();
                });
            });

            it("should fail when a file name matches a directory that already exists", function () {
                var didCreate = false, gotError = false;

                SpecRunnerUtils.loadProjectInTestWindow(testPath);

                runs(function () {
                    // skip rename
                    ProjectManager.createNewItem(testPath, "directory", true)
                        .done(function () { didCreate = true; })
                        .fail(function () { gotError = true; });
                });
                waitsFor(function () { return !didCreate && gotError; }, "ProjectManager.createNewItem() timeout", 1000);

                runs(function () {
                    expect(gotError).toBeTruthy();
                    expect(didCreate).toBeFalsy();
                });
            });

            it("should fail when file name contains special characters", function () {
                var chars = "/?*:;{}<>\\";
                var i = 0;
                var len = chars.length;
                var charAt, didCreate, gotError;

                SpecRunnerUtils.loadProjectInTestWindow(testPath);

                function createFile() {
                    // skip rename
                    ProjectManager.createNewItem(testPath, "file" + charAt + ".js", true)
                        .done(function () { didCreate = true; })
                        .fail(function () { gotError = true; });
                }

                function waitForFileCreate() {
                    return !didCreate && gotError;
                }

                function assertFile() {
                    expect(gotError).toBeTruthy();
                    expect(didCreate).toBeFalsy();
                }

                for (i = 0; i < len; i++) {
                    didCreate = false;
                    gotError = false;
                    charAt = chars.charAt(i);

                    runs(createFile);
                    waitsFor(waitForFileCreate, "ProjectManager.createNewItem() timeout", 1000);
                    runs(assertFile);
                }
            });
        });

        describe("Selection indicator", function () {

            function expectSelected(fullPath) {
                var $projectTreeItems = testWindow.$("#project-files-container > ul").children();
                var $selectedItem = $projectTreeItems.find("a.jstree-clicked");
                if (!fullPath) {
                    expect($selectedItem.length).toBe(0);
                } else {
                    expect($selectedItem.length).toBe(1);
                    expect($selectedItem.parent().data("entry").fullPath).toBe(fullPath);
                }
            }

            it("should deselect after opening file not rendered in tree", function () {
                SpecRunnerUtils.loadProjectInTestWindow(testPath);
                var promise,
                    exposedFile   = testPath + "/file.js",
                    unexposedFile = testPath + "/directory/file.js";

                runs(function () {
                    promise = CommandManager.execute(Commands.FILE_OPEN, { fullPath: exposedFile });
                    waitsForDone(promise);
                });
                runs(function () {
                    expectSelected(exposedFile);

                    promise = CommandManager.execute(Commands.FILE_OPEN, { fullPath: unexposedFile });
                    waitsForDone(promise);
                });
                runs(function () {
                    expectSelected(null);
                });
            });

            function findExtantNode(fullPath) {
                var $treeItems = testWindow.$("#project-files-container li"),
                    $result;
                $treeItems.is(function () {
                    var $treeNode = testWindow.$(this),
                        entry = $treeNode.data("entry");
                    if (entry && entry.fullPath === fullPath) {
                        $result = $treeNode;
                        return true;
                    }
                    return false;
                });
                return $result;
            }
            function toggleFolder(fullPath, open) {
                var $treeNode = findExtantNode(fullPath);

                var expectedClass = open ? "jstree-open" : "jstree-closed";
                expect($treeNode.hasClass(expectedClass)).toBe(false);

                $treeNode.children("a").click();

                // if a folder has never been expanded before, this will be async
                waitsFor(function () {
                    return $treeNode.hasClass(expectedClass);
                }, (open ? "Open" : "Close") + " tree node", 1000);
            }

            it("should reselect previously selected file when made visible again", function () {
                SpecRunnerUtils.loadProjectInTestWindow(testPath);
                var promise,
                    initialFile  = testPath + "/file.js",
                    folder       = testPath + "/directory/",
                    fileInFolder = testPath + "/directory/file.js";

                runs(function () {
                    promise = CommandManager.execute(Commands.FILE_OPEN, { fullPath: initialFile });
                    waitsForDone(promise);
                });
                runs(function () {
                    expectSelected(initialFile);
                    toggleFolder(folder, true);     // open folder
                });
                runs(function () {                  // open file in folder
                    promise = CommandManager.execute(Commands.FILE_OPEN, { fullPath: fileInFolder });
                    waitsForDone(promise);
                });
                runs(function () {
                    expectSelected(fileInFolder);
                    toggleFolder(folder, false);    // close folder
                });
                runs(function () {
                    expectSelected(folder);
                    toggleFolder(folder, true);     // open folder again
                });
                runs(function () {
                    expectSelected(fileInFolder);
                });
            });

            it("should deselect after opening file hidden in tree, but select when made visible again", function () {
                SpecRunnerUtils.loadProjectInTestWindow(testPath);
                var promise,
                    initialFile  = testPath + "/file.js",
                    folder       = testPath + "/directory/",
                    fileInFolder = testPath + "/directory/file.js";

                runs(function () {
                    promise = CommandManager.execute(Commands.FILE_OPEN, { fullPath: initialFile });
                    waitsForDone(promise);
                });
                runs(function () {
                    expectSelected(initialFile);
                    toggleFolder(folder, true);     // open folder
                });
                runs(function () {
                    toggleFolder(folder, false);    // close folder
                });
                runs(function () {                  // open file in folder
                    promise = CommandManager.execute(Commands.FILE_OPEN, { fullPath: fileInFolder });
                    waitsForDone(promise);
                });
                runs(function () {
                    expectSelected(null);
                    toggleFolder(folder, true);     // open folder again
                });
                runs(function () {
                    expectSelected(fileInFolder);
                });
            });
        });

        describe("File Display", function () {
            it("should not show useless directory entries", function () {
                var shouldShow = ProjectManager.shouldShow;
                var makeEntry = function (name) {
                    return { name: name };
                };

                expect(shouldShow(makeEntry(".gitmodules"))).toBe(false);
                expect(shouldShow(makeEntry("foobar"))).toBe(true);
                expect(shouldShow(makeEntry("pyc.py"))).toBe(true);
                expect(shouldShow(makeEntry("module.pyc"))).toBe(false);
            });
        });

    });
});


* Deterimine if the cursor is located in the name of a function declaration.

* This is so we can suppress hints when in a funtion name, as we do for variable and

* parameter declarations, but we can tell those from the token itself rather than having

Session.prototype.isFunctionName = function () {

/*
 * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50, regexp: true */
/*global define, brackets, $ */

define(function (require, exports, module) {
    "use strict";

    var StringMatch     = brackets.getModule("utils/StringMatch"),
        LanguageManager = brackets.getModule("language/LanguageManager"),
        HTMLUtils       = brackets.getModule("language/HTMLUtils"),
        TokenUtils      = brackets.getModule("utils/TokenUtils"),
        HintUtils       = require("HintUtils"),
        ScopeManager    = require("ScopeManager");

    /**
     * Session objects encapsulate state associated with a hinting session
     * and provide methods for updating and querying the session.
     *
     * @constructor
     * @param {Editor} editor - the editor context for the session
     */
    function Session(editor) {
        this.editor = editor;
        this.path = editor.document.file.fullPath;
        this.ternHints = [];
        this.ternProperties = [];
        this.fnType = null;
        this.builtins = ScopeManager.getBuiltins();
        this.builtins.push("requirejs.js");     // consider these globals as well.
    }

    /**
     * Get the name of the file associated with the current session
     *
     * @return {string} - the full pathname of the file associated with the
     *      current session
     */
    Session.prototype.getPath = function () {
        return this.path;
    };

    /**
     * Get the current cursor position.
     *
     * @return {{line: number, ch: number}} - the current cursor position
     */
    Session.prototype.getCursor = function () {
        return this.editor.getCursorPos();
    };

    /**
     * Get the text of a line.
     *
     * @param {number} line - the line number
     * @return {string} - the text of the line
     */
    Session.prototype.getLine = function (line) {
        var doc = this.editor.document;
        return doc.getLine(line);
    };

    /**
     * Get the offset of the current cursor position
     *
     * @return {number} - the offset into the current document of the current
     *      cursor
     */
    Session.prototype.getOffset = function () {
        var cursor = this.getCursor();

        return this.editor.indexFromPos(cursor);
    };

    /**
     * Get the token at the given cursor position, or at the current cursor
     * if none is given.
     *
     * @param {?{line: number, ch: number}} cursor - the cursor position
     *      at which to retrieve a token
     * @return {Object} - the CodeMirror token at the given cursor position
     */
    Session.prototype.getToken = function (cursor) {
        var cm = this.editor._codeMirror;

        if (cursor) {
            return cm.getTokenAt(cursor);
        } else {
            return cm.getTokenAt(this.getCursor());
        }
    };

    /**
     * Get the token after the one at the given cursor position
     *
     * @param {{line: number, ch: number}} cursor - cursor position before
     *      which a token should be retrieved
     * @return {Object} - the CodeMirror token after the one at the given
     *      cursor position
     */
    Session.prototype.getNextTokenOnLine = function (cursor) {
        cursor = this.getNextCursorOnLine(cursor);
        if (cursor) {
            return this.getToken(cursor);
        }

        return null;
    };

    /**
     * Get the next cursor position on the line, or null if there isn't one.
     *
     * @return {?{line: number, ch: number}} - the cursor position
     *      immediately following the current cursor position, or null if
     *      none exists.
     */
    Session.prototype.getNextCursorOnLine = function (cursor) {
        var doc     = this.editor.document,
            line    = doc.getLine(cursor.line);

        if (cursor.ch < line.length) {
            return {
                ch  : cursor.ch + 1,
                line: cursor.line
            };
        } else {
            return null;
        }
    };

    /**
     * Get the token before the one at the given cursor position
     *
     * @param {{line: number, ch: number}} cursor - cursor position after
     *      which a token should be retrieved
     * @return {Object} - the CodeMirror token before the one at the given
     *      cursor position
     */
    Session.prototype._getPreviousToken = function (cursor) {
        var token   = this.getToken(cursor),
            prev    = token,
            doc     = this.editor.document;

        do {
            if (prev.start < cursor.ch) {
                cursor.ch = prev.start;
            } else if (prev.start > 0) {
                cursor.ch = prev.start - 1;
            } else if (cursor.line > 0) {
                cursor.ch = doc.getLine(cursor.line - 1).length;
                cursor.line--;
            } else {
                break;
            }
            prev = this.getToken(cursor);
        } while (prev.string.trim() === "");

        return prev;
    };

    /**
     * Calculate a query string relative to the current cursor position
     * and token. E.g., from a state "identi<cursor>er", the query string is
     * "identi".
     *
     * @return {string} - the query string for the current cursor position
     */
    Session.prototype.getQuery = function () {
        var cursor  = this.getCursor(),
            token   = this.getToken(cursor),
            query   = "";

        if (token) {
            // If the token string is not an identifier, then the query string
            // is empty.
            if (HintUtils.maybeIdentifier(token.string)) {
                query = token.string.substring(0, token.string.length - (token.end - cursor.ch));
                query = query.trim();
            }
        }
        return query;
    };

    /**
     * Find the context of a property lookup. For example, for a lookup
     * foo(bar, baz(quux)).prop, foo is the context.
     *
     * @param {{line: number, ch: number}} cursor - the cursor position
     *      at which context information is to be retrieved
     * @param {number} depth - the current depth of the parenthesis stack, or
     *      undefined if the depth is 0.
     * @return {string} - the context for the property that was looked up
     */
    Session.prototype.getContext = function (cursor, depth) {
        var token = this.getToken(cursor);

        if (depth === undefined) {
            depth = 0;
        }

        if (token.string === ")") {
            this._getPreviousToken(cursor);
            return this.getContext(cursor, ++depth);
        } else if (token.string === "(") {
            this._getPreviousToken(cursor);
            return this.getContext(cursor, --depth);
        } else {
            if (depth > 0 || token.string === ".") {
                this._getPreviousToken(cursor);
                return this.getContext(cursor, depth);
            } else {
                return token.string;
            }
        }
    };

    /**
     * @return {{line:number, ch:number}} - the line, col info for where the previous "."
     *      in a property lookup occurred, or undefined if no previous "." was found.
     */
    Session.prototype.findPreviousDot = function () {
        var cursor = this.getCursor(),
            token = this.getToken(cursor);

        // If the cursor is right after the dot, then the current token will be "."
        if (token && token.string === ".") {
            return cursor;
        } else {
            // If something has been typed like 'foo.b' then we have to look back 2 tokens
            // to get past the 'b' token
            token = this._getPreviousToken(cursor);
            if (token && token.string === ".") {
                return cursor;
            }
        }
        return undefined;
    };

    /**
     * Get the type of the current session, i.e., whether it is a property
     * lookup and, if so, what the context of the lookup is.
     *
     * @return {{property: boolean,
                 showFunctionType:boolean,
                 context: string,
                 functionCallPos: {line:number, ch:number}}} - an Object consisting
     *      of a {boolean} "property" that indicates whether or not the type of
     *      the session is a property lookup, and a {string} "context" that
     *      indicates the object context (as described in getContext above) of
     *      the property lookup, or null if there is none. The context is
     *      always null for non-property lookups.
     *      a {boolean} "showFunctionType" indicating if the function type should
     *      be displayed instead of normal hints.  If "showFunctionType" is true, then
     *      then "functionCallPos" will be an object with line & col information of the
     *      function being called
     */
    Session.prototype.getType = function () {
        function getLexicalState(token) {
            if (token.state.lexical) {
                // in a javascript file this is just in the state field
                return token.state.lexical;
            } else if (token.state.localState && token.state.localState.lexical) {
                // inline javascript in an html file will have this in
                // the localState field
                return token.state.localState.lexical;
            }
        }
        var propertyLookup   = false,
            inFunctionCall   = false,
            showFunctionType = false,
            context          = null,
            cursor           = this.getCursor(),
            functionCallPos,
            token            = this.getToken(cursor),
            lexical;

        if (token) {
            // if this token is part of a function call, then the tokens lexical info
            // will be annotated with "call"
            lexical = getLexicalState(token);
            if (lexical.info === "call") {
                inFunctionCall = true;
                if (this.getQuery().length > 0) {
                    inFunctionCall = false;
                    showFunctionType = false;
                } else {
                    showFunctionType = true;
                    // we need to find the location of the called function so that we can request the functions type.
                    // the token's lexical info will contain the column where the open "(" for the
                    // function call occurrs, but for whatever reason it does not have the line, so
                    // we have to walk back and try to find the correct location.  We do this by walking
                    // up the lines starting with the line the token is on, and seeing if any of the lines
                    // have "(" at the column indicated by the tokens lexical state.
                    // We walk back 9 lines, as that should be far enough to find most function calls,
                    // and it will prevent us from walking back thousands of lines if something went wrong.
                    // there is nothing magical about 9 lines, and it can be adjusted if it doesn't seem to be
                    // working well
                    var col = lexical.column,
                        line,
                        e,
                        found;
                    for (line = this.getCursor().line, e = Math.max(0, line - 9), found = false; line >= e; --line) {
                        if (this.getLine(line).charAt(col) === "(") {
                            found = true;
                            break;
                        }
                    }
                    if (found) {
                        functionCallPos = {line: line, ch: col};
                    }
                }
            }
            if (token.type === "property") {
                propertyLookup = true;
            }
            if (this.findPreviousDot()) {
                propertyLookup = true;
                context = this.getContext(cursor);
            }
            if (propertyLookup) { showFunctionType = false; }
        }

        return {
            property: propertyLookup,
            inFunctionCall: inFunctionCall,
            showFunctionType: showFunctionType,
            functionCallPos: functionCallPos,
            context: context
        };
    };

    /**
     * Get a list of hints for the current session using the current scope
     * information.
     *
     * @param {string} query - the query prefix
     * @param {StringMatcher} matcher - the class to find query matches and sort the results
     * @return {Array.<Object>} - the sorted list of hints for the current
     *      session.
     */
    Session.prototype.getHints = function (query, matcher) {

        if (query === undefined) {
            query = "";
        }

        var MAX_DISPLAYED_HINTS = 500,
            type = this.getType(),
            builtins = this.builtins,
            hints;

        /**
         *  Is the origin one of the builtin files.
         *
         * @param {string} origin
         */
        function isBuiltin(origin) {
            return builtins.indexOf(origin) !== -1;
        }

        /**
         *  Filter an array hints using a given query and matcher.
         *  The hints are returned in the format of the matcher.
         *  The matcher returns the value in the "label" property,
         *  the match score in "matchGoodness" property.
         *
         * @param {Array} hints - array of hints
         * @param {StringMatcher} matcher
         * @returns {Array} - array of matching hints.
         */
        function filterWithQueryAndMatcher(hints, matcher) {
            var matchResults = $.map(hints, function (hint) {
                var searchResult = matcher.match(hint.value, query);
                if (searchResult) {
                    searchResult.value = hint.value;
                    searchResult.guess = hint.guess;

                    if (hint.keyword !== undefined) {
                        searchResult.keyword = hint.keyword;
                    }

                    if (hint.literal !== undefined) {
                        searchResult.literal = hint.literal;
                    }

                    if (hint.depth !== undefined) {
                        searchResult.depth = hint.depth;
                    }

                    if (!type.property && !type.showFunctionType && hint.origin &&
                            isBuiltin(hint.origin)) {
                        searchResult.builtin = 1;
                    } else {
                        searchResult.builtin = 0;
                    }
                }

                return searchResult;
            });

            return matchResults;
        }

        if (type.property) {
            hints = this.ternHints || [];
            hints = filterWithQueryAndMatcher(hints, matcher);

            // If there are no hints then switch over to guesses.
            if (hints.length === 0) {
                hints = filterWithQueryAndMatcher(this.ternProperties, matcher);
            }

            StringMatch.multiFieldSort(hints, { matchGoodness: 0, value: 1 });
        } else if (type.showFunctionType) {
            hints = this.getFunctionTypeHint();
        } else {     // identifiers, literals, and keywords
            hints = this.ternHints || [];
            hints = hints.concat(HintUtils.LITERALS);
            hints = hints.concat(HintUtils.KEYWORDS);
            hints = filterWithQueryAndMatcher(hints, matcher);
            StringMatch.multiFieldSort(hints, { matchGoodness: 0, depth: 1, builtin: 2, value: 3 });
        }

        if (hints.length > MAX_DISPLAYED_HINTS) {
            hints = hints.slice(0, MAX_DISPLAYED_HINTS);
        }
        return hints;
    };

    Session.prototype.setTernHints = function (newHints) {
        this.ternHints = newHints;
    };
    Session.prototype.setTernProperties = function (newProperties) {
        this.ternProperties = newProperties;
    };
    Session.prototype.setFnType = function (newFnType) {
        this.fnType = newFnType;
    };

    /**
     * Get the function type hint.  This will format the hint so
     * that it has the called variable name instead of just "fn()".
     */
    Session.prototype.getFunctionTypeHint = function () {
        var fnHint = this.fnType,
            hints = [];

        if (fnHint && (fnHint.substring(0, 3) === "fn(")) {
            var sessionType = this.getType(),
                cursor = sessionType.functionCallPos,
                token = cursor ? this.getToken(cursor) : undefined,
                varName;
            if (token &&
                    // only change the 'fn' when the token looks like a function
                    // name, and isn't some other kind of expression
                    (token.type === "variable" ||
                     token.type === "variable-2" ||
                     token.type === "property")) {
                varName = token.string;
                if (varName) {
                    fnHint = varName + fnHint.substr(2);
                }
            }
            hints[0] = {value: fnHint, positions: []};
        }
        return hints;
    };

    /**
     * Get the javascript text of the file open in the editor for this Session.
     * For a javascript file, this is just the text of the file.  For an HTML file,
     * this will be only the text in the <script> tags.  This is so that we can pass
     * just the javascript text to tern, and avoid confusing it with HTML tags, since it
     * only knows how to parse javascript.
     * @return {string} - the "javascript" text that can be sent to Tern.
     */
    Session.prototype.getJavascriptText = function () {
        if (LanguageManager.getLanguageForPath(this.editor.document.file.fullPath).getId() === "html") {
            // HTML file - need to send back only the bodies of the
            // <script> tags
            var text = "",
                offset = this.getOffset(),
                cursor = this.getCursor(),
                editor = this.editor,
                scriptBlocks = HTMLUtils.findBlocks(editor, "javascript");

            // Add all the javascript text
            // For non-javascript blocks we replace everything except for newlines
            // with whitespace.  This is so that the offset and cursor positions
            // we get from the document still work.
            // Alternatively we could strip the non-javascript text, and modify the offset,
            // and/or cursor, but then we have to remember how to reverse the translation
            // to support jump-to-definition
            var htmlStart = {line: 0, ch: 0};
            scriptBlocks.forEach(function (scriptBlock) {
                var start = scriptBlock.start,
                    end = scriptBlock.end;

                // get the preceding html text, and replace it with whitespace
                var htmlText = editor.document.getRange(htmlStart, start);
                htmlText = htmlText.replace(/./g, " ");

                htmlStart = end;
                text += htmlText + scriptBlock.text;
            });

            return text;
        } else {
            // Javascript file, just return the text
            return this.editor.document.getText();
        }
    };

    module.exports = Session;
});


var Traverse = require('traverse');
var EventEmitter = require('events').EventEmitter;

module.exports = Chainsaw;
function Chainsaw (builder) {
    var saw = Chainsaw.saw(builder, {});
    var r = builder.call(saw.handlers, saw);
    if (r !== undefined) saw.handlers = r;
    saw.record();
    return saw.chain();
};

Chainsaw.light = function ChainsawLight (builder) {
    var saw = Chainsaw.saw(builder, {});
    var r = builder.call(saw.handlers, saw);
    if (r !== undefined) saw.handlers = r;
    return saw.chain();
};

Chainsaw.saw = function (builder, handlers) {
    var saw = new EventEmitter;
    saw.handlers = handlers;
    saw.actions = [];

    saw.chain = function () {
        var ch = Traverse(saw.handlers).map(function (node) {
            if (this.isRoot) return node;
            var ps = this.path;

            if (typeof node === 'function') {
                this.update(function () {
                    saw.actions.push({
                        path : ps,
                        args : [].slice.call(arguments)
                    });
                    return ch;
                });
            }
        });

        process.nextTick(function () {
            saw.emit('begin');
            saw.next();
        });

        return ch;
    };

    saw.pop = function () {
        return saw.actions.shift();
    };

    saw.next = function () {
        var action = saw.pop();

        if (!action) {
            saw.emit('end');
        }
        else if (!action.trap) {
            var node = saw.handlers;
            action.path.forEach(function (key) { node = node[key] });
            node.apply(saw.handlers, action.args);
        }
    };

    saw.nest = function (cb) {
        var args = [].slice.call(arguments, 1);
        var autonext = true;

        if (typeof cb === 'boolean') {
            var autonext = cb;
            cb = args.shift();
        }

        var s = Chainsaw.saw(builder, {});
        var r = builder.call(s.handlers, s);

        if (r !== undefined) s.handlers = r;

        // If we are recording...
        if ("undefined" !== typeof saw.step) {
            // ... our children should, too
            s.record();
        }

        cb.apply(s.chain(), args);
        if (autonext !== false) s.on('end', saw.next);
    };

    saw.record = function () {
        upgradeChainsaw(saw);
    };

    ['trap', 'down', 'jump'].forEach(function (method) {
        saw[method] = function () {
            throw new Error("To use the trap, down and jump features, please "+
                            "call record() first to start recording actions.");
        };
    });

    return saw;
};

function upgradeChainsaw(saw) {
    saw.step = 0;

    // override pop
    saw.pop = function () {
        return saw.actions[saw.step++];
    };

    saw.trap = function (name, cb) {
        var ps = Array.isArray(name) ? name : [name];
        saw.actions.push({
            path : ps,
            step : saw.step,
            cb : cb,
            trap : true
        });
    };

    saw.down = function (name) {
        var ps = (Array.isArray(name) ? name : [name]).join('/');
        var i = saw.actions.slice(saw.step).map(function (x) {
            if (x.trap && x.step <= saw.step) return false;
            return x.path.join('/') == ps;
        }).indexOf(true);

        if (i >= 0) saw.step += i;
        else saw.step = saw.actions.length;

        var act = saw.actions[saw.step - 1];
        if (act && act.trap) {
            // It's a trap!
            saw.step = act.step;
            act.cb();
        }
        else saw.next();
    };

    saw.jump = function (step) {
        saw.step = step;
        saw.next();
    };
};


<!doctype html>

	<meta charset="utf-8">
	<title>jQuery UI Unit Tests</title>

	<link rel="stylesheet" href="../../themes/base/jquery.ui.core.css">
	<link rel="stylesheet" href="../../themes/base/jquery.ui.theme.css">
	<link rel="stylesheet" href="../index.css">

jQuery UI Unit Tests

Full Test Suite

All

Core

Core

Widget

Interactions

Draggable

Droppable

Resizable

Selectable

Sortable

Widgets

Accordion

Autocomplete

Button

Datepicker

Dialog

Menu

Progressbar

Slider

Spinner

Tabs

Tooltip

Utilities

Position

Effects

Effects