/* Forms.less
 * Base styles for various input types, form layouts, and states
 * ------------------------------------------------------------- */

// FORM STYLES
// -----------

form {
  margin-bottom: @baseline;
}

// Groups of fields with labels on top (legends)
fieldset {
  margin-bottom: @baseline;
  padding-top: @baseline;
  legend {
    display: block;
    padding-left: 150px;
    font-size: @basefont * 1.5;
    line-height: 1;
    color: @grayDark;
    *padding: 0 0 5px 145px; /* IE6-7 */
    *line-height: 1.5; /* IE6-7 */
  }
}

// Parent element that clears floats and wraps labels and fields together
form .clearfix {
  margin-bottom: @baseline;
  .clearfix()
}

// Set font for forms
label,
input,
select,
textarea {
  #font > .sans-serif(normal,13px,normal);
}

// Float labels left
label {
  padding-top: 6px;
  font-size: @basefont;
  line-height: @baseline;
  float: left;
  width: 130px;
  text-align: right;
  color: @grayDark;
}

// Shift over the inside div to align all label's relevant content
form .input {
  margin-left: 150px;
}

// Checkboxs and radio buttons
input[type=checkbox],
input[type=radio] {
  cursor: pointer;
}

// Inputs, Textareas, Selects
input,
textarea,
select,
.uneditable-input {
  display: inline-block;
  width: 210px;
  height: @baseline;
  padding: 4px;
  font-size: @basefont;
  line-height: @baseline;
  color: @gray;
  border: 1px solid #ccc;
  .border-radius(3px);
}

// remove padding from select
select {
  padding: initial;
}

// mini reset for non-html5 file types
input[type=checkbox],
input[type=radio] {
  width: auto;
  height: auto;
  padding: 0;
  margin: 3px 0;
  *margin-top: 0; /* IE6-7 */
  line-height: normal;
  border: none;
}

input[type=file] {
  background-color: @white;
  padding: initial;
  border: initial;
  line-height: initial;
  .box-shadow(none);
}

input[type=button],
input[type=reset],
input[type=submit] {
  width: auto;
  height: auto;
}

select,
input[type=file] {
  height: @baseline * 1.5; // In IE7, the height of the select element cannot be changed by height, only font-size
  *height: auto; // Reset for IE7
  line-height: @baseline * 1.5;
  *margin-top: 4px; /* For IE7, add top margin to align select with labels */
}

// Make multiple select elements height not fixed
select[multiple] {
  height: inherit;
  background-color: @white; // Fixes Chromium bug of unreadable items
}

textarea {
  height: auto;
}

// For text that needs to appear as an input but should not be an input
.uneditable-input {
  background-color: @white;
  display: block;
  border-color: #eee;
  .box-shadow(inset 0 1px 2px rgba(0,0,0,.025));
  cursor: not-allowed;
}

// Placeholder text gets special styles; can't be bundled together though for some reason
:-moz-placeholder {
  color: @grayLight;
}
::-webkit-input-placeholder {
  color: @grayLight;
}

// Focus states
input,
textarea {
  @transition: border linear .2s, box-shadow linear .2s;
  .transition(@transition);
  .box-shadow(inset 0 1px 3px rgba(0,0,0,.1));
}
input:focus,
textarea:focus {
  outline: 0;
  border-color: rgba(82,168,236,.8);
  @shadow: inset 0 1px 3px rgba(0,0,0,.1), 0 0 8px rgba(82,168,236,.6);
  .box-shadow(@shadow);
}
input[type=file]:focus,
input[type=checkbox]:focus,
select:focus {
  .box-shadow(none); // override for file inputs
  outline: 1px dotted #666; // Selet elements don't get box-shadow styles, so instead we do outline
}

// FORM FIELD FEEDBACK STATES
// --------------------------

