#!/usr/bin/env perl
#
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License (the "License").
# You may not use this file except in compliance with the License.
#
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
# or http://www.opensolaris.org/os/licensing.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information: Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
#
#
# Copyright 2008 Sun Microsystems, Inc.  All rights reserved.
# Use is subject to license terms.
#
# Copyright 2011 Joyent, Inc. All rights reserved.
#
# jsstyle - check for some common stylistic errors.
#
#	jsstyle is a sort of "lint" for Javascript coding style.  This tool is
#	derived from the cstyle tool, used to check for the style used in the
#	Solaris kernel, sometimes known as "Bill Joy Normal Form".
#
#	There's a lot this can't check for, like proper indentation of code
#	blocks.  There's also a lot more this could check for.
#
#	A note to the non perl literate:
#
#		perl regular expressions are pretty much like egrep
#		regular expressions, with the following special symbols
#
#		\s	any space character
#		\S	any non-space character
#		\w	any "word" character [a-zA-Z0-9_]
#		\W	any non-word character
#		\d	a digit [0-9]
#		\D	a non-digit
#		\b	word boundary (between \w and \W)
#		\B	non-word boundary
#

require 5.0;
use IO::File;
use Getopt::Std;
use strict;

my $usage =
"usage: jsstyle [-chvC] [-o constructs] file ...
	-c	check continuation indentation inside functions
	-h	perform heuristic checks that are sometimes wrong
	-v	verbose
	-C	don't check anything in header block comments
	-o constructs
		allow a comma-seperated list of optional constructs:
		    doxygen	allow doxygen-style block comments (/** /*!)
		    splint	allow splint-style lint comments (/*@ ... @*/)
";

my %opts;

if (!getopts("cho:vC", \%opts)) {
	print $usage;
	exit 2;
}

my $check_continuation = $opts{'c'};
my $heuristic = $opts{'h'};
my $verbose = $opts{'v'};
my $ignore_hdr_comment = $opts{'C'};

my $doxygen_comments = 0;
my $splint_comments = 0;

if (defined($opts{'o'})) {
	for my $x (split /,/, $opts{'o'}) {
		if ($x eq "doxygen") {
			$doxygen_comments = 1;
		} elsif ($x eq "splint") {
			$splint_comments = 1;
		} else {
			print "jsstyle: unrecognized construct \"$x\"\n";
			print $usage;
			exit 2;
		}
	}
}

my ($filename, $line, $prev);		# shared globals

my $fmt;
my $hdr_comment_start;

if ($verbose) {
	$fmt = "%s: %d: %s\n%s\n";
} else {
	$fmt = "%s: %d: %s\n";
}

if ($doxygen_comments) {
	# doxygen comments look like "/*!" or "/**"; allow them.
	$hdr_comment_start = qr/^\s*\/\*[\!\*]?$/;
} else {
	$hdr_comment_start = qr/^\s*\/\*$/;
}

# Note, following must be in single quotes so that \s and \w work right.
my $lint_re = qr/\/\*(?:
	jsl:\w+?|ARGSUSED[0-9]*|NOTREACHED|LINTLIBRARY|VARARGS[0-9]*|
	CONSTCOND|CONSTANTCOND|CONSTANTCONDITION|EMPTY|
	FALLTHRU|FALLTHROUGH|LINTED.*?|PRINTFLIKE[0-9]*|
	PROTOLIB[0-9]*|SCANFLIKE[0-9]*|JSSTYLED.*?
    )\*\//x;

my $splint_re = qr/\/\*@.*?@\*\//x;

my $err_stat = 0;		# exit status

if ($#ARGV >= 0) {
	foreach my $arg (@ARGV) {
		my $fh = new IO::File $arg, "r";
		if (!defined($fh)) {
			printf "%s: cannot open\n", $arg;
		} else {
			&jsstyle($arg, $fh);
			close $fh;
		}
	}
} else {
	&jsstyle("<stdin>", *STDIN);
}
exit $err_stat;

my $no_errs = 0;		# set for JSSTYLED-protected lines

sub err($) {
	my ($error) = @_;
	unless ($no_errs) {
		printf $fmt, $filename, $., $error, $line;
		$err_stat = 1;
	}
}

sub err_prefix($$) {
	my ($prevline, $error) = @_;
	my $out = $prevline."\n".$line;
	unless ($no_errs) {
		printf $fmt, $filename, $., $error, $out;
		$err_stat = 1;
	}
}

sub err_prev($) {
	my ($error) = @_;
	unless ($no_errs) {
		printf $fmt, $filename, $. - 1, $error, $prev;
		$err_stat = 1;
	}
}