// Mixin for form field states
.formFieldState(@textColor: #555, @borderColor: #ccc, @backgroundColor: #f5f5f5) {
  // Set the text color
  > label,
  .help-block,
  .help-inline {
    color: @textColor;
  }
  // Style inputs accordingly
  input,
  textarea {
    color: @textColor;
    border-color: @borderColor;
    &:focus {
      border-color: darken(@borderColor, 10%);
      .box-shadow(0 0 6px lighten(@borderColor, 20%);
    }
  }
  // Give a small background color for input-prepend/-append
  .input-prepend .add-on,
  .input-append .add-on {
    color: @textColor;
    background-color: @backgroundColor;
    border-color: @textColor;
  }
}
// Error
form .clearfix.error {
  .formFieldState(#b94a48, #ee5f5b, lighten(#ee5f5b, 30%));
}
// Warning
form .clearfix.warning {
  .formFieldState(#c09853, #ccae64, lighten(#CCAE64, 5%));
}
// Success
form .clearfix.success {
  .formFieldState(#468847, #57a957, lighten(#57a957, 30%));
}

// Form element sizes
// TODO v2: remove duplication here and just stick to .input-[size] in light of adding .spanN sizes
.input-mini,
input.mini,
textarea.mini,
select.mini {
  width: 60px;
}
.input-small,
input.small,
textarea.small,
select.small {
  width: 90px;
}
.input-medium,
input.medium,
textarea.medium,
select.medium {
  width: 150px;
}
.input-large,
input.large,
textarea.large,
select.large {
  width: 210px;
}
.input-xlarge,
input.xlarge,
textarea.xlarge,
select.xlarge {
  width: 270px;
}
.input-xxlarge,
input.xxlarge,
textarea.xxlarge,
select.xxlarge {
  width: 530px;
}
textarea.xxlarge {
  overflow-y: auto;
}

// Grid style input sizes
// This is a duplication of the main grid .columns() mixin, but subtracts 10px to account for input padding and border
.formColumns(@columnSpan: 1) {
  display: inline-block;
  float: none;
  width: ((@gridColumnWidth) * @columnSpan) + (@gridGutterWidth * (@columnSpan - 1)) - 10;
  margin-left: 0;
}
input,
textarea {
  // Default columns
  &.span1     { .formColumns(1); }
  &.span2     { .formColumns(2); }
  &.span3     { .formColumns(3); }
  &.span4     { .formColumns(4); }
  &.span5     { .formColumns(5); }
  &.span6     { .formColumns(6); }
  &.span7     { .formColumns(7); }
  &.span8     { .formColumns(8); }
  &.span9     { .formColumns(9); }
  &.span10    { .formColumns(10); }
  &.span11    { .formColumns(11); }
  &.span12    { .formColumns(12); }
  &.span13    { .formColumns(13); }
  &.span14    { .formColumns(14); }
  &.span15    { .formColumns(15); }
  &.span16    { .formColumns(16); }
}

// Disabled and read-only inputs
input[disabled],
select[disabled],
textarea[disabled],
input[readonly],
select[readonly],
textarea[readonly] {
  background-color: #f5f5f5;
  border-color: #ddd;
  cursor: not-allowed;
}

// Actions (the buttons)
.actions {
  background: #f5f5f5;
  margin-top: @baseline;
  margin-bottom: @baseline;
  padding: (@baseline - 1) 20px @baseline 150px;
  border-top: 1px solid #ddd;
  .border-radius(0 0 3px 3px);
  .secondary-action {
    float: right;
    a {
      line-height: 30px;
      &:hover {
        text-decoration: underline;
      }
    }
  }
}

// Help Text
// TODO: Do we need to set basefont and baseline here?
.help-inline,
.help-block {
  font-size: @basefont;
  line-height: @baseline;
  color: @grayLight;
}
.help-inline {
  padding-left: 5px;
  *position: relative; /* IE6-7 */
  *top: -5px; /* IE6-7 */
}

// Big blocks of help text
.help-block {
  display: block;
  max-width: 600px;
}

// Inline Fields (input fields that appear as inline objects
.inline-inputs {
  color: @gray;
  span {
    padding: 0 2px 0 1px;
  }
}

// Allow us to put symbols and text within the input field for a cleaner look
.input-prepend,
.input-append {
  input {
    .border-radius(0 3px 3px 0);
  }
  .add-on {
    position: relative;
    background: #f5f5f5;
    border: 1px solid #ccc;
    z-index: 2;
    float: left;
    display: block;
    width: auto;
    min-width: 16px;
    height: 18px;
    padding: 4px 4px 4px 5px;
    margin-right: -1px;
    font-weight: normal;
    line-height: 18px;
    color: @grayLight;
    text-align: center;
    text-shadow: 0 1px 0 @white;
    .border-radius(3px 0 0 3px);
  }
  .active {
    background: lighten(@green, 30);
    border-color: @green;
  }
}
.input-prepend {
  .add-on {
    *margin-top: 1px; /* IE6-7 */
  }
}
.input-append {
  input {
    float: left;
    .border-radius(3px 0 0 3px);
  }
  .add-on {
    .border-radius(0 3px 3px 0);
    margin-right: 0;
    margin-left: -1px;
  }
}

// Stacked options for forms (radio buttons or checkboxes)
.inputs-list {
  margin: 0 0 5px;
  width: 100%;
  li {
    display: block;
    padding: 0;
    width: 100%;
  }
  label {
    display: block;
    float: none;
    width: auto;
    padding: 0;
    margin-left: 20px;
    line-height: @baseline;
    text-align: left;
    white-space: normal;
    strong {
      color: @gray;
    }
    small {
      font-size: @basefont - 2;
      font-weight: normal;
    }
  }
  .inputs-list {
    margin-left: 25px;
    margin-bottom: 10px;
    padding-top: 0;
  }
  &:first-child {
    padding-top: 6px;
  }
  li + li {
    padding-top: 2px;
  }
  input[type=radio],
  input[type=checkbox] {
    margin-bottom: 0;
    margin-left: -20px;
    float: left;
  }
}

// Stacked forms
.form-stacked {
  padding-left: 20px;
  fieldset {
    padding-top: @baseline / 2;
  }
  legend {
    padding-left: 0;
  }
  label {
    display: block;
    float: none;
    width: auto;
    font-weight: bold;
    text-align: left;
    line-height: 20px;
    padding-top: 0;
  }
  .clearfix {
    margin-bottom: @baseline / 2;
    div.input {
      margin-left: 0;
    }
  }
  .inputs-list {
    margin-bottom: 0;
    li {
      padding-top: 0;
      label {
        font-weight: normal;
        padding-top: 0;
      }
    }
  }
  div.clearfix.error {
    padding-top: 10px;
    padding-bottom: 10px;
    padding-left: 10px;
    margin-top: 0;
    margin-left: -10px;
  }
  .actions {
    margin-left: -20px;
    padding-left: 20px;
  }
}


* Enable or disable key bindings. Clients such as dialogs may wish to disable

* @param {string} A key-description string.

* @return {boolean} true if the key was processed, false otherwise

/*
 * This file only exists because jsTree insists on loading a CSS theme file itself.
 *
 * See jsTreeTheme.less for our actual theme for the jsTree control.
 */


/* Styles for jsTree control */
/* (these are based on jsTree's default theme .css file, so they are not very LESS-like) */

.jstree-brackets li,
.jstree-brackets ins { background-image:url("styles/images/jsTreeSprites.png"); background-repeat:no-repeat; background-color:transparent; }
.jstree-brackets li { background-position:-90px 0; background-repeat:repeat-y; }
.jstree-brackets li.jstree-last { background:transparent; }
.jstree-brackets .jstree-open > ins { background-position:-72px 0; }
.jstree-brackets .jstree-closed > ins { background-position:-54px 0; }
.jstree-brackets .jstree-leaf > ins { background-position:-36px 0; }

.jstree-brackets .jstree-hovered {
    background: #e7f4f9;
}
.jstree-brackets .jstree-clicked {
    color: #fff;
    #gradient > .vertical(#5fa3e0, #0065be);
}

/* Make the links in the JS tree the width of the container
 * by shifting the off the screen by negative margin and moving the
 * content back by pushing the padding. The icons are positioned absolute
 * relative to the containing list item so the sit above the a's background.
 * This also means we need to include the size of the sprite in the padding
 * so the text ends up back in the same spot visually
 */
.jstree-brackets li > a {
    padding-left: @spriteSize18 + 10000;
    margin-left: -10000px;
    width: 100%;
}

.jstree li {
    position: relative;
}

.jstree ins {
    position: absolute;
}

.jstree-brackets a .jstree-icon { background-position:-56px -19px; }
.jstree-brackets a.jstree-loading .jstree-icon { background:url("styles/images/throbber.gif") center center no-repeat !important; }

.jstree-brackets li > a {
    padding-top: 2px;
    padding-bottom: 2px;
}
.jstree-brackets li > ins {
    margin-top: 2px;
    margin-bottom: 2px;
}

.jstree-brackets .jstree-no-dots li,
.jstree-brackets .jstree-no-dots .jstree-leaf > ins { background:transparent; }
.jstree-brackets .jstree-no-dots .jstree-open > ins { background-position:-18px 0; }
.jstree-brackets .jstree-no-dots .jstree-closed > ins { background-position:0 0; }

.jstree-brackets .jstree-no-icons a .jstree-icon { display:none; }

.jstree-brackets .jstree-search { font-style:italic; }

.jstree-brackets .jstree-no-icons .jstree-checkbox { display:inline-block; }
.jstree-brackets .jstree-no-checkboxes .jstree-checkbox { display:none !important; }
.jstree-brackets .jstree-checked > a > .jstree-checkbox { background-position:-38px -19px; }
.jstree-brackets .jstree-unchecked > a > .jstree-checkbox { background-position:-2px -19px; }
.jstree-brackets .jstree-undetermined > a > .jstree-checkbox { background-position:-20px -19px; }
.jstree-brackets .jstree-checked > a > .jstree-checkbox:hover { background-position:-38px -37px; }
.jstree-brackets .jstree-unchecked > a > .jstree-checkbox:hover { background-position:-2px -37px; }
.jstree-brackets .jstree-undetermined > a > .jstree-checkbox:hover { background-position:-20px -37px; }

#vakata-dragged.jstree-brackets ins { background:transparent !important; }
#vakata-dragged.jstree-brackets .jstree-ok { background:url("styles/images/jsTreeSprites.png") -2px -53px no-repeat !important; }
#vakata-dragged.jstree-brackets .jstree-invalid { background:url("styles/images/jsTreeSprites.png") -18px -53px no-repeat !important; }
#jstree-marker.jstree-brackets { background:url("styles/images/jsTreeSprites.png") -41px -57px no-repeat !important; text-indent:-100px; }

.jstree-brackets a.jstree-search { color:aqua; }
.jstree-brackets .jstree-locked a { color:silver; cursor:default; }

.jstree-brackets .jstree-rename-input { color: black; }

#vakata-contextmenu.jstree-brackets-context,
#vakata-contextmenu.jstree-brackets-context li ul { background:#f0f0f0; border:1px solid #979797; -moz-box-shadow: 1px 1px 2px #999; -webkit-box-shadow: 1px 1px 2px #999; box-shadow: 1px 1px 2px #999; }
#vakata-contextmenu.jstree-brackets-context li { }
#vakata-contextmenu.jstree-brackets-context a { color:black; }
#vakata-contextmenu.jstree-brackets-context a:hover,
#vakata-contextmenu.jstree-brackets-context .vakata-hover > a { padding:0 5px; background:#e8eff7; border:1px solid #aecff7; color:black; -moz-border-radius:2px; -webkit-border-radius:2px; border-radius:2px; }
#vakata-contextmenu.jstree-brackets-context li.jstree-contextmenu-disabled a,
#vakata-contextmenu.jstree-brackets-context li.jstree-contextmenu-disabled a:hover { color:silver; background:transparent; border:0; padding:1px 4px; }
#vakata-contextmenu.jstree-brackets-context li.vakata-separator { background:white; border-top:1px solid #e0e0e0; margin:0; }
#vakata-contextmenu.jstree-brackets-context li ul { margin-left:-4px; }


/*
 * Copyright 2011 Adobe Systems Incorporated. All Rights Reserved.
 */

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

/**
 * Manages the mapping of keyboard inputs to commands.
 */
define(function (require, exports, module) {
    'use strict';

    var CommandManager = require("CommandManager");

    /**
     * The currently installed keymap.
     */
    var _keymap = null;

    /**
     * Install the specified keymap as the current keymap, overwriting the existing keymap.
     *
     * @param {KeyMap} keymap The keymap to install.
     */
    function installKeymap(keymap) {
        _keymap = keymap;
    }

    /**
     * Process the keybinding for the current key.
     *
     * @param {string} A key-description string.
     * @return {boolean} true if the key was processed, false otherwise
     */
    function handleKey(key) {
        if (_keymap && _keymap.map[key]) {
            CommandManager.execute(_keymap.map[key]);
            return true;
        }
        return false;
    }

    // Define public API
    exports.installKeymap = installKeymap;
    exports.handleKey = handleKey;
});


function Song() {
}

Song.prototype.persistFavoriteStatus = function(value) {
  // something complicated
  throw new Error("not yet implemented");
};


Overview
========

This repository is for the core Brackets editor, written in HTML/CSS/JS. The
desktop application shell, which adds native menu and local file access
functionality, lives in a [separate repo](http://github.com/adobe/brackets-app).

Getting started
===============

In addition to pulling the source from github, you'll need to also grab
submodules referenced by Brackets. To do so, first make sure you have SSH
access to github (since the submodule is referenced via a git: URL rather than
https). Then run the following command in the root of your Brackets repo:

	git submodule update --init --recursive

See [Pro Git section 6.6](http://progit.org/book/ch6-6.html) for some caveats
when working with submodules.

To test if everything's working, load index.html into Safari (Chrome won't work,
see below). You should see a message with a blue background, and an editor area
below that with a line of code in it.

Known Issues
============

* Loading index.html directly into Chrome from the local filesystem will not work
  (the LESS processing will fail) due to Chrome security restrictions. You can run
  it within the shell app, or load it into a different browser, like Safari.


/*
 * jsTree default-rtl theme 1.0
 * Supported features: dots/no-dots, icons/no-icons, focused, loading
 * Supported plugins: ui (hovered, clicked), checkbox, contextmenu, search
 */

.jstree-default-rtl li,
.jstree-default-rtl ins { background-image:url("d.png"); background-repeat:no-repeat; background-color:transparent; }
.jstree-default-rtl li { background-position:-90px 0; background-repeat:repeat-y; }
.jstree-default-rtl li.jstree-last { background:transparent; }
.jstree-default-rtl .jstree-open > ins { background-position:-72px 0; }
.jstree-default-rtl .jstree-closed > ins { background-position:-54px 0; }
.jstree-default-rtl .jstree-leaf > ins { background-position:-36px 0; }

.jstree-default-rtl .jstree-hovered { background:#e7f4f9; border:1px solid #d8f0fa; padding:0 2px 0 1px; }
.jstree-default-rtl .jstree-clicked { background:#beebff; border:1px solid #99defd; padding:0 2px 0 1px; }
.jstree-default-rtl a .jstree-icon { background-position:-56px -19px; }
.jstree-default-rtl a.jstree-loading .jstree-icon { background:url("throbber.gif") center center no-repeat !important; }

.jstree-default-rtl.jstree-focused { background:#ffffee; }

.jstree-default-rtl .jstree-no-dots li,
.jstree-default-rtl .jstree-no-dots .jstree-leaf > ins { background:transparent; }
.jstree-default-rtl .jstree-no-dots .jstree-open > ins { background-position:-18px 0; }
.jstree-default-rtl .jstree-no-dots .jstree-closed > ins { background-position:0 0; }

.jstree-default-rtl .jstree-no-icons a .jstree-icon { display:none; }

.jstree-default-rtl .jstree-search { font-style:italic; }

.jstree-default-rtl .jstree-no-icons .jstree-checkbox { display:inline-block; }
.jstree-default-rtl .jstree-no-checkboxes .jstree-checkbox { display:none !important; }
.jstree-default-rtl .jstree-checked > a > .jstree-checkbox { background-position:-38px -19px; }
.jstree-default-rtl .jstree-unchecked > a > .jstree-checkbox { background-position:-2px -19px; }
.jstree-default-rtl .jstree-undetermined > a > .jstree-checkbox { background-position:-20px -19px; }
.jstree-default-rtl .jstree-checked > a > .jstree-checkbox:hover { background-position:-38px -37px; }
.jstree-default-rtl .jstree-unchecked > a > .jstree-checkbox:hover { background-position:-2px -37px; }
.jstree-default-rtl .jstree-undetermined > a > .jstree-checkbox:hover { background-position:-20px -37px; }

#vakata-dragged.jstree-default-rtl ins { background:transparent !important; }
#vakata-dragged.jstree-default-rtl .jstree-ok { background:url("d.png") -2px -53px no-repeat !important; }
#vakata-dragged.jstree-default-rtl .jstree-invalid { background:url("d.png") -18px -53px no-repeat !important; }
#jstree-marker.jstree-default-rtl { background:url("d.png") -41px -57px no-repeat !important; text-indent:-100px; }

.jstree-default-rtl a.jstree-search { color:aqua; }
.jstree-default-rtl .jstree-locked a { color:silver; cursor:default; }

#vakata-contextmenu.jstree-default-rtl-context,
#vakata-contextmenu.jstree-default-rtl-context li ul { background:#f0f0f0; border:1px solid #979797; -moz-box-shadow: 1px 1px 2px #999; -webkit-box-shadow: 1px 1px 2px #999; box-shadow: 1px 1px 2px #999; }
#vakata-contextmenu.jstree-default-rtl-context li { }
#vakata-contextmenu.jstree-default-rtl-context a { color:black; }
#vakata-contextmenu.jstree-default-rtl-context a:hover,
#vakata-contextmenu.jstree-default-rtl-context .vakata-hover > a { padding:0 5px; background:#e8eff7; border:1px solid #aecff7; color:black; -moz-border-radius:2px; -webkit-border-radius:2px; border-radius:2px; }
#vakata-contextmenu.jstree-default-rtl-context li.jstree-contextmenu-disabled a,
#vakata-contextmenu.jstree-default-rtl-context li.jstree-contextmenu-disabled a:hover { color:silver; background:transparent; border:0; padding:1px 4px; }
#vakata-contextmenu.jstree-default-rtl-context li.vakata-separator { background:white; border-top:1px solid #e0e0e0; margin:0; }
#vakata-contextmenu.jstree-default-rtl-context li ul { margin-left:-4px; }

/* IE6 BEGIN */
.jstree-default-rtl li,
.jstree-default-rtl ins,
#vakata-dragged.jstree-default-rtl .jstree-invalid,
#vakata-dragged.jstree-default-rtl .jstree-ok,
#jstree-marker.jstree-default-rtl { _background-image:url("d.gif"); }
.jstree-default-rtl .jstree-open ins { _background-position:-72px 0; }
.jstree-default-rtl .jstree-closed ins { _background-position:-54px 0; }
.jstree-default-rtl .jstree-leaf ins { _background-position:-36px 0; }
.jstree-default-rtl a ins.jstree-icon { _background-position:-56px -19px; }
#vakata-contextmenu.jstree-default-rtl-context ins { _display:none; }
#vakata-contextmenu.jstree-default-rtl-context li { _zoom:1; }
.jstree-default-rtl .jstree-undetermined a .jstree-checkbox { _background-position:-18px -19px; }
.jstree-default-rtl .jstree-checked a .jstree-checkbox { _background-position:-36px -19px; }
.jstree-default-rtl .jstree-unchecked a .jstree-checkbox { _background-position:0px -19px; }
/* IE6 END */

/* RTL part */
.jstree-default-rtl .jstree-hovered, .jstree-default-rtl .jstree-clicked { padding:0 1px 0 2px; }
.jstree-default-rtl li { background-image:url("dots.gif"); background-position: 100% 0px; }
.jstree-default-rtl .jstree-checked > a > .jstree-checkbox { background-position:-36px -19px; margin-left:2px; }
.jstree-default-rtl .jstree-unchecked > a > .jstree-checkbox { background-position:0px -19px; margin-left:2px; }
.jstree-default-rtl .jstree-undetermined > a > .jstree-checkbox { background-position:-18px -19px; margin-left:2px; }
.jstree-default-rtl .jstree-checked > a > .jstree-checkbox:hover { background-position:-36px -37px; }
.jstree-default-rtl .jstree-unchecked > a > .jstree-checkbox:hover { background-position:0px -37px; }
.jstree-default-rtl .jstree-undetermined > a > .jstree-checkbox:hover { background-position:-18px -37px; }


case "php3":
case "php4":
case "php5":
case "phtm":
case "phtml":

/*
 * 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, nomen: true, regexp: true, indent: 4, maxerr: 50 */
/*global define, PathUtils, FileError, brackets */

/**
 * Set of utilites for working with the code editor
 */
define(function (require, exports, module) {
    'use strict';

    require("thirdparty/path-utils/path-utils.min");
    require("thirdparty/CodeMirror2/mode/xml/xml");
    require("thirdparty/CodeMirror2/mode/javascript/javascript");
    require("thirdparty/CodeMirror2/mode/css/css");
    require("thirdparty/CodeMirror2/mode/less/less");
    require("thirdparty/CodeMirror2/mode/htmlmixed/htmlmixed");
    require("thirdparty/CodeMirror2/mode/xml/xml");
    require("thirdparty/CodeMirror2/mode/clike/clike");
    require("thirdparty/CodeMirror2/mode/php/php");
    require("thirdparty/CodeMirror2/mode/coffeescript/coffeescript");
    require("thirdparty/CodeMirror2/mode/clojure/clojure");
    require("thirdparty/CodeMirror2/mode/perl/perl");
    require("thirdparty/CodeMirror2/mode/ruby/ruby");
    require("thirdparty/CodeMirror2/mode/mysql/mysql");
    require("thirdparty/CodeMirror2/mode/diff/diff");
    require("thirdparty/CodeMirror2/mode/markdown/markdown");

    /**
     * @private
     * Given a file URL, determines the mode to use based
     * off the file's extension.
     * @param {string} fileUrl  A cannonical file URL to extract the extension from
     */
    function getModeFromFileExtension(fileUrl) {
        var ext = PathUtils.filenameExtension(fileUrl);
        //incase the arg is just the ext
        if (!ext) {
            ext = fileUrl;
        }
        if (ext.charAt(0) === '.') {
            ext = ext.substr(1);
        }

        switch (ext) {

        case "js":
            return "javascript";

        case "json":
            return {name: "javascript", json: true};

        case "css":
            return "css";

        case "less":
            return "less";

        case "html":
        case "htm":
        case "xhtml":
            return "htmlmixed";

        case "xml":
            return "xml";

        case "php":
            return "php";

        case "c":
        case "cc":
        case "c++":
        case "cxx":
        case "cp":
        case "cpp":
        case "h":
        case "hpp":
        case "vert":
        case "frag":
        case "java":
            return "clike";

        case "coffee":
            return "coffeescript";

        case "clj":
            return "clojure";

        case "pl":
            return "perl";

        case "rb":
            return "ruby";

        case "sql":
            return "mysql";

        case "diff":
        case "patch":
            return "diff";

        case "md":
            return "markdown";

        default:
            console.log("Called EditorUtils.js _getModeFromFileExtensions with an unhandled file extension: " + ext);
            return "";
        }
    }

    // Define public API
    exports.getModeFromFileExtension = getModeFromFileExtension;
});


/*
 * 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: false, describe: false, it: false, xit: false, expect: false, beforeEach: false, afterEach: false, waitsFor: false, runs: false, $: false, brackets: false */

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

    var Commands,           // loaded from brackets.test
        EditorManager,      // loaded from brackets.test
        FileSyncManager,    // loaded from brackets.test
        FileIndexManager,   // loaded from brackets.test
        DocumentManager,    // loaded from brackets.test
        FileViewController, // loaded from brackets.test
        Dialogs          = require("widgets/Dialogs"),
        NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem,
        FileUtils        = require("file/FileUtils"),
        SpecRunnerUtils  = require("./SpecRunnerUtils.js");

    describe("InlineEditorProviders", function () {

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

        function toRange(startLine, endLine) {
            return {startLine: startLine, endLine: endLine};
        }

        function rewriteProject(spec) {
            var result = new $.Deferred();

            FileIndexManager.getFileInfoList("all").done(function (allFiles) {
                // convert fileInfos to fullPaths
                allFiles = allFiles.map(function (fileInfo) {
                    return fileInfo.fullPath;
                });

                // parse offsets and save
                SpecRunnerUtils.saveFilesWithoutOffsets(allFiles).done(function (offsetInfos) {
                    spec.infos = offsetInfos;

                    // install after function to restore file content
                    spec.after(function () {
                        var done = false;

                        runs(function () {
                            SpecRunnerUtils.saveFilesWithOffsets(spec.infos).done(function () {
                                done = true;
                            });
                        });

                        waitsFor(function () { return done; }, "saveFilesWithOffsets timeout", 1000);
                    });

                    result.resolve();
                }).fail(function () {
                    result.reject();
                });
            });

            return result.promise();
        }

        /**
         * Performs setup for an inline editor test. Parses offsets (saved to Spec.offsets) for all files in
         * the test project (testPath) and saves files back to disk without offset markup.
         * When finished, open an editor for the specified project relative file path
         * then attempts opens an inline editor at the given offset. Installs an after()
         * function restore all file content back to original state with offset markup.
         *
         * @param {!string} openFile Project relative file path to open in a main editor.
         * @param {!number} openOffset The offset index location within openFile to open an inline editor.
         * @param {?boolean} expectInline Use false to verify that an inline editor should not be opened. Omit otherwise.
         */
        var _initInlineTest = function (openFile, openOffset, expectInline, workingSet) {
            var allFiles,
                hostOpened = false,
                err = false,
                inlineOpened = null,
                spec = this,
                rewriteDone = false,
                rewriteErr = false;

            workingSet = workingSet || [];

            expectInline = (expectInline !== undefined) ? expectInline : true;

            SpecRunnerUtils.loadProjectInTestWindow(testPath);

            // load project to set CSSUtils scope
            runs(function () {
                rewriteProject(spec)
                    .done(function () { rewriteDone = true; })
                    .fail(function () { rewriteErr = true; });
            });

            waitsFor(function () { return rewriteDone && !rewriteErr; }, "rewriteProject timeout", 1000);

            runs(function () {
                workingSet.push(openFile);
                SpecRunnerUtils.openProjectFiles(workingSet).done(function (documents) {
                    hostOpened = true;
                }).fail(function () {
                    err = true;
                });
            });

            waitsFor(function () { return hostOpened && !err; }, "FILE_OPEN timeout", 1000);

            runs(function () {
                var editor = EditorManager.getCurrentFullEditor();

                // open inline editor at specified offset index
                var inlineEditorResult = SpecRunnerUtils.openInlineEditorAtOffset(
                    editor,
                    spec.infos[openFile].offsets[openOffset]
                );

                inlineEditorResult.done(function () {
                    inlineOpened = true;
                }).fail(function () {
                    inlineOpened = false;
                });
            });

            waitsFor(function () {
                return (inlineOpened !== null) && (inlineOpened === expectInline);
            }, "inline editor timeout", 1000);
        };

        /*
         * Note that the bulk of selector matching tests are in CSSutils-test.js.
         * These tests are primarily focused on the InlineEditorProvider module.
         */
        describe("htmlToCSSProvider", function () {

            beforeEach(function () {
                initInlineTest = _initInlineTest.bind(this);
                SpecRunnerUtils.createTestWindowAndRun(this, function (w) {
                    testWindow          = w;
                    Commands            = testWindow.brackets.test.Commands;
                    EditorManager       = testWindow.brackets.test.EditorManager;
                    FileSyncManager     = testWindow.brackets.test.FileSyncManager;
                    FileIndexManager    = testWindow.brackets.test.FileIndexManager;
                    DocumentManager     = testWindow.brackets.test.DocumentManager;
                    FileViewController  = testWindow.brackets.test.FileViewController;
                });

                this.addMatchers({

                    toHaveInlineEditorRange: function (range) {
                        var i = 0,
                            editor = this.actual,
                            hidden,
                            lineCount = editor.lineCount(),
                            shouldHide = [],
                            shouldShow = [],
                            startLine = range.startLine,
                            endLine = range.endLine,
                            visibleRangeCheck;

                        for (i = 0; i < lineCount; i++) {
                            hidden = editor._codeMirror.getLineHandle(i).hidden || false;

                            if (i < startLine) {
                                if (!hidden) {
                                    shouldHide.push(i); // lines above start line should be hidden
                                }
                            } else if ((i >= startLine) && (i <= endLine)) {
                                if (hidden) {
                                    shouldShow.push(i); // lines in the range should be visible
                                }
                            } else if (i > endLine) {
                                if (!hidden) {
                                    shouldHide.push(i); // lines below end line should be hidden
                                }
                            }
                        }

                        visibleRangeCheck = (editor._visibleRange.startLine === startLine)
                            && (editor._visibleRange.endLine === endLine);

                        this.message = function () {
                            var msg = "";

                            if (shouldHide.length > 0) {
                                msg += "Expected inline editor to hide [" + shouldHide.toString() + "].\n";
                            }

                            if (shouldShow.length > 0) {
                                msg += "Expected inline editor to show [" + shouldShow.toString() + "].\n";
                            }

                            if (!visibleRangeCheck) {
                                msg += "Editor._visibleRange ["
                                    + editor._visibleRange.startLine + ","
                                    + editor._visibleRange.endLine + "] should be ["
                                    + startLine + "," + endLine + "].";
                            }

                            return msg;
                        };

                        return (shouldHide.length === 0)
                            && (shouldShow.length === 0)
                            && visibleRangeCheck;
                    }
                });
            });

            afterEach(function () {
                //debug visual confirmation of inline editor
                //waits(1000);

                // revert files to original content with offset markup
                SpecRunnerUtils.closeTestWindow();
            });

            it("should open a type selector", function () {
                initInlineTest("test1.html", 0);

                runs(function () {
                    var inlineWidget = EditorManager.getCurrentFullEditor().getInlineWidgets()[0];
                    var inlinePos = inlineWidget.editors[0].getCursorPos();

                    // verify cursor position in inline editor
                    expect(inlinePos).toEqual(this.infos["test1.css"].offsets[0]);
                });
            });

            it("should open a class selector", function () {
                initInlineTest("test1.html", 1);

                runs(function () {
                    var inlineWidget = EditorManager.getCurrentFullEditor().getInlineWidgets()[0];
                    var inlinePos = inlineWidget.editors[0].getCursorPos();

                    // verify cursor position in inline editor
                    expect(inlinePos).toEqual(this.infos["test1.css"].offsets[1]);
                });
            });

            it("should also open a class selector", function () {
                initInlineTest("test1.html", 7);

                runs(function () {
                    var inlineWidget = EditorManager.getCurrentFullEditor().getInlineWidgets()[0];
                    var inlinePos = inlineWidget.editors[0].getCursorPos();

                    // verify cursor position in inline editor
                    expect(inlinePos).toEqual(this.infos["test1.css"].offsets[1]);
                });
            });

            it("should open an id selector", function () {
                initInlineTest("test1.html", 2);

                runs(function () {
                    var inlineWidget = EditorManager.getCurrentFullEditor().getInlineWidgets()[0];
                    var inlinePos = inlineWidget.editors[0].getCursorPos();

                    // verify cursor position in inline editor
                    expect(inlinePos).toEqual(this.infos["test1.css"].offsets[2]);
                });
            });

            it("should close, then remove the inline widget and restore focus", function () {
                initInlineTest("test1.html", 0);

                var hostEditor, inlineWidget, inlinePos, savedPos;

                runs(function () {
                    hostEditor =  EditorManager.getCurrentFullEditor();
                    savedPos = hostEditor.getCursorPos();
                    inlineWidget = hostEditor.getInlineWidgets()[0];
                    inlinePos = inlineWidget.editors[0].getCursorPos();

                    // verify cursor position in inline editor
                    expect(inlinePos).toEqual(this.infos["test1.css"].offsets[0]);

                    // close the editor
                    EditorManager.closeInlineWidget(hostEditor, inlineWidget, true);

                    // verify no inline widgets
                    expect(hostEditor.getInlineWidgets().length).toBe(0);

                    // verify full editor cursor restored
                    expect(savedPos).toEqual(hostEditor.getCursorPos());
                });
            });

            it("should not open an inline editor when positioned on textContent", function () {
                initInlineTest("test1.html", 3, false);

                runs(function () {
                    // verify no inline open
                    expect(EditorManager.getCurrentFullEditor().getInlineWidgets().length).toBe(0);
                });
            });

            it("should not open an inline editor when positioned on a tag in a comment", function () {
                initInlineTest("test1.html", 4, false);

                runs(function () {
                    // verify no inline open
                    expect(EditorManager.getCurrentFullEditor().getInlineWidgets().length).toBe(0);
                });
            });

            it("should increase size based on content", function () {
                initInlineTest("test1.html", 1);

                var inlineEditor, widgetHeight;

                runs(function () {
                    inlineEditor = EditorManager.getCurrentFullEditor().getInlineWidgets()[0].editors[0];
                    widgetHeight = inlineEditor.totalHeight(true);

                    // verify original line count
                    expect(inlineEditor.lineCount()).toBe(12);

                    // change inline editor content
                    var newLines = ".bar {\ncolor: #f00;\n}\n.cat {\ncolor: #f00;\n}";

                    // insert new lines at current cursor position
                    // can't mutate document directly at this point
                    inlineEditor._codeMirror.replaceRange(
                        newLines,
                        inlineEditor.getCursorPos()
                    );

                    // verify widget resizes when contents is changed
                    expect(inlineEditor.lineCount()).toBe(17);
                    expect(inlineEditor.totalHeight(true)).toBeGreaterThan(widgetHeight);
                });
            });

            it("should decrease size based on content", function () {
                initInlineTest("test1.html", 1);

                var inlineEditor, widgetHeight;

                runs(function () {
                    inlineEditor = EditorManager.getCurrentFullEditor().getInlineWidgets()[0].editors[0];
                    widgetHeight = inlineEditor.totalHeight(true);

                    // verify original line count
                    expect(inlineEditor.lineCount()).toBe(12);

                    // replace the entire .foo rule with an empty string
                    // set text on the editor, can't mutate document directly at this point
                    inlineEditor._codeMirror.replaceRange(
                        "",
                        inlineEditor.getCursorPos(),
                        this.infos["test1.css"].offsets[3]
                    );

                    // verify widget resizes when contents is changed
                    expect(inlineEditor.lineCount()).toBe(10);
                    expect(inlineEditor.totalHeight(true)).toBeLessThan(widgetHeight);
                });
            });

            it("should save changes in the inline editor ", function () {
                initInlineTest("test1.html", 1);

                var saved = false,
                    err = false,
                    hostEditor,
                    inlineEditor,
                    newText = "\n/* jasmine was here */",
                    savedText;

                runs(function () {
                    hostEditor = EditorManager.getCurrentFullEditor();
                    inlineEditor = hostEditor.getInlineWidgets()[0].editors[0];

                    // insert text at the inline editor's cursor position
                    // can't mutate document directly at this point
                    inlineEditor._codeMirror.replaceRange(newText, inlineEditor.getCursorPos());
                    newText = inlineEditor.document.getText();

                    // verify isDirty flag
                    expect(inlineEditor.document.isDirty).toBeTruthy();
                    expect(hostEditor.document.isDirty).toBeFalsy();

                    // execute file save command
                    testWindow.executeCommand(Commands.FILE_SAVE).done(function () {
                        saved = true;
                    }).fail(function () {
                        err = true;
                    });
                });

                waitsFor(function () { return saved && !err; }, "save timeout", 1000);

                runs(function () {
                    // read saved file contents
                    FileUtils.readAsText(inlineEditor.document.file).done(function (text) {
                        savedText = text;
                    }).fail(function () {
                        err = true;
                    });
                });

                waitsFor(function () { return savedText !== null; }, "readAsText timeout", 1000);

                runs(function () {
                    expect(savedText).toEqual(newText);

                    // verify isDirty flag
                    expect(inlineEditor.document.isDirty).toBeFalsy();
                    expect(hostEditor.document.isDirty).toBeFalsy();
                });
            });

            it("should not save changes in the host editor", function () {
                initInlineTest("test1.html", 1);

                var saved = false,
                    err = false,
                    hostEditor,
                    inlineEditor,
                    newInlineText = "/* jasmine was inline */\n",
                    newHostText = "/* jasmine was here */\n",
                    savedInlineText,
                    savedHostText;

                runs(function () {
                    hostEditor = EditorManager.getCurrentFullEditor();
                    inlineEditor = hostEditor.getInlineWidgets()[0].editors[0];

                    // insert text at the host editor's cursor position
                    hostEditor._codeMirror.replaceRange(newHostText, hostEditor.getCursorPos());
                    newHostText = hostEditor.document.getText();

                    // insert text at the inline editor's cursor position
                    // can't mutate document directly at this point
                    inlineEditor._codeMirror.replaceRange(newInlineText, inlineEditor.getCursorPos());
                    newInlineText = inlineEditor.document.getText();

                    // verify isDirty flag
                    expect(inlineEditor.document.isDirty).toBeTruthy();
                    expect(hostEditor.document.isDirty).toBeTruthy();

                    // execute file save command
                    testWindow.executeCommand(Commands.FILE_SAVE).done(function () {
                        saved = true;
                    }).fail(function () {
                        err = true;
                    });
                });

                waitsFor(function () { return saved && !err; }, "save timeout", 1000);

                runs(function () {
                    // read saved inline file contents
                    FileUtils.readAsText(inlineEditor.document.file).done(function (text) {
                        savedInlineText = text;
                    }).fail(function () {
                        err = true;
                    });

                    // read saved host editor file contents
                    FileUtils.readAsText(hostEditor.document.file).done(function (text) {
                        savedHostText = text;
                    }).fail(function () {
                        err = true;
                    });
                });

                waitsFor(function () { return savedInlineText !== null && savedHostText !== null; }, "readAsText timeout", 1000);

                runs(function () {
                    expect(savedInlineText).toEqual(newInlineText);
                    expect(savedHostText).toEqual(this.infos["test1.html"].text);

                    // verify isDirty flag
                    expect(inlineEditor.document.isDirty).toBeFalsy();
                    expect(hostEditor.document.isDirty).toBeTruthy();
                });
            });

            describe("Inline Editor syncing from disk", function () {

                it("should close inline editor when file deleted on disk", function () {
                    // Create an expendable CSS file
                    var fileToWrite = new NativeFileSystem.FileEntry(testPath + "/tempCSS.css");
                    var savedTempCSSFile = false;
                    runs(function () {
                        FileUtils.writeText(fileToWrite, "#anotherDiv {}")
                            .done(function () {
                                savedTempCSSFile = true;
                            });
                    });
                    waitsFor(function () { return savedTempCSSFile; }, "writeText timeout", 1000);

                    // Open inline editor for that file
                    runs(function () {
                        initInlineTest("test1.html", 6, true);
                    });
                    // initInlineTest() inserts a waitsFor() automatically, so must end runs() block here

                    // Delete the file
                    var fileDeleted = false;
                    runs(function () {
                        var hostEditor = EditorManager.getCurrentFullEditor();
                        expect(hostEditor.getInlineWidgets().length).toBe(1);

                        brackets.fs.unlink(fileToWrite.fullPath, function (err) {
                            if (!err) {
                                fileDeleted = true;
                            }
                        });
                    });
                    waitsFor(function () { return fileDeleted; }, 1000);

                    // Ping FileSyncManager to recognize the deletion
                    runs(function () {
                        FileSyncManager.syncOpenDocuments();
                    });

                    // Verify inline is now closed
                    waitsFor(function () {
                        var hostEditor = EditorManager.getCurrentFullEditor();
                        return (hostEditor.getInlineWidgets().length === 0);
                    }, 1000);

                    // Cleanup: initInlineTest() saves contents of all files in the project and rewrites
                    // them back to disk at the end (to restore any stripped offset markers). But in this
                    // case we really want the temp file to stay gone.
                    runs(function () {
                        delete this.infos["tempCSS.css"];
                    });
                });

                // FUTURE: Eventually we'll instead want it to stay open and revert to the content on disk.
                // Editor's syncing code can't yet handle blowing away the whole Document like that, though.
                it("should close inline when file is closed without saving changes", function () {
                    initInlineTest("test1.html", 1);

                    var newText = "\n/* jasmine was here */",
                        hostEditor,
                        inlineEditor;

                    runs(function () {
                        hostEditor = EditorManager.getCurrentFullEditor();
                        inlineEditor = hostEditor.getInlineWidgets()[0].editors[0];

                        // verify inline is open
                        expect(hostEditor.getInlineWidgets().length).toBe(1);

                        // insert text at the inline editor's cursor position
                        inlineEditor._codeMirror.replaceRange(newText, inlineEditor.getCursorPos());

                        // verify isDirty flag
                        expect(inlineEditor.document.isDirty).toBe(true);

                        // close the main editor / working set entry for the inline's file
                        testWindow.executeCommand(Commands.FILE_CLOSE, {file: inlineEditor.document.file});

                        SpecRunnerUtils.clickDialogButton(Dialogs.DIALOG_BTN_DONTSAVE);
                    });
                    // clickDialogButton() inserts a wait automatically, so must end runs() block here

                    runs(function () {
                        // verify inline is closed
                        expect(hostEditor.getInlineWidgets().length).toBe(0);
                    });
                });
            });

            describe("Bi-directional Editor Synchronizing", function () {

                it("should not add an inline document to the working set without being edited", function () {
                    initInlineTest("test1.html", 0);

                    runs(function () {
                        var i = DocumentManager.findInWorkingSet(this.infos["test1.css"].fileEntry.fullPath);
                        expect(i).toEqual(-1);
                    });
                });

                it("should add dirty documents to the working set", function () {
                    initInlineTest("test1.html", 1);

                    var inlineEditor, widgetHeight;

                    runs(function () {
                        inlineEditor = EditorManager.getCurrentFullEditor().getInlineWidgets()[0].editors[0];
                        widgetHeight = inlineEditor.totalHeight(true);

                        // change inline editor content
                        var newLines = ".bar {\ncolor: #f00;\n}\n.cat {\ncolor: #f00;\n}";

                        // insert new lines at current cursor position
                        inlineEditor._codeMirror.replaceRange(
                            newLines,
                            inlineEditor.getCursorPos()
                        );

                        var i = DocumentManager.findInWorkingSet(this.infos["test1.css"].fileEntry.fullPath);
                        expect(i).toEqual(1);
                    });
                });

                it("should sync edits between full and inline editors", function () {
                    var fullEditor,
                        inlineEditor,
                        newInlineText = "/* jasmine was inline */\n";

                    initInlineTest("test1.html", 1, true, ["test1.css"]);

                    runs(function () {
                        var cssPath = this.infos["test1.css"].fileEntry.fullPath;
                        var cssDoc = DocumentManager.getOpenDocumentForPath(cssPath);

                        // edit the inline editor
                        inlineEditor = EditorManager.getCurrentFullEditor().getInlineWidgets()[0].editors[0];
                        inlineEditor._codeMirror.replaceRange(
                            newInlineText,
                            inlineEditor.getCursorPos()
                        );

                        // activate the full editor
                        DocumentManager.setCurrentDocument(cssDoc);
                        fullEditor = EditorManager.getCurrentFullEditor();

                        // sanity check
                        expect(fullEditor).not.toBe(inlineEditor);
                        expect(fullEditor._codeMirror).not.toBe(inlineEditor._codeMirror);

                        // compare inline editor to full editor
                        expect(inlineEditor._getText()).toBe(fullEditor._getText());

                        // make sure the text was inserted
                        expect(fullEditor._getText().indexOf(newInlineText)).toBeGreaterThan(0);

                        // edit in the full editor and compare
                        fullEditor._codeMirror.replaceRange(
                            newInlineText,
                            inlineEditor.getCursorPos()
                        );
                        expect(inlineEditor._getText()).toBe(fullEditor._getText());
                    });
                });
            });

            describe("Inline Editor range updating", function () {

                var fullEditor,
                    hostEditor,
                    inlineEditor,
                    newInlineText = "/* insert in full editor */\n",
                    before,
                    start,
                    middle,
                    middleOfMiddle,
                    end,
                    endOfEnd,
                    after,
                    wayafter;

                beforeEach(function () {
                    initInlineTest("test1.html", 1, true, ["test1.css"]);

                    runs(function () {
                        var cssPath = this.infos["test1.css"].fileEntry.fullPath;
                        var cssDoc = DocumentManager.getOpenDocumentForPath(cssPath);
                        hostEditor = EditorManager.getCurrentFullEditor();
                        inlineEditor = hostEditor.getInlineWidgets()[0].editors[0];

                        // activate the full editor
                        DocumentManager.setCurrentDocument(cssDoc);
                        fullEditor = EditorManager.getCurrentFullEditor();

                        // alias offsets to nice names
                        before = this.infos["test1.css"].offsets[4];
                        start = this.infos["test1.css"].offsets[1];
                        middle = this.infos["test1.css"].offsets[5];
                        middleOfMiddle = {line: middle.line, ch: 3};
                        end = this.infos["test1.css"].offsets[6];
                        endOfEnd = this.infos["test1.css"].offsets[3];
                        after = this.infos["test1.css"].offsets[7];
                        wayafter = this.infos["test1.css"].offsets[2];
                    });
                });

                it("should insert new line at start of range, and stay open on undo", function () {
                    // insert new line at start of inline range--the new line should be included in
                    // the inline
                    fullEditor._codeMirror.replaceRange(
                        newInlineText,
                        start
                    );
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line + 1));

                    // undo is equivalent to deleting the first line in the range: shouldn't close the editor
                    fullEditor._codeMirror.undo();
                    expect(hostEditor.getInlineWidgets().length).toBe(1);
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line));
                });

                it("should insert new line within first line of range, and stay open on undo", function () {
                    // insert new line in middle of first line of inline range--the new line should
                    // be included
                    fullEditor._codeMirror.replaceRange(
                        newInlineText,
                        {line: start.line, ch: start.ch + 1}
                    );
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line + 1));

                    // Even though the edit didn't start at the beginning of the line, the undo touches
                    // the whole line; but deleting the first line still shouldn't close the editor
                    fullEditor._codeMirror.undo();
                    expect(hostEditor.getInlineWidgets().length).toBe(1);
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line));
                });

                it("should not close inline when undoing changes to first line in range that did not include newlines", function () {
                    // undo is NOT deleting the entire first line in these cases, so the inline should be able to stay open

                    // insert new text at start of inline range, without newlines--the new text should be included in
                    // the inline
                    fullEditor._codeMirror.replaceRange(
                        ".bar, ",
                        start
                    );
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line));
                    fullEditor._codeMirror.undo();
                    expect(hostEditor.getInlineWidgets().length).toBe(1);
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line));

                    // replace text at start of inline range with text without newlines
                    fullEditor._codeMirror.replaceRange(
                        ".bar",
                        {line: start.line, ch: 0},
                        {line: start.line, ch: 4}
                    );
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line));
                    fullEditor._codeMirror.undo();
                    expect(hostEditor.getInlineWidgets().length).toBe(1);
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line));

                    // insert new text in middle of first line in range, without newlines
                    fullEditor._codeMirror.replaceRange(
                        "ABCD",
                        {line: start.line, ch: 3}
                    );
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line));
                    fullEditor._codeMirror.undo();
                    expect(hostEditor.getInlineWidgets().length).toBe(1);
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line));
                });

                it("should sync deletions (and their undos) on last line in range, with or without newlines", function () {
                    // undo is NOT deleting the entire last line in these cases, so the inline should be able to stay open

                    // insert new line in middle of last line of inline range--the new line should
                    // be included
                    fullEditor._codeMirror.replaceRange(
                        newInlineText,
                        {line: end.line, ch: end.ch + 1}
                    );
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line + 1));
                    fullEditor._codeMirror.undo();
                    expect(hostEditor.getInlineWidgets().length).toBe(1);
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line));

                    // insert new line at end of last line of inline range--the new line should
                    // be included
                    fullEditor._codeMirror.replaceRange(
                        newInlineText,
                        endOfEnd
                    );
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line + 1));
                    fullEditor._codeMirror.undo();
                    expect(hostEditor.getInlineWidgets().length).toBe(1);
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line));

                    // replace all text on last line (without replacing trailing newline)
                    fullEditor._codeMirror.replaceRange(
                        "ABCD",
                        end,
                        endOfEnd
                    );
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line));
                    fullEditor._codeMirror.undo();
                    expect(hostEditor.getInlineWidgets().length).toBe(1);
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line));
                });

                it("should sync insertions (and their undos) in most cases without closing", function () {
                    // insert line above inline range
                    fullEditor._codeMirror.replaceRange(
                        newInlineText,
                        before
                    );
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line + 1, end.line + 1));
                    fullEditor._codeMirror.undo();
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line));

                    // insert line within inline range
                    fullEditor._codeMirror.replaceRange(
                        newInlineText,
                        middle
                    );
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line + 1));
                    fullEditor._codeMirror.undo();
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line));

                    // insert line at end of inline range
                    fullEditor._codeMirror.replaceRange(
                        newInlineText,
                        end
                    );
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line + 1));
                    fullEditor._codeMirror.undo();
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line));

                    // insert line below inline range
                    fullEditor._codeMirror.replaceRange(
                        newInlineText,
                        after
                    );
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line));
                    fullEditor._codeMirror.undo();
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line));
                });

                it("should sync deletions from the full editor and update the visual range of the inline editor", function () {
                    // delete line within inline range
                    fullEditor._codeMirror.replaceRange("", middle, end);
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line - 1));
                    fullEditor._codeMirror.undo();
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line));

                    // delete line below inline range
                    fullEditor._codeMirror.replaceRange("", after, wayafter);
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line));
                    fullEditor._codeMirror.undo();
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line));
                });
                it("should close inline when newline at beginning of range is deleted", function () {
                    // delete all of line just above the range
                    fullEditor._codeMirror.replaceRange("", before, start);
                    expect(hostEditor.getInlineWidgets().length).toBe(0);
                });
                it("should stay open when first line entirely deleted", function () {
                    // delete all of first line in range
                    fullEditor._codeMirror.replaceRange("", start, middle);
                    expect(hostEditor.getInlineWidgets().length).toBe(1);
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line - 1));
                });
                it("should stay open when first line and following text deleted", function () {
                    // delete all of first line in range, plus some of the next line
                    fullEditor._codeMirror.replaceRange("", start, middleOfMiddle);
                    expect(hostEditor.getInlineWidgets().length).toBe(1);
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line - 1));
                });
                it("should stay open when first two lines deleted", function () {
                    // delete all of first two lines in range
                    fullEditor._codeMirror.replaceRange("", start, end);
                    expect(hostEditor.getInlineWidgets().length).toBe(1);
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line - 2));
                });
                it("should close inline when last line entirely deleted", function () {
                    // delete all of last line in range
                    fullEditor._codeMirror.replaceRange("", end, after);
                    expect(hostEditor.getInlineWidgets().length).toBe(0);
                });
                it("should close inline when last line and preceding text deleted", function () {
                    // delete all of last line in range, plus some of previous line
                    fullEditor._codeMirror.replaceRange("", middleOfMiddle, after);
                    expect(hostEditor.getInlineWidgets().length).toBe(0);
                });
                it("should close inline when last line and more preceding text deleted", function () {
                    // delete all of last line in range, plus some of previous two lines
                    fullEditor._codeMirror.replaceRange("", {line: start.line, ch: 3}, after);
                    expect(hostEditor.getInlineWidgets().length).toBe(0);
                });
                it("should close inline when deletion spans top of range", function () {
                    // delete a block starting before the inline and ending within it
                    fullEditor._codeMirror.replaceRange("", {line: before.line - 1, ch: 0}, middleOfMiddle);
                    expect(hostEditor.getInlineWidgets().length).toBe(0);
                });
                it("should close inline when deletion spans top of range (barely)", function () {
                    // delete a block starting before the inline and ending within it
                    fullEditor._codeMirror.replaceRange("", before, {line: start.line, ch: 1});
                    expect(hostEditor.getInlineWidgets().length).toBe(0);
                });
                it("should close inline when deletion spans bottom of range", function () {
                    // delete a block starting within the inline and ending after it
                    fullEditor._codeMirror.replaceRange("", middleOfMiddle, wayafter);
                    expect(hostEditor.getInlineWidgets().length).toBe(0);
                });

                it("should sync insertions with multiple change objects in one event", function () {
                    // Insert two new lines: one at start of inline range, the other at the (newly
                    // updated) end of range. The edits are batched into just one event (so the
                    // event's changeList will have length 2); both new lines should be included in
                    // the inline
                    fullEditor._codeMirror.operation(function () {
                        fullEditor._codeMirror.replaceRange(
                            newInlineText,
                            start
                        );
                        // because the edit shifted everything down a line, 'after' is now pointing
                        // at what 'end' used to point at (the "}" line)
                        fullEditor._codeMirror.replaceRange(
                            newInlineText,
                            after
                        );
                    });
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(start.line, end.line + 2));

                    // TODO: can't do our usual undo + re-check range test at the end, becuase of
                    // marijnh/CodeMirror2 bug #487
                });

                it("should sync multiple edits between full and inline editors", function () {
                    var i,
                        editor,
                        text,
                        newInlineText = "/* jasmine was inline */\n",
                        newFullText = "/* full editor edit */\n";

                    // alternate editors while inserting new text
                    for (i = 0; i < 10; i++) {
                        editor = (i % 2 === 0) ? fullEditor : inlineEditor;
                        text = (i % 2 === 0) ? newFullText : newInlineText;
                        editor._codeMirror.replaceRange(
                            text,
                            editor.getCursorPos()
                        );
                        expect(inlineEditor._getText()).toBe(fullEditor._getText());
                    }
                });

                it("should close inline if the contents of the full editor are all deleted", function () {
                    var newInlineText = "/* jasmine was inline */\n";

                    // verify inline is open
                    expect(hostEditor.getInlineWidgets().length).toBe(1);

                    // delete all text via full editor
                    fullEditor._setText("");

                    // verify inline is closed
                    expect(hostEditor.getInlineWidgets().length).toBe(0);
                });

                it("should sync after undoing and redoing an edit", function () {
                    var originalText,
                        editedText,
                        newInlineText = "/* jasmine was inline */\n";

                    originalText = inlineEditor._codeMirror.getValue();

                    // edit the inline editor in the middle of the text so it doesn't
                    // run into one of the collapsing cases
                    inlineEditor._codeMirror.replaceRange(
                        newInlineText,
                        middle
                    );
                    editedText = inlineEditor._codeMirror.getValue();

                    // compare inline editor to full editor
                    expect(inlineEditor._getText()).toBe(fullEditor._getText());

                    // undo the inline editor
                    inlineEditor._codeMirror.undo();
                    expect(inlineEditor._getText()).toBe(fullEditor._getText());
                    expect(inlineEditor._getText()).toBe(originalText);

                    // redo the inline editor
                    inlineEditor._codeMirror.redo();
                    expect(inlineEditor._getText()).toBe(fullEditor._getText());
                    expect(inlineEditor._getText()).toBe(editedText);

                    // undo the full editor
                    fullEditor._codeMirror.undo();
                    expect(inlineEditor._getText()).toBe(fullEditor._getText());
                    expect(fullEditor._getText()).toBe(originalText);

                    // redo the full editor
                    fullEditor._codeMirror.redo();
                    expect(inlineEditor._getText()).toBe(fullEditor._getText());
                    expect(fullEditor._getText()).toBe(editedText);
                });
            });

            describe("Inline Editor range equal to file range", function () {
                var fullEditor,
                    hostEditor,
                    inlineEditor;

                beforeEach(function () {
                    initInlineTest("test1.html", 5, true, ["testOneRuleFile.css"]);

                    runs(function () {
                        var cssPath = this.infos["testOneRuleFile.css"].fileEntry.fullPath;
                        var cssDoc = DocumentManager.getOpenDocumentForPath(cssPath);
                        hostEditor = EditorManager.getCurrentFullEditor();
                        inlineEditor = hostEditor.getInlineWidgets()[0].editors[0];

                        // activate the full editor
                        DocumentManager.setCurrentDocument(cssDoc);
                        fullEditor = EditorManager.getCurrentFullEditor();
                    });
                });

                it("should delete line at bottom and not close on undo", function () {
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(0, 2));

                    // delete all of last line in range
                    fullEditor._codeMirror.replaceRange("", {line: 1, ch: 16}, {line: 2, ch: 1});
                    expect(hostEditor.getInlineWidgets().length).toBe(1);
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(0, 1));
                    fullEditor._codeMirror.undo();
                    expect(hostEditor.getInlineWidgets().length).toBe(1);
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(0, 2));
                });
                it("should insert new line at bottom and not close on undo", function () {
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(0, 2));

                    // delete all of last line in range
                    fullEditor._codeMirror.replaceRange("\n/* New last line */", {line: 2, ch: 1});
                    expect(hostEditor.getInlineWidgets().length).toBe(1);
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(0, 3));
                    fullEditor._codeMirror.undo();
                    expect(hostEditor.getInlineWidgets().length).toBe(1);
                    expect(inlineEditor).toHaveInlineEditorRange(toRange(0, 2));
                });
            });

        });
    });
});


Thumbs.db
src/brackets.css
src/brackets.min.css

# ignore everything in the user extension directory EXCEPT the README
# (so that the directory is non-empty and can be in git)
src/extensions/user/*
!src/extensions/user/README

#OSX .DS_Store files
.DS_Store


/*
 * 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, regexp: true */
/*global define: false, $: false,  describe: false, it: false, expect: false, beforeEach: false, afterEach: false, waitsFor: false, waits: false, runs: false */
define(function (require, exports, module) {
    'use strict';

    var NativeFileSystem    = require("file/NativeFileSystem").NativeFileSystem,
        Commands            = require("command/Commands"),
        FileUtils           = require("file/FileUtils"),
        Async               = require("utils/Async"),
        DocumentManager     = require("document/DocumentManager");

    var TEST_PREFERENCES_KEY    = "com.adobe.brackets.test.preferences",
        OPEN_TAG                = "{{",
        CLOSE_TAG               = "}}",
        RE_MARKER               = /[^\\]?\{\{(\d+)[^\\]?\}\}/g,
        testWindow;

    function getTestRoot() {
        // /path/to/brackets/test/SpecRunner.html
        var path = window.location.pathname;
        path = path.substr(0, path.lastIndexOf("/"));
        path = FileUtils.convertToNativePath(path);
        return path;
    }

    function getTestPath(path) {
        return getTestRoot() + path;
    }

    function getBracketsSourceRoot() {
        var path = window.location.pathname;
        path = path.split("/");
        path = path.slice(0, path.length - 2);
        path.push("src");
        return path.join("/");
    }

    /**
     * Returns a Document suitable for use with an Editor in isolation: i.e., a Document that will
     * never be set as the currentDocument or added to the working set.
     */
    function createMockDocument(initialContent) {
        // Use unique filename to avoid collissions in open documents list
        var dummyFile = new NativeFileSystem.FileEntry("_unitTestDummyFile_.js");

        var docToShim = new DocumentManager.Document(dummyFile, new Date(), initialContent);

        // Prevent adding doc to global 'open docs' list; prevents leaks or collisions if a test
        // fails to clean up properly (if test fails, or due to an apparent bug with afterEach())
        docToShim.addRef = function () {};
        docToShim.releaseRef = function () {};

        // Prevent adding doc to working set
        docToShim._handleEditorChange = function (event, editor, changeList) {
            this.isDirty = true;

            // TODO: This needs to be kept in sync with Document._handleEditorChange(). In the
            // future, we should fix things so that we either don't need mock documents or that this
            // is factored so it will just run in both.
            $(this).triggerHandler("change", [this, changeList]);
        };
        docToShim.notifySaved = function () {
            throw new Error("Cannot notifySaved() a unit-test dummy Document");
        };
        return docToShim;
    }

    function createTestWindowAndRun(spec, callback) {
        runs(function () {
            // Position popup windows in the lower right so they're out of the way
            var testWindowWid = 1000,
                testWindowHt  =  700,
                testWindowX   = window.screen.availWidth - testWindowWid,
                testWindowY   = window.screen.availHeight - testWindowHt,
                optionsStr    = "left=" + testWindowX + ",top=" + testWindowY +
                                ",width=" + testWindowWid + ",height=" + testWindowHt;
            testWindow = window.open(getBracketsSourceRoot() + "/index.html", "_blank", optionsStr);

            testWindow.executeCommand = function executeCommand(cmd, args) {
                return testWindow.brackets.test.CommandManager.execute(cmd, args);
            };
        });

        // FIXME (issue #249): Need an event or something a little more reliable...
        waitsFor(
            function () {
                return testWindow.brackets && testWindow.brackets.test;
            },
            5000
        );

        runs(function () {
            // callback allows specs to query the testWindow before they run
            callback.call(spec, testWindow);
        });
    }

    function closeTestWindow() {
        // debug-only to see testWindow state before closing
        // waits(500);

        runs(function () {
            //we need to mark the documents as not dirty before we close
            //or the window will stay open prompting to save
            var openDocs = testWindow.brackets.test.DocumentManager.getAllOpenDocuments();
            openDocs.forEach(function resetDoc(doc) {
                if (doc.isDirty) {
                    //just refresh it back to it's current text. This will mark it
                    //clean to save
                    doc.refreshText(doc.getText(), doc.diskTimestamp);
                }
            });
            testWindow.close();
        });
    }

    /**
     * Dismiss the currently open dialog as if the user had chosen the given button. Dialogs close
     * asynchronously; after calling this, you need to start a new runs() block before testing the
     * outcome.
     * @param {string} buttonId  One of the Dialogs.DIALOG_BTN_* symbolic constants.
     */
    function clickDialogButton(buttonId) {
        runs(function () {
            // Make sure there's one and only one dialog open
            expect(testWindow.$(".modal.instance").length).toBe(1);

            // Make sure desired button exists
            var dismissButton = testWindow.$(".modal.instance .dialog-button[data-button-id='" + buttonId + "']");
            expect(dismissButton.length).toBe(1);

            dismissButton.click();
        });
        // Wait until dialog's result handler runs; it's done on a timeout to avoid Bootstrap bugs
        // TODO: add unit-test helper API to Dialogs that cleanly tell us when it's done closing
        waits(100);
    }

    function loadProjectInTestWindow(path) {
        var isReady = false;

        runs(function () {
            // begin loading project path
            var result = testWindow.brackets.test.ProjectManager.loadProject(path);
            result.done(function () {
                isReady = true;
            });
        });

        // wait for file system to finish loading
        waitsFor(function () { return isReady; }, "loadProject() timeout", 1000);
    }

    /**
     * Parses offsets from text offset markup (e.g. "{{1}}" for offset 1).
     * @param {!string} text Text to parse
     * @return {!{offsets:!Array.<{line:number, ch:number}>, text:!string, original:!string}}
     */
    function parseOffsetsFromText(text) {
        var offsets = [],
            output  = [],
            i       = 0,
            line    = 0,
            char    = 0,
            ch      = 0,
            length  = text.length,
            exec    = null,
            found   = false;

        while (i < length) {
            found = false;

            if (text.slice(i, i + OPEN_TAG.length) === OPEN_TAG) {
                // find "{{[0-9]+}}"
                RE_MARKER.lastIndex = i;
                exec = RE_MARKER.exec(text);
                found = (exec !== null);

                if (found) {
                    // record offset info
                    offsets[exec[1]] = {line: line, ch: ch};

                    // advance
                    i += exec[0].length;
                }
            }

            if (!found) {
                char = text.substr(i, 1);
                output.push(char);

                if (char === '\n') {
                    line++;
                    ch = 0;
                } else {
                    ch++;
                }

                i++;
            }
        }

        return {offsets: offsets, text: output.join(""), original: text};
    }

    /**
     * Creates absolute paths based on the test window's current project
     * @param {!Array.<string>|string} paths Project relative file path(s) to convert. May pass a single string path or array.
     * @return {!Array.<string>|string} Absolute file path(s)
     */
    function makeAbsolute(paths) {
        var fullPath = testWindow.brackets.test.ProjectManager.getProjectRoot().fullPath;

        function prefixProjectPath(path) {
            if (path.indexOf(fullPath) === 0) {
                return path;
            }

            return fullPath + path;
        }

        if (Array.isArray(paths)) {
            return paths.map(prefixProjectPath);
        } else {
            return prefixProjectPath(paths);
        }
    }

    /**
     * Creates relative paths based on the test window's current project. Any paths,
     * outside the project are included in the result, but left as absolute paths.
     * @param {!Array.<string>|string} paths Absolute file path(s) to convert. May pass a single string path or array.
     * @return {!Array.<string>|string} Relative file path(s)
     */
    function makeRelative(paths) {
        var fullPath = testWindow.brackets.test.ProjectManager.getProjectRoot().fullPath,
            fullPathLength = fullPath.length;

        function removeProjectPath(path) {
            if (path.indexOf(fullPath) === 0) {
                return path.substring(fullPathLength);
            }

            return path;
        }

        if (Array.isArray(paths)) {
            return paths.map(removeProjectPath);
        } else {
            return removeProjectPath(paths);
        }
    }

    function makeArray(arg) {
        if (!Array.isArray(arg)) {
            return [arg];
        }

        return arg;
    }

    /**
     * Parses offsets from a file using offset markup (e.g. "{{1}}" for offset 1).
     * @param {!FileEntry} entry File to open
     * @return {$.Promise} A promise resolved with a record that contains parsed offsets,
     *  the file text without offset markup, the original file content, and the corresponding
     *  file entry.
     */
    function parseOffsetsFromFile(entry) {
        var result = new $.Deferred();

        FileUtils.readAsText(entry).done(function (text) {
            var info = parseOffsetsFromText(text);
            info.fileEntry = entry;

            result.resolve(info);
        }).fail(function (err) {
            result.reject(err);
        });

        return result.promise();
    }

    /**
     * Opens project relative file paths in the test window
     * @param {!(Array.<string>|string)} paths Project relative file path(s) to open
     * @return {!$.Promise} A promise resolved with a mapping of project-relative path
     *  keys to a corresponding Document
     */
    function openProjectFiles(paths) {
        var result = new $.Deferred(),
            fullpaths = makeArray(makeAbsolute(paths)),
            keys = makeArray(makeRelative(paths)),
            docs = {},
            FileViewController = testWindow.brackets.test.FileViewController;

        Async.doSequentially(fullpaths, function (path, i) {
            var one = new $.Deferred();

            FileViewController.addToWorkingSetAndSelect(path).done(function (doc) {
                docs[keys[i]] = doc;
                one.resolve();
            }).fail(function () {
                one.reject();
            });

            return one.promise();
        }, false).done(function () {
            result.resolve(docs);
        }).fail(function () {
            result.reject();
        });

        return result.promise();
    }

    /**
     * Opens a file path, parses offset markup then saves the results to the original file.
     * @param {!string} path Project relative file path to open
     * @return {$.Promise} A promise resolved with the offset information results of parseOffsetsFromFile.
     */
    function saveFileWithoutOffsets(path) {
        var result = new $.Deferred(),
            fileEntry = new NativeFileSystem.FileEntry(path);

        parseOffsetsFromFile(fileEntry).done(function (info) {
            // rewrite file without offset markup
            FileUtils.writeText(fileEntry, info.text).done(function () {
                result.resolve(info);
            }).fail(function () {
                result.reject();
            });
        }).fail(function () {
            result.reject();
        });

        return result.promise();
    }

    /**
     * Opens an array of file paths, parses offset markup then saves the results to each original file.
     * @param {!Array.<string>|string} paths Project relative or absolute file paths to open. May pass a single string path or array.
     * @return {!$.Promise} A promised resolved with a map of offset information indexed by project-relative file path.
     */
    function saveFilesWithoutOffsets(paths) {
        var result = new $.Deferred(),
            infos  = {},
            fullpaths = makeArray(makeAbsolute(paths)),
            keys = makeArray(makeRelative(paths));

        var parallel = Async.doSequentially(fullpaths, function (path, i) {
            var one = new $.Deferred();

            saveFileWithoutOffsets(path).done(function (info) {
                infos[keys[i]] = info;
                one.resolve();
            }).fail(function () {
                one.reject();
            });

            return one.promise();
        }, false);

        parallel.done(function () {
            result.resolve(infos);
        }).fail(function () {
            result.reject();
        });

        return result.promise();
    }

    /**
     * Restore file content with offset markup. When using saveFileWithoutOffsets(),
     * remember to call this function during spec teardown (after()).
     * @param {$.Promise} A promise resolved when all files are re-written to their original content.
     */
    function saveFilesWithOffsets(infos) {
        var arr = [];
        $.each(infos, function (index, value) {
            arr.push(value);
        });

        return Async.doInParallel(arr, function (info) {
            return FileUtils.writeText(info.fileEntry, info.original);
        }, false);
    }

    /**
     * Set editor cursor position to the given offset then activate an inline editor.
     * @param {!Editor} editor
     * @param {!{line:number, ch:number}} offset
     * @return {$.Promise} a promise that will be resolved when an inline
     *  editor is created or rejected when no inline editors are available.
     */
    function openInlineEditorAtOffset(editor, offset) {
        editor.setCursorPos(offset.line, offset.ch);

        // TODO (jasonsj): refactor CMD+E as a Command instead of a CodeMirror key binding?
        return testWindow.brackets.test.EditorManager._openInlineWidget(editor);
    }

    exports.TEST_PREFERENCES_KEY    = TEST_PREFERENCES_KEY;

    exports.getTestRoot                 = getTestRoot;
    exports.getTestPath                 = getTestPath;
    exports.getBracketsSourceRoot       = getBracketsSourceRoot;
    exports.makeAbsolute                = makeAbsolute;
    exports.createMockDocument          = createMockDocument;
    exports.createTestWindowAndRun      = createTestWindowAndRun;
    exports.closeTestWindow             = closeTestWindow;
    exports.clickDialogButton           = clickDialogButton;
    exports.loadProjectInTestWindow     = loadProjectInTestWindow;
    exports.openProjectFiles            = openProjectFiles;
    exports.openInlineEditorAtOffset    = openInlineEditorAtOffset;
    exports.saveFilesWithOffsets        = saveFilesWithOffsets;
    exports.saveFilesWithoutOffsets     = saveFilesWithoutOffsets;
    exports.saveFileWithoutOffsets      = saveFileWithoutOffsets;
});