sub jsstyle($$) {

my ($fn, $filehandle) = @_;
$filename = $fn;			# share it globally

my $in_cpp = 0;
my $next_in_cpp = 0;

my $in_comment = 0;
my $in_header_comment = 0;
my $comment_done = 0;
my $in_function = 0;
my $in_function_header = 0;
my $in_declaration = 0;
my $note_level = 0;
my $nextok = 0;
my $nocheck = 0;

my $in_string = 0;

my ($okmsg, $comment_prefix);

$line = '';
$prev = '';
reset_indent();

line: while (<$filehandle>) {
	s/\r?\n$//;	# strip return and newline

	# save the original line, then remove all text from within
	# double or single quotes, we do not want to check such text.

	$line = $_;

	#
	# C allows strings to be continued with a backslash at the end of
	# the line.  We translate that into a quoted string on the previous
	# line followed by an initial quote on the next line.
	#
	# (we assume that no-one will use backslash-continuation with character
	# constants)
	#
	$_ = '"' . $_		if ($in_string && !$nocheck && !$in_comment);

	#
	# normal strings and characters
	#
	s/'([^\\']|\\.)*'/\'\'/g;
	s/"([^\\"]|\\.)*"/\"\"/g;

	#
	# detect string continuation
	#
	if ($nocheck || $in_comment) {
		$in_string = 0;
	} else {
		#
		# Now that all full strings are replaced with "", we check
		# for unfinished strings continuing onto the next line.
		#
		$in_string =
		    (s/([^"](?:"")*)"([^\\"]|\\.)*\\$/$1""/ ||
		    s/^("")*"([^\\"]|\\.)*\\$/""/);
	}

	#
	# figure out if we are in a cpp directive
	#
	$in_cpp = $next_in_cpp || /^\s*#/;	# continued or started
	$next_in_cpp = $in_cpp && /\\$/;	# only if continued

	# strip off trailing backslashes, which appear in long macros
	s/\s*\\$//;

	# an /* END JSSTYLED */ comment ends a no-check block.
	if ($nocheck) {
		if (/\/\* *END *JSSTYLED *\*\//) {
			$nocheck = 0;
		} else {
			reset_indent();
			next line;
		}
	}

	# a /*JSSTYLED*/ comment indicates that the next line is ok.
	if ($nextok) {
		if ($okmsg) {
			err($okmsg);
		}
		$nextok = 0;
		$okmsg = 0;
		if (/\/\* *JSSTYLED.*\*\//) {
			/^.*\/\* *JSSTYLED *(.*) *\*\/.*$/;
			$okmsg = $1;
			$nextok = 1;
		}
		$no_errs = 1;
	} elsif ($no_errs) {
		$no_errs = 0;
	}

	# check length of line.
	# first, a quick check to see if there is any chance of being too long.
	if (($line =~ tr/\t/\t/) * 7 + length($line) > 80) {
		# yes, there is a chance.
		# replace tabs with spaces and check again.
		my $eline = $line;
		1 while $eline =~
		    s/\t+/' ' x (length($&) * 8 - length($`) % 8)/e;
		if (length($eline) > 80) {
			err("line > 80 characters");
		}
	}

	# ignore NOTE(...) annotations (assumes NOTE is on lines by itself).
	if ($note_level || /\b_?NOTE\s*\(/) { # if in NOTE or this is NOTE
		s/[^()]//g;			  # eliminate all non-parens
		$note_level += s/\(//g - length;  # update paren nest level
		next;
	}

	# a /* BEGIN JSSTYLED */ comment starts a no-check block.
	if (/\/\* *BEGIN *JSSTYLED *\*\//) {
		$nocheck = 1;
	}

	# a /*JSSTYLED*/ comment indicates that the next line is ok.
	if (/\/\* *JSSTYLED.*\*\//) {
		/^.*\/\* *JSSTYLED *(.*) *\*\/.*$/;
		$okmsg = $1;
		$nextok = 1;
	}
	if (/\/\/ *JSSTYLED/) {
		/^.*\/\/ *JSSTYLED *(.*)$/;
		$okmsg = $1;
		$nextok = 1;
	}

	# universal checks; apply to everything
	if (/\t +\t/) {
		err("spaces between tabs");
	}
	if (/ \t+ /) {
		err("tabs between spaces");
	}
	if (/\s$/) {
		err("space or tab at end of line");
	}
	if (/[^ \t(]\/\*/ && !/\w\(\/\*.*\*\/\);/) {
		err("comment preceded by non-blank");
	}

	# is this the beginning or ending of a function?
	# (not if "struct foo\n{\n")
	if (/^{$/ && $prev =~ /\)\s*(const\s*)?(\/\*.*\*\/\s*)?\\?$/) {
		$in_function = 1;
		$in_declaration = 1;
		$in_function_header = 0;
		$prev = $line;
		next line;
	}
	if (/^}\s*(\/\*.*\*\/\s*)*$/) {
		if ($prev =~ /^\s*return\s*;/) {
			err_prev("unneeded return at end of function");
		}
		$in_function = 0;
		reset_indent();		# we don't check between functions
		$prev = $line;
		next line;
	}
	if (/^\w*\($/) {
		$in_function_header = 1;
	}

	# a blank line terminates the declarations within a function.
	# XXX - but still a problem in sub-blocks.
	if ($in_declaration && /^$/) {
		$in_declaration = 0;
	}

	if ($comment_done) {
		$in_comment = 0;
		$in_header_comment = 0;
		$comment_done = 0;
	}
	# does this looks like the start of a block comment?
	if (/$hdr_comment_start/) {
		if (!/^\t*\/\*/) {
			err("block comment not indented by tabs");
		}
		$in_comment = 1;
		/^(\s*)\//;
		$comment_prefix = $1;
		if ($comment_prefix eq "") {
			$in_header_comment = 1;
		}
		$prev = $line;
		next line;
	}
	# are we still in the block comment?
	if ($in_comment) {
		if (/^$comment_prefix \*\/$/) {
			$comment_done = 1;
		} elsif (/\*\//) {
			$comment_done = 1;
			err("improper block comment close")
			    unless ($ignore_hdr_comment && $in_header_comment);
		} elsif (!/^$comment_prefix \*[ \t]/ &&
		    !/^$comment_prefix \*$/) {
			err("improper block comment")
			    unless ($ignore_hdr_comment && $in_header_comment);
		}
	}

	if ($in_header_comment && $ignore_hdr_comment) {
		$prev = $line;
		next line;
	}

	# check for errors that might occur in comments and in code.

	# allow spaces to be used to draw pictures in header comments.
	#if (/[^ ]     / && !/".*     .*"/ && !$in_header_comment) {
	#	err("spaces instead of tabs");
	#}
	#if (/^ / && !/^ \*[ \t\/]/ && !/^ \*$/ &&
	#    (!/^    \w/ || $in_function != 0)) {
	#	err("indent by spaces instead of tabs");
	#}
	if (/^ {2,}/ && !/^    [^ ]/) {
		err("indent by spaces instead of tabs");
	}
	if (/^\t+ [^ \t\*]/ || /^\t+  \S/ || /^\t+   \S/) {
		err("continuation line not indented by 4 spaces");
	}

	if (/^\s*\/\*./ && !/^\s*\/\*.*\*\// && !/$hdr_comment_start/) {
		err("improper first line of block comment");
	}

	if ($in_comment) {	# still in comment, don't do further checks
		$prev = $line;
		next line;
	}

	if ((/[^(]\/\*\S/ || /^\/\*\S/) &&
	    !(/$lint_re/ || ($splint_comments && /$splint_re/))) {
		err("missing blank after open comment");
	}
	if (/\S\*\/[^)]|\S\*\/$/ &&
	    !(/$lint_re/ || ($splint_comments && /$splint_re/))) {
		err("missing blank before close comment");
	}
	if (/\/\/\S/) {		# C++ comments
		err("missing blank after start comment");
	}
	# check for unterminated single line comments, but allow them when
	# they are used to comment out the argument list of a function
	# declaration.
	if (/\S.*\/\*/ && !/\S.*\/\*.*\*\// && !/\(\/\*/) {
		err("unterminated single line comment");
	}

	if (/^(#else|#endif|#include)(.*)$/) {
		$prev = $line;
		next line;
	}

	#
	# delete any comments and check everything else.  Note that
	# ".*?" is a non-greedy match, so that we don't get confused by
	# multiple comments on the same line.
	#
	s/\/\*.*?\*\///g;
	s/\/\/.*$//;		# C++ comments

	# delete any trailing whitespace; we have already checked for that.
	s/\s*$//;

	# following checks do not apply to text in comments.
	if (/"/) {
		err("literal string using double-quote instead of single");
	}

	if (/[^=!<>\s][!<>=]=/ || /[^<>!=][!<>=]==?[^\s,=]/ ||
	    (/[^->]>[^,=>\s]/ && !/[^->]>$/) ||
	    (/[^<]<[^,=<\s]/ && !/[^<]<$/) ||
	    /[^<\s]<[^<]/ || /[^->\s]>[^>]/) {
		err("missing space around relational operator");
	}
	if (/\S>>=/ || /\S<<=/ || />>=\S/ || /<<=\S/ || /\S[-+*\/&|^%]=/ ||
	    (/[^-+*\/&|^%!<>=\s]=[^=]/ && !/[^-+*\/&|^%!<>=\s]=$/) ||
	    (/[^!<>=]=[^=\s]/ && !/[^!<>=]=$/)) {
		# XXX - should only check this for C++ code
		# XXX - there are probably other forms that should be allowed
		if (!/\soperator=/) {
			err("missing space around assignment operator");
		}
	}
	if (/[,;]\S/ && !/\bfor \(;;\)/) {
		err("comma or semicolon followed by non-blank");
	}
	# allow "for" statements to have empty "while" clauses
	if (/\s[,;]/ && !/^[\t]+;$/ && !/^\s*for \([^;]*; ;[^;]*\)/) {
		err("comma or semicolon preceded by blank");
	}
	if (/^\s*(&&|\|\|)/) {
		err("improper boolean continuation");
	}
	if (/\S   *(&&|\|\|)/ || /(&&|\|\|)   *\S/) {
		err("more than one space around boolean operator");
	}
	if (/\b(delete|typeof|instanceOf|throw|with|catch|new|function|in|for|if|while|switch|return|case)\(/) {
		err("missing space between keyword and paren");
	}
	if (/(\b(catch|for|if|with|while|switch|return)\b.*){2,}/) {
		# multiple "case" and "sizeof" allowed
		err("more than one keyword on line");
	}
	if (/\b(delete|typeof|instanceOf|with|throw|catch|new|function|in|for|if|while|switch|return|case)\s\s+\(/ &&
	    !/^#if\s+\(/) {
		err("extra space between keyword and paren");
	}
	# try to detect "func (x)" but not "if (x)" or
	# "#define foo (x)" or "int (*func)();"
	if (/\w\s\(/) {
		my $s = $_;
		# strip off all keywords on the line
		s/\b(delete|typeof|instanceOf|throw|with|catch|new|function|in|for|if|while|switch|return|case)\s\(/XXX(/g;
		s/#elif\s\(/XXX(/g;
		s/^#define\s+\w+\s+\(/XXX(/;
		# do not match things like "void (*f)();"
		# or "typedef void (func_t)();"
		s/\w\s\(+\*/XXX(*/g;
		s/\b(void)\s+\(+/XXX(/og;
		if (/\w\s\(/) {
			err("extra space between function name and left paren");
		}
		$_ = $s;
	}

	if (/^\s*return\W[^;]*;/ && !/^\s*return\s*\(.*\);/) {
		err("unparenthesized return expression");
	}
	if (/\btypeof\b/ && !/\btypeof\s*\(.*\)/) {
		err("unparenthesized typeof expression");
	}
	if (/\(\s/) {
		err("whitespace after left paren");
	}
	# allow "for" statements to have empty "continue" clauses
	if (/\s\)/ && !/^\s*for \([^;]*;[^;]*; \)/) {
		err("whitespace before right paren");
	}
	if (/^\s*\(void\)[^ ]/) {
		err("missing space after (void) cast");
	}
	if (/\S{/ && !/({|\(){/) {
		err("missing space before left brace");
	}
	if ($in_function && /^\s+{/ &&
	    ($prev =~ /\)\s*$/ || $prev =~ /\bstruct\s+\w+$/)) {
		err("left brace starting a line");
	}
	if (/}(else|while)/) {
		err("missing space after right brace");
	}
	if (/}\s\s+(else|while)/) {
		err("extra space after right brace");
	}
	if (/^\s+#/) {
		err("preprocessor statement not in column 1");
	}
	if (/^#\s/) {
		err("blank after preprocessor #");
	}

	#
	# We completely ignore, for purposes of indentation:
	#  * lines outside of functions
	#  * preprocessor lines
	#
	if ($check_continuation && $in_function && !$in_cpp) {
		process_indent($_);
	}

	if ($heuristic) {
		# cannot check this everywhere due to "struct {\n...\n} foo;"
		if ($in_function && !$in_declaration &&
		    /}./ && !/}\s+=/ && !/{.*}[;,]$/ && !/}(\s|)*$/ &&
		    !/} (else|while)/ && !/}}/) {
			err("possible bad text following right brace");
		}
		# cannot check this because sub-blocks in
		# the middle of code are ok
		if ($in_function && /^\s+{/) {
			err("possible left brace starting a line");
		}
	}
	if (/^\s*else\W/) {
		if ($prev =~ /^\s*}$/) {
			err_prefix($prev,
			    "else and right brace should be on same line");
		}
	}
	$prev = $line;
}

if ($prev eq "") {
	err("last line in file is blank");
}

}

#
# Continuation-line checking
#
# The rest of this file contains the code for the continuation checking
# engine.  It's a pretty simple state machine which tracks the expression
# depth (unmatched '('s and '['s).
#
# Keep in mind that the argument to process_indent() has already been heavily
# processed; all comments have been replaced by control-A, and the contents of
# strings and character constants have been elided.
#

my $cont_in;		# currently inside of a continuation
my $cont_off;		# skipping an initializer or definition
my $cont_noerr;		# suppress cascading errors
my $cont_start;		# the line being continued
my $cont_base;		# the base indentation
my $cont_first;		# this is the first line of a statement
my $cont_multiseg;	# this continuation has multiple segments

my $cont_special;	# this is a C statement (if, for, etc.)
my $cont_macro;		# this is a macro
my $cont_case;		# this is a multi-line case

my @cont_paren;		# the stack of unmatched ( and [s we've seen

sub
reset_indent()
{
	$cont_in = 0;
	$cont_off = 0;
}

sub
delabel($)
{
	#
	# replace labels with tabs.  Note that there may be multiple
	# labels on a line.
	#
	local $_ = $_[0];

	while (/^(\t*)( *(?:(?:\w+\s*)|(?:case\b[^:]*)): *)(.*)$/) {
		my ($pre_tabs, $label, $rest) = ($1, $2, $3);
		$_ = $pre_tabs;
		while ($label =~ s/^([^\t]*)(\t+)//) {
			$_ .= "\t" x (length($2) + length($1) / 8);
		}
		$_ .= ("\t" x (length($label) / 8)).$rest;
	}

	return ($_);
}

sub
process_indent($)
{
	require strict;
	local $_ = $_[0];			# preserve the global $_

	s///g;	# No comments
	s/\s+$//;	# Strip trailing whitespace

	return			if (/^$/);	# skip empty lines

	# regexps used below; keywords taking (), macros, and continued cases
	my $special = '(?:(?:\}\s*)?else\s+)?(?:if|for|while|switch)\b';
	my $macro = '[A-Z_][A-Z_0-9]*\(';
	my $case = 'case\b[^:]*$';

	# skip over enumerations, array definitions, initializers, etc.
	if ($cont_off <= 0 && !/^\s*$special/ &&
	    (/(?:(?:\b(?:enum|struct|union)\s*[^\{]*)|(?:\s+=\s*)){/ ||
	    (/^\s*{/ && $prev =~ /=\s*(?:\/\*.*\*\/\s*)*$/))) {
		$cont_in = 0;
		$cont_off = tr/{/{/ - tr/}/}/;
		return;
	}
	if ($cont_off) {
		$cont_off += tr/{/{/ - tr/}/}/;
		return;
	}

	if (!$cont_in) {
		$cont_start = $line;

		if (/^\t* /) {
			err("non-continuation indented 4 spaces");
			$cont_noerr = 1;		# stop reporting
		}
		$_ = delabel($_);	# replace labels with tabs

		# check if the statement is complete
		return		if (/^\s*\}?$/);
		return		if (/^\s*\}?\s*else\s*\{?$/);
		return		if (/^\s*do\s*\{?$/);
		return		if (/{$/);
		return		if (/}[,;]?$/);

		# Allow macros on their own lines
		return		if (/^\s*[A-Z_][A-Z_0-9]*$/);

		# cases we don't deal with, generally non-kosher
		if (/{/) {
			err("stuff after {");
			return;
		}

		# Get the base line, and set up the state machine
		/^(\t*)/;
		$cont_base = $1;
		$cont_in = 1;
		@cont_paren = ();
		$cont_first = 1;
		$cont_multiseg = 0;

		# certain things need special processing
		$cont_special = /^\s*$special/? 1 : 0;
		$cont_macro = /^\s*$macro/? 1 : 0;
		$cont_case = /^\s*$case/? 1 : 0;
	} else {
		$cont_first = 0;

		# Strings may be pulled back to an earlier (half-)tabstop
		unless ($cont_noerr || /^$cont_base    / ||
		    (/^\t*(?:    )?(?:gettext\()?\"/ && !/^$cont_base\t/)) {
			err_prefix($cont_start,
			    "continuation should be indented 4 spaces");
		}
	}

	my $rest = $_;			# keeps the remainder of the line

	#
	# The split matches 0 characters, so that each 'special' character
	# is processed separately.  Parens and brackets are pushed and
	# popped off the @cont_paren stack.  For normal processing, we wait
	# until a ; or { terminates the statement.  "special" processing
	# (if/for/while/switch) is allowed to stop when the stack empties,
	# as is macro processing.  Case statements are terminated with a :
	# and an empty paren stack.
	#
	foreach $_ (split /[^\(\)\[\]\{\}\;\:]*/) {
		next		if (length($_) == 0);

		# rest contains the remainder of the line
		my $rxp = "[^\Q$_\E]*\Q$_\E";
		$rest =~ s/^$rxp//;

		if (/\(/ || /\[/) {
			push @cont_paren, $_;
		} elsif (/\)/ || /\]/) {
			my $cur = $_;
			tr/\)\]/\(\[/;

			my $old = (pop @cont_paren);
			if (!defined($old)) {
				err("unexpected '$cur'");
				$cont_in = 0;
				last;
			} elsif ($old ne $_) {
				err("'$cur' mismatched with '$old'");
				$cont_in = 0;
				last;
			}

			#
			# If the stack is now empty, do special processing
			# for if/for/while/switch and macro statements.
			#
			next		if (@cont_paren != 0);
			if ($cont_special) {
				if ($rest =~ /^\s*{?$/) {
					$cont_in = 0;
					last;
				}
				if ($rest =~ /^\s*;$/) {
					err("empty if/for/while body ".
					    "not on its own line");
					$cont_in = 0;
					last;
				}
				if (!$cont_first && $cont_multiseg == 1) {
					err_prefix($cont_start,
					    "multiple statements continued ".
					    "over multiple lines");
					$cont_multiseg = 2;
				} elsif ($cont_multiseg == 0) {
					$cont_multiseg = 1;
				}
				# We've finished this section, start
				# processing the next.
				goto section_ended;
			}
			if ($cont_macro) {
				if ($rest =~ /^$/) {
					$cont_in = 0;
					last;
				}
			}
		} elsif (/\;/) {
			if ($cont_case) {
				err("unexpected ;");
			} elsif (!$cont_special) {
				err("unexpected ;")	if (@cont_paren != 0);
				if (!$cont_first && $cont_multiseg == 1) {
					err_prefix($cont_start,
					    "multiple statements continued ".
					    "over multiple lines");
					$cont_multiseg = 2;
				} elsif ($cont_multiseg == 0) {
					$cont_multiseg = 1;
				}
				if ($rest =~ /^$/) {
					$cont_in = 0;
					last;
				}
				if ($rest =~ /^\s*special/) {
					err("if/for/while/switch not started ".
					    "on its own line");
				}
				goto section_ended;
			}
		} elsif (/\{/) {
			err("{ while in parens/brackets") if (@cont_paren != 0);
			err("stuff after {")		if ($rest =~ /[^\s}]/);
			$cont_in = 0;
			last;
		} elsif (/\}/) {
			err("} while in parens/brackets") if (@cont_paren != 0);
			if (!$cont_special && $rest !~ /^\s*(while|else)\b/) {
				if ($rest =~ /^$/) {
					err("unexpected }");
				} else {
					err("stuff after }");
				}
				$cont_in = 0;
				last;
			}
		} elsif (/\:/ && $cont_case && @cont_paren == 0) {
			err("stuff after multi-line case") if ($rest !~ /$^/);
			$cont_in = 0;
			last;
		}
		next;
section_ended:
		# End of a statement or if/while/for loop.  Reset
		# cont_special and cont_macro based on the rest of the
		# line.
		$cont_special = ($rest =~ /^\s*$special/)? 1 : 0;
		$cont_macro = ($rest =~ /^\s*$macro/)? 1 : 0;
		$cont_case = 0;
		next;
	}
	$cont_noerr = 0			if (!$cont_in);
}


$('<span>' + " " + token.type.split('->').join(':').trim() + '</span>').appendTo($hintObj).addClass("brackets-js-hints-type-details");

$('<span>' + token.type.split('->').join(':').toString().trim() + '</span>').appendTo($hintObj).addClass("brackets-js-hints-type-details");


$('<span>' + " " + "keyword" + '</span>').appendTo($hintObj).addClass("brackets-js-hints-type-details").addClass("keyword");

keep keep keep keep replace keep keep keep

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

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

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

    var _ = brackets.getModule("thirdparty/lodash");

    var CodeHintManager      = brackets.getModule("editor/CodeHintManager"),
        EditorManager        = brackets.getModule("editor/EditorManager"),
        Commands             = brackets.getModule("command/Commands"),
        CommandManager       = brackets.getModule("command/CommandManager"),
        LanguageManager      = brackets.getModule("language/LanguageManager"),
        AppInit              = brackets.getModule("utils/AppInit"),
        ExtensionUtils       = brackets.getModule("utils/ExtensionUtils"),
        StringMatch          = brackets.getModule("utils/StringMatch"),
        ProjectManager       = brackets.getModule("project/ProjectManager"),
        PreferencesManager   = brackets.getModule("preferences/PreferencesManager"),
        Strings              = brackets.getModule("strings"),
        ParameterHintManager = require("ParameterHintManager"),
        HintUtils            = require("HintUtils"),
        ScopeManager         = require("ScopeManager"),
        Session              = require("Session"),
        Acorn                = require("thirdparty/acorn/acorn");

    var session        = null,  // object that encapsulates the current session state
        cachedCursor   = null,  // last cursor of the current hinting session
        cachedHints    = null,  // sorted hints for the current hinting session
        cachedType     = null,  // describes the lookup type and the object context
        cachedToken    = null,  // the token used in the current hinting session
        matcher        = null,  // string matcher for hints
        jsHintsEnabled = true,  // preference setting to enable/disable the hint session
        noHintsOnDot   = false, // preference setting to prevent hints on dot
        ignoreChange;           // can ignore next "change" event if true;

    // Languages that support inline JavaScript
    var _inlineScriptLanguages = ["html", "php"];

    // Define the detectedExclusions which are files that have been detected to cause Tern to run out of control.
    PreferencesManager.definePreference("jscodehints.detectedExclusions", "array", [], {
        description: Strings.DESCRIPTION_DETECTED_EXCLUSIONS
    });

    // This preference controls when Tern will time out when trying to understand files
    PreferencesManager.definePreference("jscodehints.inferenceTimeout", "number", 30000, {
        description: Strings.DESCRIPTION_INFERENCE_TIMEOUT
    });

    // This preference controls whether to prevent hints from being displayed when dot is typed
    PreferencesManager.definePreference("jscodehints.noHintsOnDot", "boolean", false, {
        description: Strings.DESCRIPTION_NO_HINTS_ON_DOT
    });

    // This preference controls whether to create a session and process all JS files or not.
    PreferencesManager.definePreference("codehint.JSHints", "boolean", true, {
        description: Strings.DESCRIPTION_JS_HINTS
    });

    /**
     * Check whether any of code hints preferences for JS Code Hints is disabled
     * @return {boolean} enabled/disabled
     */
    function _areHintsEnabled() {
        return (PreferencesManager.get("codehint.JSHints") !== false) &&
            (PreferencesManager.get("showCodeHints") !== false);
    }

    PreferencesManager.on("change", "codehint.JSHints", function () {
        jsHintsEnabled = _areHintsEnabled();
    });

    PreferencesManager.on("change", "showCodeHints", function () {
        jsHintsEnabled = _areHintsEnabled();
    });

    PreferencesManager.on("change", "jscodehints.noHintsOnDot", function () {
        noHintsOnDot = !!PreferencesManager.get("jscodehints.noHintsOnDot");
    });

    /**
     * Sets the configuration, generally for testing/debugging use.
     * Configuration keys are merged into the current configuration.
     * The Tern worker is automatically updated to the new config as well.
     *
     * * debug: Set to true if you want verbose logging
     * * noReset: Set to true if you don't want the worker to restart periodically
     *
     * @param {Object} configUpdate keys/values to merge into the config
     */
    function setConfig(configUpdate) {
        var config = setConfig.config;
        Object.keys(configUpdate).forEach(function (key) {
            config[key] = configUpdate[key];
        });

        ScopeManager._setConfig(configUpdate);
    }

    setConfig.config = {};

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

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

        var trimmedQuery,
            formattedHints;

        if (setConfig.config.debug) {
            console.debug("Hints", _.pluck(hints, "label"));
        }

        var _infered = true;

        function getInferHelper(type) {
            return function (element, index, array) {
                if (element === type && _infered) {
                    _infered = true;
                } else {
                    _infered = false;
                }
            };
        }

        function inferArrayTypeClass(typeExpr) {
            var type = "type-array";
            var types = typeExpr.split('[')[1].split(']')[0].split(',');

            _infered = true;

            types.every(getInferHelper('string'));
            if (_infered) {
                type = 'type-string-array';
            } else {
                _infered = true;
                types.every(getInferHelper('number'));
                if (_infered) {
                    type = 'type-num-array';
                } else {
                    _infered = true;
                    types.every(getInferHelper('Object'));
                    if (_infered) {
                        type = 'type-object-array';
                    }
                }
            }
            return type;
        }

        function getRenderTypeClass(type) {
            var typeClass = 'type-undetermined';
            if (type) {
                if (type.indexOf('Object') === 0) {
                    typeClass = 'type-object';
                } else if (type.indexOf('[') === 0) {
                    typeClass = inferArrayTypeClass(type);
                } else if (type.indexOf('fn') === 0) {
                    typeClass = 'type-function';
                } else if (type.indexOf('string') === 0) {
                    typeClass = "type-string";
                } else if (type.indexOf('number') === 0) {
                    typeClass = 'type-number';
                } else if (type.indexOf('bool') === 0) {
                    typeClass = 'type-boolean';
                }
            }
            return typeClass;
        }

        /*
         * Returns a formatted list of hints with the query substring
         * highlighted.
         *
         * @param {Array.<Object>} hints - the list of hints to format
         * @param {string} query - querystring used for highlighting matched
         *      poritions of each hint
         * @return {jQuery.Deferred|{
         *              hints: Array.<string|jQueryObject>,
         *              match: string,
         *              selectInitial: boolean,
         *              handleWideResults: boolean}}
         */
        function formatHints(hints, query) {
            return hints.map(function (token) {
                var $hintObj    = $("").addClass("brackets-js-hints");
                ($hintObj).addClass(getRenderTypeClass(token.type));
                //$('' + getRenderType(token.type) + '').appendTo($hintObj).addClass("brackets-js-hints-type");
                // level indicates either variable scope or property confidence
                if (!type.property && !token.builtin && token.depth !== undefined) {
                    switch (token.depth) {
                    case 0:
                        $hintObj.addClass("priority-high");
                        break;
                    case 1:
                        $hintObj.addClass("priority-medium");
                        break;
                    case 2:
                        $hintObj.addClass("priority-low");
                        break;
                    default:
                        $hintObj.addClass("priority-lowest");
                        break;
                    }
                }

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

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

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

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

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

                function _appendLink() {
                    if (token.url) {
                        $('< >').appendTo($hintObj).addClass("jshint-link").attr('href', token.url).on("click", function (event) {
                            event.stopImmediatePropagation();
                            event.stopPropagation();
                        });
                    }
                }

                if (token.type) {
                    if (token.type.length > 40) {
                        _appendLink();
                        $('' + " " + token.type.split('->').join(':').trim() + '').appendTo($hintObj).addClass("jshint-description");
                    } else {
                        $('' + " " + token.type.split('->').join(':').trim() + '').appendTo($hintObj).addClass("brackets-js-hints-type-details");
                        _appendLink();
                    }
                } else {
                    if (token.keyword) {
                        $('' + " " + "keyword" + '').appendTo($hintObj).addClass("brackets-js-hints-type-details").addClass("keyword");
                    }
                }

                if (token.doc) {
                    $hintObj.attr('title', token.doc);
                    $('' + " " + token.doc.trim() + '').appendTo($hintObj).addClass("jshint-jsdoc");
                }

                $('

').appendTo($hintObj);

                return $hintObj;
            });
        }

        // trim leading and trailing string literal delimiters from the query
        trimmedQuery = _.trim(query, HintUtils.SINGLE_QUOTE + HintUtils.DOUBLE_QUOTE);

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

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

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

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

        return !cachedHints || !cachedCursor || !cachedType ||
            cachedCursor.line !== cursor.line ||
            type.property !== cachedType.property ||
            type.context !== cachedType.context ||
            type.showFunctionType !== cachedType.showFunctionType ||
            (type.functionCallPos && cachedType.functionCallPos &&
            type.functionCallPos.ch !== cachedType.functionCallPos.ch);
    };

    /**
     *  Cache the hints and the hint's context.
     *
     *  @param {Array.<string>} hints - array of hints
     *  @param {{line:number, ch:number}} cursor - the location where the hints
     *  were created.
     * @param {{property: boolean,
                showFunctionType:boolean,
                context: string,
                functionCallPos: {line:number, ch:number}}} type -
     *  type information about the hints
     *  @param {Object} token - CodeMirror token
     */
    function setCachedHintContext(hints, cursor, type, token) {
        cachedHints = hints;
        cachedCursor = cursor;
        cachedType = type;
        cachedToken = token;
    }

    /**
     *  Reset cached hint context.
     */
    function resetCachedHintContext() {
        cachedHints = null;
        cachedCursor = null;
        cachedType = null;
        cachedToken =  null;
    }

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

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

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

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

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

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

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

    /**
     * @return {boolean} - true if the document supports inline JavaScript
     */
    function isInlineScriptSupported(document) {
        var language = LanguageManager.getLanguageForPath(document.file.fullPath).getId();
        return _inlineScriptLanguages.indexOf(language) !== -1;
    }

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

    /**
     *  Create a new StringMatcher instance, if needed.
     *
     * @return {StringMatcher} - a StringMatcher instance.
     */
    function getStringMatcher() {
        if (!matcher) {
            matcher = new StringMatch.StringMatcher({
                preferPrefixMatches: true
            });
        }

        return matcher;
    }

    /**
     *  Check if a hint response is pending.
     *
     * @param {jQuery.Deferred} deferredHints - deferred hint response
     * @return {boolean} - true if deferred hints are pending, false otherwise.
     */
    function hintsArePending(deferredHints) {
        return (deferredHints && !deferredHints.hasOwnProperty("hints") &&
            deferredHints.state() === "pending");
    }

    /**
     *  Common code to get the session hints. Will get guesses if there were
     *  no completions for the query.
     *
     * @param {string} query - user text to search hints with
     *  @param {{line:number, ch:number}} cursor - the location where the hints
     *  were created.
     * @param {{property: boolean,
                 showFunctionType:boolean,
                 context: string,
                 functionCallPos: {line:number, ch:number}}} type -
     *  type information about the hints
     *  @param {Object} token - CodeMirror token
     * @param {jQuery.Deferred=} $deferredHints - existing Deferred we need to
     * resolve (optional). If not supplied a new Deferred will be created if
     * needed.
     * @return {Object + jQuery.Deferred} - hint response (immediate or
     *     deferred) as defined by the CodeHintManager API
     */
    function getSessionHints(query, cursor, type, token, $deferredHints) {

        var hintResults = session.getHints(query, getStringMatcher());
        if (hintResults.needGuesses) {
            var guessesResponse = ScopeManager.requestGuesses(session,
                session.editor.document);

            if (!$deferredHints) {
                $deferredHints = $.Deferred();
            }

            guessesResponse.done(function () {
                if (hintsArePending($deferredHints)) {
                    hintResults = session.getHints(query, getStringMatcher());
                    setCachedHintContext(hintResults.hints, cursor, type, token);
                    var hintResponse = getHintResponse(cachedHints, query, type);
                    $deferredHints.resolveWith(null, [hintResponse]);
                }
            }).fail(function () {
                if (hintsArePending($deferredHints)) {
                    $deferredHints.reject();
                }
            });

            return $deferredHints;
        } else if (hintsArePending($deferredHints)) {
            setCachedHintContext(hintResults.hints, cursor, type, token);
            var hintResponse    = getHintResponse(cachedHints, query, type);
            $deferredHints.resolveWith(null, [hintResponse]);
            return null;
        } else {
            setCachedHintContext(hintResults.hints, cursor, type, token);
            return getHintResponse(cachedHints, query, type);
        }
    }

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

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

            // don't autocomplete within strings or comments, etc.
            if (token && HintUtils.hintable(token)) {
                if (session.isFunctionName()) {
                    return false;
                }

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

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

        if (token && HintUtils.hintableKey(key, !noHintsOnDot) && HintUtils.hintable(token)) {
            var type    = session.getType(),
                query   = session.getQuery();

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

            // Compute fresh hints if none exist, or if the session
            // type has changed since the last hint computation
            if (this.needNewHints(session)) {
                if (key) {
                    ScopeManager.handleFileChange([{from: cursor, to: cursor, text: [key]}]);
                    ignoreChange = true;
                }

                var scopeResponse   = ScopeManager.requestHints(session, session.editor.document),
                    $deferredHints  = $.Deferred(),
                    scopeSession    = session;

                scopeResponse.done(function () {
                    if (hintsArePending($deferredHints)) {
                        // Verify we are still in same session
                        if (scopeSession === session) {
                            getSessionHints(query, cursor, type, token, $deferredHints);
                        } else {
                            $deferredHints.reject();
                        }
                    }
                    scopeSession = null;
                }).fail(function () {
                    if (hintsArePending($deferredHints)) {
                        $deferredHints.reject();
                    }
                    scopeSession = null;
                });

                return $deferredHints;
            }

            if (cachedHints) {
                return getSessionHints(query, cursor, type, token);
            }
        }

        return null;
    };

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

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

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

            if (invalidPropertyName) {
                // need to walk back to the '.' and replace
                // with '["<hint>"]
                var dotCursor = session.findPreviousDot();
                if (dotCursor) {
                    completion = "[\"" + completion + "\"]";
                    start.line = dotCursor.line;
                    start.ch = dotCursor.ch - 1;
                }
            }
        }

        // Replace the current token with the completion
        // HACK (tracking adobe/brackets#1688): We talk to the private CodeMirror instance
        // directly to replace the range instead of using the Document, as we should. The
        // reason is due to a flaw in our current document synchronization architecture when
        // inline editors are open.
        session.editor._codeMirror.replaceRange(completion, start, end);

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

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

        /*
         * When the editor is changed, reset the hinting session and cached
         * information, and reject any pending deferred requests.
         *
         * @param {!Editor} editor - editor context to be initialized.
         * @param {?Editor} previousEditor - the previous editor.
         */
        function initializeSession(editor, previousEditor) {
            session = new Session(editor);
            ScopeManager.handleEditorChange(session, editor.document,
                previousEditor ? previousEditor.document : null);
            ParameterHintManager.setSession(session);
            cachedHints = null;
        }

        /*
         * Connects to the given editor, creating a new Session & adding listeners
         *
         * @param {?Editor} editor - editor context on which to listen for
         *      changes. If null, 'session' is cleared.
         * @param {?Editor} previousEditor - the previous editor
         */
        function installEditorListeners(editor, previousEditor) {
            // always clean up cached scope and hint info
            resetCachedHintContext();

            if (!jsHintsEnabled) {
                return;
            }

            if (editor && HintUtils.isSupportedLanguage(LanguageManager.getLanguageForPath(editor.document.file.fullPath).getId())) {
                initializeSession(editor, previousEditor);
                editor
                    .on(HintUtils.eventName("change"), function (event, editor, changeList) {
                        if (!ignoreChange) {
                            ScopeManager.handleFileChange(changeList);
                            ParameterHintManager.popUpHintAtOpenParen();
                        }
                        ignoreChange = false;
                    });
                ParameterHintManager.installListeners(editor);
            } else {
                session = null;
            }
        }

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

        /*
         * Handle the activeEditorChange event fired by EditorManager.
         * Uninstalls the change listener on the previous editor
         * and installs a change listener on the new editor.
         *
         * @param {Event} event - editor change event (ignored)
         * @param {Editor} current - the new current editor context
         * @param {Editor} previous - the previous editor context
         */
        function handleActiveEditorChange(event, current, previous) {
            // Uninstall "languageChanged" event listeners on previous editor's document & put them on current editor's doc
            if (previous) {
                previous.document
                    .off(HintUtils.eventName("languageChanged"));
            }
            if (current) {
                current.document
                    .on(HintUtils.eventName("languageChanged"), function () {
                        // If current doc's language changed, reset our state by treating it as if the user switched to a
                        // different document altogether
                        uninstallEditorListeners(current);
                        installEditorListeners(current);
                    });
            }

            uninstallEditorListeners(previous);
            installEditorListeners(current, previous);
        }

        /*
         * Handle JumptoDefiniton menu/keyboard command.
         */
        function handleJumpToDefinition() {
            var offset,
                handleJumpResponse;

            // Only provide jump-to-definition results when cursor is in JavaScript content
            if (!session || session.editor.getModeForSelection() !== "javascript") {
                return null;
            }

            var result = new $.Deferred();

            /**
             * Make a jump-to-def request based on the session and offset passed in.
             * @param {Session} session - the session
             * @param {number} offset - the offset of where to jump from
             */
            function requestJumpToDef(session, offset) {
                var response = ScopeManager.requestJumptoDef(session, session.editor.document, offset);

                if (response.hasOwnProperty("promise")) {
                    response.promise.done(handleJumpResponse).fail(function () {
                        result.reject();
                    });
                }
            }

            /**
             * Sets the selection to move the cursor to the result position.
             * Assumes that the editor has already changed files, if necessary.
             *
             * Additionally, this will check to see if the selection looks like an
             * assignment to a member expression - if it is, and the type is a function,
             * then we will attempt to jump to the RHS of the expression.
             *
             * 'exports.foo = foo'
             *
             * if the selection is 'foo' in 'exports.foo', then we will attempt to jump to def
             * on the rhs of the assignment.
             *
             * @param {number} start - the start of the selection
             * @param {number} end - the end of the selection
             * @param {boolean} isFunction - true if we are jumping to the source of a function def
             */
            function setJumpSelection(start, end, isFunction) {

                /**
                 * helper function to decide if the tokens on the RHS of an assignment
                 * look like an identifier, or member expr.
                 */
                function validIdOrProp(token) {
                    if (!token) {
                        return false;
                    }
                    if (token.string === ".") {
                        return true;
                    }
                    var type = token.type;
                    if (type === "variable-2" || type === "variable" || type === "property") {
                        return true;
                    }

                    return false;
                }

                var madeNewRequest = false;

                if (isFunction) {
                    // When jumping to function defs, follow the chain back
                    // to get to the original function def
                    var cursor = {line: end.line, ch: end.ch},
                        prev = session._getPreviousToken(cursor),
                        next,
                        offset;

                    // see if the selection is preceded by a '.', indicating we're in a member expr
                    if (prev.string === ".") {
                        cursor = {line: end.line, ch: end.ch};
                        next = session.getNextToken(cursor, true);
                        // check if the next token indicates an assignment
                        if (next && next.string === "=") {
                            next = session.getNextToken(cursor, true);
                            // find the last token of the identifier, or member expr
                            while (validIdOrProp(next)) {
                                offset = session.getOffsetFromCursor({line: cursor.line, ch: next.end});
                                next = session.getNextToken(cursor, false);
                            }
                            if (offset) {
                                // trigger another jump to def based on the offset of the RHS
                                requestJumpToDef(session, offset);
                                madeNewRequest = true;
                            }
                        }
                    }
                }
                // We didn't make a new jump-to-def request, so we can resolve the promise
                // and set the selection
                if (!madeNewRequest) {
                    // set the selection
                    session.editor.setSelection(start, end, true);
                    result.resolve(true);
                }
            }

            /**
             * handle processing of the completed jump-to-def request.
             * will open the appropriate file, and set the selection based
             * on the response.
             */
            handleJumpResponse = function (jumpResp) {

                if (jumpResp.resultFile) {
                    if (jumpResp.resultFile !== jumpResp.file) {
                        var resolvedPath = ScopeManager.getResolvedPath(jumpResp.resultFile);
                        if (resolvedPath) {
                            CommandManager.execute(Commands.FILE_OPEN, {fullPath: resolvedPath})
                                .done(function () {
                                    setJumpSelection(jumpResp.start, jumpResp.end, jumpResp.isFunction);
                                });
                        }
                    } else {
                        setJumpSelection(jumpResp.start, jumpResp.end, jumpResp.isFunction);
                    }
                } else {
                    result.reject();
                }
            };

            offset = session.getOffset();
            // request a jump-to-def
            requestJumpToDef(session, offset);

            return result.promise();
        }

        /*
         * Helper for QuickEdit jump-to-definition request.
         */
        function quickEditHelper() {
            var offset     = session.getCursor(),
                response   = ScopeManager.requestJumptoDef(session, session.editor.document, offset);

            return response;
        }

        // Register quickEditHelper.
        brackets._jsCodeHintsHelper = quickEditHelper;

        // Configuration function used for debugging
        brackets._configureJSCodeHints = setConfig;

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

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

        ProjectManager.on("beforeProjectClose", function () {
            ScopeManager.handleProjectClose();
        });

        ProjectManager.on("projectOpen", function () {
            ScopeManager.handleProjectOpen();
        });

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

        // init
        EditorManager.registerJumpToDefProvider(handleJumpToDefinition);

        var jsHints = new JSHints();
        CodeHintManager.registerHintProvider(jsHints, HintUtils.SUPPORTED_LANGUAGES, 0);

        ParameterHintManager.addCommands();

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


[submodule "support/expresso"]
	path = support/expresso
	url = git://github.com/visionmedia/expresso.git
[submodule "support/should"]
	path = support/should
	url = git://github.com/visionmedia/should.js.git


/*
 * 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, indent: 4, maxerr: 50 */
/*global define, $, window, Mustache */

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

    // Load dependent modules
    var KeyBindingManager = require("command/KeyBindingManager"),
        Menus             = require("command/Menus"),
        KeyEvent          = require("utils/KeyEvent"),
        StringUtils       = require("utils/StringUtils"),
        ValidationUtils   = require("utils/ValidationUtils"),
        ViewUtils         = require("utils/ViewUtils"),
        PopUpManager      = require("widgets/PopUpManager");

    var CodeHintListHTML  = require("text!htmlContent/code-hint-list.html");

    /**
     * Displays a popup list of hints for a given editor context.
     *
     * @constructor
     * @param {Editor} editor
     * @param {boolean} insertHintOnTab Whether pressing tab inserts the selected hint
     * @param {number} maxResults Maximum hints displayed at once. Defaults to 50
     */
    function CodeHintList(editor, insertHintOnTab, maxResults) {

        /**
         * The list of hints to display
         *
         * @type {Array.<string|jQueryObject>}
         */
        this.hints = [];

        /**
         * The selected position in the list; otherwise -1.
         *
         * @type {number}
         */
        this.selectedIndex = -1;

        /**
         * The maximum number of hints to display. Can be overriden via maxCodeHints pref
         *
         * @type {number}
         */
        this.maxResults = ValidationUtils.isIntegerInRange(maxResults, 1, 1000) ? maxResults : 50;

        /**
         * Is the list currently open?
         *
         * @type {boolean}
         */
        this.opened = false;

        /**
         * The editor context
         *
         * @type {Editor}
         */
        this.editor = editor;

        /**
         * Whether the currently selected hint should be inserted on a tab key event
         *
         * @type {boolean}
         */
        this.insertHintOnTab = insertHintOnTab;

        /**
         * Pending text insertion
         *
         * @type {string}
         */
        this.pendingText = "";

        /**
         * The hint selection callback function
         *
         * @type {Function}
         */
        this.handleSelect = null;

        /**
         * The hint list closure callback function
         *
         * @type {Function}
         */
        this.handleClose = null;

        /**
         * The hint list menu object
         *
         * @type {jQuery.Object}
         */
        this.$hintMenu =
            $("

")
                .append($("")
                        .hide())
                .append("

");

        this._keydownHook = this._keydownHook.bind(this);
    }

    /**
     * Select the item in the hint list at the specified index, or remove the
     * selection if index < 0.
     *
     * @private
     * @param {number} index
     */
    CodeHintList.prototype._setSelectedIndex = function (index) {
        var items = this.$hintMenu.find("li");

        // Range check
        index = Math.max(-1, Math.min(index, items.length - 1));

        // Clear old highlight
        if (this.selectedIndex !== -1) {
            $(items[this.selectedIndex]).find("a").removeClass("highlight");
        }

        this.selectedIndex = index;

        // Highlight the new selected item, if necessary
        if (this.selectedIndex !== -1) {
            var $item = $(items[this.selectedIndex]);
            var $view = this.$hintMenu.find("ul.dropdown-menu");

            $item.find("a").addClass("highlight");
            ViewUtils.scrollElementIntoView($view, $item, false);
        }
    };

    /**
     * Appends text to end of pending text.
     *
     * @param {string} text
     */
    CodeHintList.prototype.addPendingText = function (text) {
        this.pendingText += text;
    };

    /**
     * Removes text from beginning of pending text.
     *
     * @param {string} text
     */
    CodeHintList.prototype.removePendingText = function (text) {
        if (this.pendingText.indexOf(text) === 0) {
            this.pendingText = this.pendingText.slice(text.length);
        }
    };

    /**
     * Rebuilds the list items for the hint list.
     *
     * @private
     */
    CodeHintList.prototype._buildListView = function (hintObj) {
        var self            = this,
            match           = hintObj.match,
            selectInitial   = hintObj.selectInitial,
            view            = { hints: [] },
            _addHint;

        this.hints = hintObj.hints;
        this.hints.handleWideResults = hintObj.handleWideResults;

        // if there is no match, assume name is already a formatted jQuery
        // object; otherwise, use match to format name for display.
        if (match) {
            _addHint = function (name) {
                var displayName = name.replace(
                    new RegExp(StringUtils.regexEscape(match), "i"),
                    "$&"
                );

                view.hints.push({ formattedHint: "" + displayName + "" });
            };
        } else {
            _addHint = function (hint) {
                view.hints.push({ formattedHint: (hint.jquery) ? "" : hint });
            };
        }

        // clear the list
        this.$hintMenu.find("li").remove();

        // if there are no hints then close the list; otherwise add them and
        // set the selection
        if (this.hints.length === 0) {
            if (this.handleClose) {
                this.handleClose();
            }
        } else {
            this.hints.some(function (item, index) {
                if (index >= self.maxResults) {
                    return true;
                }

                _addHint(item);
            });

            // render code hint list
            var $ul = this.$hintMenu.find("ul.dropdown-menu"),
                $parent = $ul.parent();

            // remove list temporarily to save rendering time
            $ul.remove().append(Mustache.render(CodeHintListHTML, view));

            $ul.children("li").each(function (index, element) {
                var hint        = self.hints[index],
                    $element    = $(element);

                // store hint on each list item
                $element.data("hint", hint);

                // insert jQuery hint objects after the template is rendered
                if (hint.jquery) {
                    $element.find(".codehint-item").append(hint);
                }
            });

            // delegate list item events to the top-level ul list element
            $ul.on("click", "li", function (e) {
                // Don't let the click propagate upward (otherwise it will
                // hit the close handler in bootstrap-dropdown).
                e.stopPropagation();
                if (self.handleSelect) {
                    self.handleSelect($(this).data("hint"));
                }
            });

            // Lists with wide results require different formatting
            if (this.hints.handleWideResults) {
                $ul.find("li a").addClass("wide-result");
            }

            // attach to DOM
            $parent.append($ul);

            this._setSelectedIndex(selectInitial ? 0 : -1);
        }
    };

    /**
     * Computes top left location for hint list so that the list is not clipped by the window.
     * Also computes the largest available width.
     *
     * @private
     * @return {{left: number, top: number, width: number}}
     */
    CodeHintList.prototype._calcHintListLocation = function () {
        var cursor      = this.editor._codeMirror.cursorCoords(),
            posTop      = cursor.bottom,
            posLeft     = cursor.left,
            textHeight  = this.editor.getTextHeight(),
            $window     = $(window),
            $menuWindow = this.$hintMenu.children("ul"),
            menuHeight  = $menuWindow.outerHeight();

        // TODO Ty: factor out menu repositioning logic so code hints and Context menus share code
        // adjust positioning so menu is not clipped off bottom or right
        var bottomOverhang = posTop + menuHeight - $window.height();
        if (bottomOverhang > 0) {
            posTop -= (textHeight + 2 + menuHeight);
        }

        posTop -= 30;   // shift top for hidden parent element

        var menuWidth = $menuWindow.width();
        var availableWidth = menuWidth;
        var rightOverhang = posLeft + menuWidth - $window.width();
        if (rightOverhang > 0) {
            posLeft = Math.max(0, posLeft - rightOverhang);
        } else if (this.hints.handleWideResults) {
            // Right overhang is negative
            availableWidth = menuWidth + Math.abs(rightOverhang);
        }

        return {left: posLeft, top: posTop, width: availableWidth};
    };

    /**
     * Check whether keyCode is one of the keys that we handle or not.
     *
     * @param {number} keyCode
     */
    CodeHintList.prototype.isHandlingKeyCode = function (keyCode) {
        return (keyCode === KeyEvent.DOM_VK_UP || keyCode === KeyEvent.DOM_VK_DOWN ||
                keyCode === KeyEvent.DOM_VK_PAGE_UP || keyCode === KeyEvent.DOM_VK_PAGE_DOWN ||
                keyCode === KeyEvent.DOM_VK_RETURN ||
                (keyCode === KeyEvent.DOM_VK_TAB && this.insertHintOnTab));
    };

    /**
     * Convert keydown events into hint list navigation actions.
     *
     * @param {KeyBoardEvent} keyEvent
     */
    CodeHintList.prototype._keydownHook = function (event) {
        var keyCode,
            self = this;

        // positive distance rotates down; negative distance rotates up
        function _rotateSelection(distance) {
            var len = Math.min(self.hints.length, self.maxResults),
                pos;

            if (self.selectedIndex < 0) {
                // set the initial selection
                pos = (distance > 0) ? distance - 1 : len - 1;

            } else {
                // adjust current selection
                pos = self.selectedIndex;

                // Don't "rotate" until all items have been shown
                if (distance > 0) {
                    if (pos === (len - 1)) {
                        pos = 0;  // wrap
                    } else {
                        pos = Math.min(pos + distance, len - 1);
                    }
                } else {
                    if (pos === 0) {
                        pos = (len - 1);  // wrap
                    } else {
                        pos = Math.max(pos + distance, 0);
                    }
                }
            }

            self._setSelectedIndex(pos);
        }

        // Calculate the number of items per scroll page.
        function _itemsPerPage() {
            var itemsPerPage = 1,
                $items = self.$hintMenu.find("li"),
                $view = self.$hintMenu.find("ul.dropdown-menu"),
                itemHeight;

            if ($items.length !== 0) {
                itemHeight = $($items[0]).height();
                if (itemHeight) {
                    // round down to integer value
                    itemsPerPage = Math.floor($view.height() / itemHeight);
                    itemsPerPage = Math.max(1, Math.min(itemsPerPage, $items.length));
                }
            }

            return itemsPerPage;
        }

        // If we're no longer visible, skip handling the key and end the session.
        if (!this.isOpen()) {
            this.handleClose();
            return false;
        }

        // (page) up, (page) down, enter and tab key are handled by the list
        if (event.type === "keydown" && this.isHandlingKeyCode(event.keyCode)) {
            keyCode = event.keyCode;

            if (event.shiftKey &&
                    (event.keyCode === KeyEvent.DOM_VK_UP ||
                     event.keyCode === KeyEvent.DOM_VK_DOWN ||
                     event.keyCode === KeyEvent.DOM_VK_PAGE_UP ||
                     event.keyCode === KeyEvent.DOM_VK_PAGE_DOWN)) {
                this.handleClose();

                // Let the event bubble.
                return false;
            } else if (keyCode === KeyEvent.DOM_VK_UP) {
                _rotateSelection.call(this, -1);
            } else if (keyCode === KeyEvent.DOM_VK_DOWN) {
                _rotateSelection.call(this, 1);
            } else if (keyCode === KeyEvent.DOM_VK_PAGE_UP) {
                _rotateSelection.call(this, -_itemsPerPage());
            } else if (keyCode === KeyEvent.DOM_VK_PAGE_DOWN) {
                _rotateSelection.call(this, _itemsPerPage());
            } else if (this.selectedIndex !== -1 &&
                    (keyCode === KeyEvent.DOM_VK_RETURN ||
                    (keyCode === KeyEvent.DOM_VK_TAB && this.insertHintOnTab))) {

                if (this.pendingText) {
                    // Issues #5003: We received a "selection" key while there is "pending
                    // text". This is rare but can happen because CM uses polling, so we
                    // can receive key events while CM is waiting for timeout to expire.
                    // Pending text may dismiss the list, or it may cause a valid selection
                    // which keeps open hint list. We can compare pending text against
                    // list to determine whether list is dismissed or not, but to handle
                    // inserting selection in the page we'd need to either:
                    // 1. Synchronously force CodeMirror to poll (but there is not
                    //    yet a public API for that).
                    // 2. Pass pending text back to where text gets inserted, which
                    //    means it would need to be implemented for every HintProvider!
                    // You have to be typing so fast to hit this case, that's it's
                    // highly unlikely that inserting something from list was the intent,
                    // which makes this pretty rare, so case #2 is not worth implementing.
                    // If case #1 gets implemented, then we may want to use it here.
                    // So, assume that pending text dismisses hints and let event bubble.
                    return false;
                }

                // Trigger a click handler to commmit the selected item
                $(this.$hintMenu.find("li")[this.selectedIndex]).trigger("click");
            } else {
                // Let the event bubble.
                return false;
            }

            event.stopImmediatePropagation();
            event.preventDefault();
            return true;
        }

        // If we didn't handle it, let other global keydown hooks handle it.
        return false;
    };

    /**
     * Is the CodeHintList open?
     *
     * @return {boolean}
     */
    CodeHintList.prototype.isOpen = function () {
        // We don't get a notification when the dropdown closes. The best
        // we can do is keep an "opened" flag and check to see if we
        // still have the "open" class applied.
        if (this.opened && !this.$hintMenu.hasClass("open")) {
            this.opened = false;
        }

        return this.opened;
    };

    /**
     * Displays the hint list at the current cursor position
     *
     * @param {{hints: Array.<string|jQueryObject>, match: string,
     *          selectInitial: boolean}} hintObj
     */
    CodeHintList.prototype.open = function (hintObj) {
        Menus.closeAll();
        this._buildListView(hintObj);

        if (this.hints.length) {
            // Need to add the menu to the DOM before trying to calculate its ideal location.
            $("#codehint-menu-bar > ul").append(this.$hintMenu);

            var hintPos = this._calcHintListLocation();

            this.$hintMenu.addClass("open")
                .css({"left": hintPos.left, "top": hintPos.top, "width": hintPos.width + "px"});
            this.opened = true;

            PopUpManager.addPopUp(this.$hintMenu, this.handleClose, true);

            KeyBindingManager.addGlobalKeydownHook(this._keydownHook);
        }
    };

    /**
     * Updates the (already open) hint list window with new hints
     *
     * @param {{hints: Array.<string|jQueryObject>, match: string,
     *          selectInitial: boolean}} hintObj
     */
    CodeHintList.prototype.update = function (hintObj) {
        this.$hintMenu.addClass("apply-transition");
        this._buildListView(hintObj);

        // Update the CodeHintList location
        if (this.hints.length) {
            var hintPos = this._calcHintListLocation();
            this.$hintMenu.css({"left": hintPos.left, "top": hintPos.top,
                                "width": hintPos.width + "px"});
        }
    };

    /**
     * Closes the hint list
     */
    CodeHintList.prototype.close = function () {
        this.opened = false;

        if (this.$hintMenu) {
            this.$hintMenu.removeClass("open");
            PopUpManager.removePopUp(this.$hintMenu);
            this.$hintMenu.remove();
        }

        KeyBindingManager.removeGlobalKeydownHook(this._keydownHook);
    };

    /**
     * Set the hint list selection callback function
     *
     * @param {Function} callback
     */
    CodeHintList.prototype.onSelect = function (callback) {
        this.handleSelect = callback;
    };

    /**
     * Set the hint list closure callback function
     *
     * @param {Function} callback
     */
    CodeHintList.prototype.onClose = function (callback) {
        // TODO: Due to #1381, this won't get called if the user clicks out of
        // the code hint menu. That's (sort of) okay right now since it doesn't
        // really matter if a single old invisible code hint list is lying
        // around (it will ignore keydown events, and it'll get closed the next
        // time the user pops up a code hint). Once #1381 is fixed this issue
        // should go away.
        this.handleClose = callback;
    };

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


# decompress-zip [![Build Status](https://secure.travis-ci.org/bower/decompress-zip.png?branch=master)](http://travis-ci.org/bower/decompress-zip)

Extract files from a ZIP archive

## Usage

### .extract(options)

Extracts the contents of the ZIP archive `file`.

Returns an EventEmitter with two possible events - `error` on an error, and `extract` when the extraction has completed. The value passed to the `extract` event is a basic log of each file and how it was compressed.

The default value for `options` is `{ path: '.' }`. Currently `path` is the
only option, and is the output path for the extraction.

```js
var DecompressZip = require('decompress-zip');
var unzipper = new DecompressZip(filename)

unzipper.on('error', function (err) {
    console.log('Caught an error');
});

unzipper.on('extract', function (log) {
    console.log('Finished extracting');
});

unzipper.extract({
	path: 'some/path'
});
```

If `path` does not exist, decompress-zip will attempt to create it first.

### .list()

Much like extract, except:
- the success event is `list`
- the data for the event is an array of paths
- no files are actually extracted
- there are no options

```js
var DecompressZip = require('decompress-zip');
var unzipper = new DecompressZip(filename)

unzipper.on('error', function (err) {
    console.log('Caught an error');
});

unzipper.on('list', function (files) {
    console.log('The archive contains:');
    console.log(files);
});

unzipper.list();
```

## License

Released under the [MIT License](http://www.opensource.org/licenses/mit-license.php).


$('<span>' + " " + token.doc.trim() + '</span>').appendTo($hintObj).addClass("jshint-jsdoc");

keep keep keep keep keep replace keep keep


/*
 * 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, describe, beforeEach, afterEach, it, runs, waitsForDone, expect, $, beforeFirst, afterLast */

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

    // Load dependent modules
    var SpecRunnerUtils = require("spec/SpecRunnerUtils"),
        KeyEvent        = require("utils/KeyEvent"),
        Commands        = require("command/Commands"),
        EditorManager,      // loaded from brackets.test
        CommandManager,
        CodeHintManager,
        KeyBindingManager;

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

    describe("CodeHintManager", function () {
        this.category = "integration";

        /**
         * Performs setup for a code hint test. Opens a file and set pos.
         *
         * @param {!string} openFile Project relative file path to open in a main editor.
         * @param {!number|Array} openPos The pos within openFile to place the IP, or an array
         *      representing a multiple selection to set.
         */
        function initCodeHintTest(openFile, openPos) {
            SpecRunnerUtils.loadProjectInTestWindow(testPath);

            runs(function () {
                var promise = SpecRunnerUtils.openProjectFiles([openFile]);
                waitsForDone(promise);
            });

            runs(function () {
                var editor = EditorManager.getCurrentFullEditor();
                if (Array.isArray(openPos)) {
                    editor.setSelections(openPos);
                } else {
                    editor.setCursorPos(openPos.line, openPos.ch);
                }
            });
        }

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

                // uncomment this line to debug test window:
                //testWindow.brackets.app.showDeveloperTools();

                // Load module instances from brackets.test
                CodeHintManager     = testWindow.brackets.test.CodeHintManager;
                EditorManager       = testWindow.brackets.test.EditorManager;
                CommandManager      = testWindow.brackets.test.CommandManager;
                KeyBindingManager   = testWindow.brackets.test.KeyBindingManager;
            });
        });

        afterLast(function () {
            testWindow          = null;
            CodeHintManager     = null;
            EditorManager       = null;
            CommandManager      = null;
            KeyBindingManager   = null;
            SpecRunnerUtils.closeTestWindow();
        });

        afterEach(function () {
            runs(function () {
                testWindow.closeAllFiles();
            });
        });

        function invokeCodeHints() {
            CommandManager.execute(Commands.SHOW_CODE_HINTS);
        }

        // Note: these don't request hint results - they only examine hints that might already be open
        function expectNoHints() {
            var codeHintList = CodeHintManager._getCodeHintList();
            expect(codeHintList).toBeFalsy();
        }

        function expectSomeHints() {
            var codeHintList = CodeHintManager._getCodeHintList();
            expect(codeHintList).toBeTruthy();
            expect(codeHintList.isOpen()).toBe(true);
            return codeHintList;
        }

        // TODO: There seems to be an issue with CodeHintManager._removeHintProvider because of which the tests
        // added for "Hint Provider Registration" are interfering with this test case
        // (mock provider added for new language shows up for RegExp, even though no code hint should show up)
        // I am unable to reproduce this in a real scenario though/
        // This is the reason this test case is being added before the registration test cases.
        // We need to either figure out whats going wrong with register or change the RegExp code hint fix to bail
        // before HintUtils (Something like the fix for no code hints for multiple selection)
        describe("RegExp codehint tests", function () {
            it("should not show codehints for regular expression in a script block in html", function () {
                var editor,
                    pos = {line: 8, ch: 30};

                // Place cursor inside a sample regular expression
                // Note: line for pos is 0-based and editor lines numbers are 1-based
                initCodeHintTest("test1.html", pos);

                runs(function () {
                    editor = EditorManager.getCurrentFullEditor();
                    expect(editor).toBeTruthy();
                    invokeCodeHints();
                    expectNoHints();
                    editor = null;
                });
            });

            it("should not show codehints for regular expression in a Javascript file", function () {
                var editor,
                    pos = {line: 2, ch: 30};

                // Place cursor inside a sample regular expression
                // Note: line for pos is 0-based and editor lines numbers are 1-based
                initCodeHintTest("testRegexp.js", pos);

                runs(function () {
                    editor = EditorManager.getCurrentFullEditor();
                    expect(editor).toBeTruthy();
                    invokeCodeHints();
                    expectNoHints();
                    editor = null;
                });
            });
        });

        describe("Hint Provider Registration", function () {
            beforeEach(function () {
                initCodeHintTest("test1.html", {line: 0, ch: 0});
            });

            var mockProvider = {
                hasHints: function (editor, implicitChar) {
                    return true;
                },
                getHints: function (implicitChar) {
                    return { hints: ["mock hint"], match: null, selectInitial: false };
                },
                insertHint: function (hint) { }
            };

            function expectMockHints() {
                var codeHintList = expectSomeHints();
                expect(codeHintList.hints[0]).toBe("mock hint");
                expect(codeHintList.hints.length).toBe(1);
            }

            it("should register provider for a new language", function () {
                runs(function () {
                    CodeHintManager.registerHintProvider(mockProvider, ["clojure"], 0);

                    // Ensure no hints in language we didn't register for
                    invokeCodeHints();
                    expectNoHints();

                    // Expect hints in language we did register for
                    var promise = CommandManager.execute(Commands.FILE_OPEN, { fullPath: SpecRunnerUtils.makeAbsolute("test.clj") });
                    waitsForDone(promise);
                });
                runs(function () {
                    invokeCodeHints();
                    expectMockHints();

                    CodeHintManager._removeHintProvider(mockProvider, ["clojure"], 0);
                });
            });

            it("should register higher-priority provider for existing language", function () {
                runs(function () {
                    CodeHintManager.registerHintProvider(mockProvider, ["html"], 1);

                    // Expect hints to replace default HTML hints
                    var editor = EditorManager.getCurrentFullEditor();
                    editor.setCursorPos(3, 1);
                    invokeCodeHints();
                    expectMockHints();

                    CodeHintManager._removeHintProvider(mockProvider, ["html"], 1);
                });
            });

            it("should register \"all\" languages provider", function () {
                runs(function () {
                    CodeHintManager.registerHintProvider(mockProvider, ["all"], 0);

                    // Expect hints in language that already had hints (when not colliding with original provider)
                    invokeCodeHints();
                    expectMockHints();

                    // Expect hints in language that had no hints before
                    var promise = CommandManager.execute(Commands.FILE_OPEN, { fullPath: SpecRunnerUtils.makeAbsolute("test.clj") });
                    waitsForDone(promise);
                });
                runs(function () {
                    invokeCodeHints();
                    expectMockHints();

                    CodeHintManager._removeHintProvider(mockProvider, ["all"], 0);
                });
            });
        });

        describe("HTML Tests", function () {

            it("should show code hints menu and insert text at IP", function () {
                var editor,
                    pos = {line: 3, ch: 1},
                    lineBefore,
                    lineAfter;

                // minimal markup with an open '<' before IP
                // Note: line for pos is 0-based and editor lines numbers are 1-based
                initCodeHintTest("test1.html", pos);

                runs(function () {
                    editor = EditorManager.getCurrentFullEditor();
                    expect(editor).toBeTruthy();

                    // get text before insert operation
                    lineBefore = editor.document.getLine(pos.line);

                    invokeCodeHints();
                    expectSomeHints();
                });

                // simulate Enter key to insert code hint into doc
                runs(function () {
                    var e = $.Event("keydown");
                    e.keyCode = KeyEvent.DOM_VK_RETURN;

                    editor = EditorManager.getCurrentFullEditor();
                    expect(editor).toBeTruthy();

                    CodeHintManager._getCodeHintList()._keydownHook(e);

                    // doesn't matter what was inserted, but line should be different
                    var newPos = editor.getCursorPos();
                    lineAfter = editor.document.getLine(newPos.line);
                    expect(lineBefore).not.toEqual(lineAfter);

                    // and popup should auto-close
                    expectNoHints();

                    editor = null;
                });
            });

            it("should not show code hints if there is a multiple selection", function () {
                // minimal markup with an open '<' before IP
                // Note: line for pos is 0-based and editor lines numbers are 1-based
                initCodeHintTest("test1.html", [
                    {start: {line: 3, ch: 1}, end: {line: 3, ch: 1}, primary: true},
                    {start: {line: 4, ch: 1}, end: {line: 4, ch: 1}}
                ]);

                runs(function () {
                    invokeCodeHints();
                    expectNoHints();
                });
            });

            it("should dismiss existing code hints if selection changes to a multiple selection", function () {
                var editor;

                initCodeHintTest("test1.html", {line: 3, ch: 1});

                runs(function () {
                    editor = EditorManager.getCurrentFullEditor();
                    expect(editor).toBeTruthy();

                    invokeCodeHints();
                    expectSomeHints();
                });

                runs(function () {
                    editor.setSelections([
                        {start: {line: 3, ch: 1}, end: {line: 3, ch: 1}, primary: true},
                        {start: {line: 4, ch: 1}, end: {line: 4, ch: 1}}
                    ]);
                    expectNoHints();
                });
            });

            it("should dismiss code hints menu with Esc key", function () {
                var pos = {line: 3, ch: 1};

                // minimal markup with an open '<' before IP
                // Note: line for pos is 0-based and editor lines numbers are 1-based
                initCodeHintTest("test1.html", pos);

                runs(function () {
                    invokeCodeHints();

                    // verify list is open
                    expectSomeHints();
                });

                // simulate Esc key to dismiss code hints menu
                runs(function () {
                    var key = KeyEvent.DOM_VK_ESCAPE,
                        element = testWindow.$(".dropdown.open")[0];
                    SpecRunnerUtils.simulateKeyEvent(key, "keydown", element);

                    // verify list is no longer open
                    expectNoHints();
                });
            });

            it("should dismiss code hints menu when launching a command", function () {
                var editor,
                    pos = {line: 3, ch: 1};

                // minimal markup with an open '<' before IP
                // Note: line for pos is 0-based and editor lines numbers are 1-based
                initCodeHintTest("test1.html", pos);

                runs(function () {
                    editor = EditorManager.getCurrentFullEditor();
                    expect(editor).toBeTruthy();

                    editor.document.replaceRange("di", pos);
                    invokeCodeHints();

                    // verify list is open
                    expectSomeHints();
                });

                // Call Undo command to remove "di" and then verify no code hints
                runs(function () {
                    CommandManager.execute(Commands.EDIT_UNDO);

                    // verify list is no longer open
                    expectNoHints();

                    editor = null;
                });
            });

            it("should stop handling keydowns if closed by a click outside", function () {
                var editor,
                    pos = {line: 3, ch: 1};

                // minimal markup with an open '<' before IP
                // Note: line for pos is 0-based and editor lines numbers are 1-based
                initCodeHintTest("test1.html", pos);

                runs(function () {
                    editor = EditorManager.getCurrentFullEditor();
                    expect(editor).toBeTruthy();

                    editor.document.replaceRange("di", pos);
                    invokeCodeHints();

                    // verify list is open
                    expectSomeHints();

                    // get the document text and make sure it doesn't change if we
                    // click outside and then keydown
                    var text = editor.document.getText();

                    testWindow.$("body").click();
                    KeyBindingManager._handleKeyEvent({
                        keyCode: KeyEvent.DOM_VK_ENTER,
                        stopImmediatePropagation: function () { },
                        stopPropagation: function () { },
                        preventDefault: function () { }
                    });

                    // Verify that after the keydown, the session is closed
                    // (not just the hint popup). Because of #1381, we don't
                    // actually have a way to close the session as soon as the
                    // popup is dismissed by Bootstrap, so we do so on the next
                    // keydown. Eventually, once that's fixed, we should be able
                    // to move this expectNoHints() up after the click.
                    expectNoHints();
                    expect(editor.document.getText()).toEqual(text);

                    editor = null;
                });
            });
        });
    });
});