/*
 * jsTree apple theme 1.0
 * Supported features: dots/no-dots, icons/no-icons, focused, loading
 * Supported plugins: ui (hovered, clicked), checkbox, contextmenu, search
 */

.jstree-apple > ul { background:url("bg.jpg") left top repeat; }
.jstree-apple li,
.jstree-apple ins { background-image:url("d.png"); background-repeat:no-repeat; background-color:transparent; }
.jstree-apple li { background-position:-90px 0; background-repeat:repeat-y;  }
.jstree-apple li.jstree-last { background:transparent; }
.jstree-apple .jstree-open > ins { background-position:-72px 0; }
.jstree-apple .jstree-closed > ins { background-position:-54px 0; }
.jstree-apple .jstree-leaf > ins { background-position:-36px 0; }

.jstree-apple a { border-radius:4px; -moz-border-radius:4px; -webkit-border-radius:4px; text-shadow:1px 1px 1px white; }
.jstree-apple .jstree-hovered { background:#e7f4f9; border:1px solid #d8f0fa; padding:0 3px 0 1px; text-shadow:1px 1px 1px silver; }
.jstree-apple .jstree-clicked { background:#beebff; border:1px solid #99defd; padding:0 3px 0 1px; }
.jstree-apple a .jstree-icon { background-position:-56px -20px; }
.jstree-apple a.jstree-loading .jstree-icon { background:url("throbber.gif") center center no-repeat !important; }

.jstree-apple.jstree-focused { background:white; }

.jstree-apple .jstree-no-dots li,
.jstree-apple .jstree-no-dots .jstree-leaf > ins { background:transparent; }
.jstree-apple .jstree-no-dots .jstree-open > ins { background-position:-18px 0; }
.jstree-apple .jstree-no-dots .jstree-closed > ins { background-position:0 0; }

.jstree-apple .jstree-no-icons a .jstree-icon { display:none; }

.jstree-apple .jstree-search { font-style:italic; }

.jstree-apple .jstree-no-icons .jstree-checkbox { display:inline-block; }
.jstree-apple .jstree-no-checkboxes .jstree-checkbox { display:none !important; }
.jstree-apple .jstree-checked > a > .jstree-checkbox { background-position:-38px -19px; }
.jstree-apple .jstree-unchecked > a > .jstree-checkbox { background-position:-2px -19px; }
.jstree-apple .jstree-undetermined > a > .jstree-checkbox { background-position:-20px -19px; }
.jstree-apple .jstree-checked > a > .checkbox:hover { background-position:-38px -37px; }
.jstree-apple .jstree-unchecked > a > .jstree-checkbox:hover { background-position:-2px -37px; }
.jstree-apple .jstree-undetermined > a > .jstree-checkbox:hover { background-position:-20px -37px; }

#vakata-dragged.jstree-apple ins { background:transparent !important; }
#vakata-dragged.jstree-apple .jstree-ok { background:url("d.png") -2px -53px no-repeat !important; }
#vakata-dragged.jstree-apple .jstree-invalid { background:url("d.png") -18px -53px no-repeat !important; }
#jstree-marker.jstree-apple { background:url("d.png") -41px -57px no-repeat !important; text-indent:-100px; }

.jstree-apple a.jstree-search { color:aqua; }
.jstree-apple .jstree-locked a { color:silver; cursor:default; }

#vakata-contextmenu.jstree-apple-context,
#vakata-contextmenu.jstree-apple-context li ul { background:#f0f0f0; border:1px solid #979797; -moz-box-shadow: 1px 1px 2px #999; -webkit-box-shadow: 1px 1px 2px #999; box-shadow: 1px 1px 2px #999; }
#vakata-contextmenu.jstree-apple-context li { }
#vakata-contextmenu.jstree-apple-context a { color:black; }
#vakata-contextmenu.jstree-apple-context a:hover,
#vakata-contextmenu.jstree-apple-context .vakata-hover > a { padding:0 5px; background:#e8eff7; border:1px solid #aecff7; color:black; -moz-border-radius:2px; -webkit-border-radius:2px; border-radius:2px; }
#vakata-contextmenu.jstree-apple-context li.jstree-contextmenu-disabled a,
#vakata-contextmenu.jstree-apple-context li.jstree-contextmenu-disabled a:hover { color:silver; background:transparent; border:0; padding:1px 4px; }
#vakata-contextmenu.jstree-apple-context li.vakata-separator { background:white; border-top:1px solid #e0e0e0; margin:0; }
#vakata-contextmenu.jstree-apple-context li ul { margin-left:-4px; }

/* TODO: IE6 support - the `>` selectors */