diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d0d16c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +*~ +*.pyc +local/* diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..2e741b4 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,30 @@ +# GraphTerm: A Graphical Terminal Interface +# +# GraphTerm was developed as part of the Mindmeldr project. +# Documentation can be found at http://info.mindmeldr.com/code/graphterm +# +# BSD License +# +# Copyright (c) 2012, Ramalingam Saravanan +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..7d58cad --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +include LICENSE.txt *.rst *.html +recursive-include graphterm *.py *.js *.css *.html *.rst *.txt *.gif *.jpg *.png +graft graphterm/bin +graft graphterm/certs +global-exclude *~ *.pyc test* diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..a3f050d --- /dev/null +++ b/README.rst @@ -0,0 +1,343 @@ +GraphTerm: A Graphical Terminal Interface +********************************************************************************* +.. sectnum:: +.. contents:: + +.. figure:: doc-images/gt-screen-stars3d.png + :align: center + :width: 90% + :figwidth: 60% + + Screenshot of GraphTerm illustrating graphical ``gls`` and ``cat`` command + output using a 3D perspective theme (captured on OS X Lion, using + Google Chrome). + + (More images can be found in SCREENSHOTS.rst or SCREENSHOTS.html + and in this `YouTube Video `_.) + + +Introduction +============================= + +``GraphTerm`` is a browser-based graphical terminal interface, that +aims to seamlessly blend the command line and graphical user +interfaces. The goal is to be fully backwards compatible with +``xterm``. You should be able to use it just like a regular terminal +interface, accessing additional features only as needed. GraphTerm builds +upon two earlier projects, +`XMLTerm `_ +which implemented a terminal using the Mozilla framework and +`AjaxTerm `_ +which is an AJAX/Python terminal implementation. (Another recent +project along these lines is `TermKit `_.) + +In addition to terminal features, GraphTerm implements file "finder" +or "explorer" features. It also incorporates some detached terminal +features of ``GNU screen``, as well as additional browser-based +sharing and collaboration capabilities. GraphTerm is designed to +be touch-friendly, by facilitating command re-use to minimize +the use of the keyboard. + +For a demo of some of the GraphTerm features, see this +`YouTube Video `_. + + +GraphTerm Design Goals: +--------------------------------------------- + + - Full backwards compatibility with xterm + + - Incremental feature set + + - Minimalist no-frills graphical UI + + - Minimize use of keyboard (tab/menu completion) + + - Touch-friendly + + - Cloud friendly + + - Platform-independent browser client + + - Easy sharing/collaboration + + +GraphTerm Features: +-------------------------------------------- + + - Clickable text: text displayed on terminal becomes clickable or "tappable" + + - Seamlessly blend text and (optional) graphics + + - History of all commands, entered by typing, clicking, or tapping + + - Multiple users can collaborate on a single terminal window + + - Multiple computers can be accessed from a single browser window + + - Drag and drop + + - Themable using CSS (including 3D perspectives) + + + +Installation +============================== + +To install ``GraphTerm``, you need to have Python 2.6+ and the Bash +shell on your Mac/Linux/Unix computer. For a quick install, if the python +``setuptools`` module is already installed on your system, +use the following commands:: + + sudo easy_install graphterm + sudo gterm_setup + +For the normal install procedure, download the release tarball from the +`Python Package Index `_, untar, +and execute the following command in the ``graphterm-`` directory:: + + python setup.py install + +You can also try out ``GraphTerm`` without installing it, by +running the server ``gtermserver.py`` in the ``graphterm`` +subdirectory, provided you have the ``tornado`` python module +installed in your system (or in the ``graphterm`` subdirectory). + +You can browse/fork the ``GraphTerm`` source code, and download the latest +version, at `Github `_. + + +Usage +================================= + +To start the ``GraphTerm`` server, use the command:: + + gtermserver --auth_code=none + +(You can use the ``--daemon=start`` option to run it in the background.) +Then, open up a browser that supports websockets, such as Google +Chrome, Firefox, or Safari (Chrome works best), and enter the +following URL:: + + http://localhost:8900 + +Alternatively, you can use the ``gterm`` command to open up the +browser window. + +Once within the ``graphterm`` browser page, select the host you +wish to connect to and create a new terminal session on the host. +Then try out the following commands:: + + gls + gvi + gweather + +The first two are graphterm-aware scripts that imitate +basic features of the standard ``ls`` and ``vi`` commands. + +*Usage Tips:* + + - *Terminal type:* The default terminal type is set to ``linux``, + but it has a poor fullscreen mode and command history does + not work properly. You can try out the terminal types ``screen`` + or ``xterm``, which may work better for some purposes. + Use the ``--term_type`` option to set the default terminal type. + (Fully supporting these terminal types is a work in progress.) + + - *Sessions and sharing:* For each host, sessions are assigned default names like + ``tty1`` etc. You can also create unique session names simply by using + it in an URL, e.g.:: + + http://localhost:8900/local/mysession + + The first user to create a session "owns" it. Others connecting to the + same session have read-only access (unless they "steal" the session). + + - *Multiple hosts:* More than one host can connect to the ``graphterm`` server. + The local host is connected by default. To connect an additional + host, run the following command on the host you wish to connect:: + + gtermhost + + where ``serveraddr`` is the address or name of the computer where + the server is running. You can use the ``--daemon=start`` option to + run the command in the background. (By default, the server listens for host + connections on port 8899.) + + - *Security:* The ``--auth_code`` option can be used to specify + an authentication code required for users connecting to the server. + Although multiple hosts can connect to the terminal server, + initially, it would be best to use ``graphterm`` to simply connect + to ``localhost``, on a computer with only trusted users. + (*Note:* Users can always use SSH port forwarding to securely connect + to the ``graphterm`` server listening as ``localhost`` on a remote + computer, e.g.. ``ssh -L 8900:localhost:8900 user@example.com``) + *Do not run the server as root*. As the code matures, + security can be improved through the use of SSL certificates + and server/client authentication. + These features are implemented in the code, but have not been + properly configured/tested. + + - *Visual cues:* In the default theme, *blue* color denotes text that can + be *clicked* or *tapped*. The action triggered by clicking depends on + several factors, such as whether there is text in the current command + line, and whether the Control modifier in the *Bottom menu* is active. + Click on the last displayed prompt to toggle display of the *Bottom menu*. + Clicking on other prompts toggles display of the command output + (unless the Control modifier is used, in which case the command line + is copied and pasted.) + + - *Copy/paste:* Click on the cursor to paste text from the clipboard. + + - *Drag and drop:* Sort of works within a window and across two + windows. You can drag filenames (text-only) and drop them on + folders, executables, or the command line. Visual feedback can + be confusing. + + - *Command recall:* Use *up/down arrows* after partially typing a + command to search for matching commands, and use *right arrow* + for completion. + + - *Touch devices:* Click on the cursor to display virtual keyboard + on the ipad etc. + + - *Themes:* Themes are a work in progress, especially the 3-D + perspective theme (which only works on Chrome/Safari). + + + +Support +============================= + + - Report bugs and other issues using the Github `Issue Tracker `_. + + - Additional documentation and updates will be made available on the project home page, + `info.mindmeldr.com/code/graphterm `_. + + +Implementation +========================================== + +The GraphTerm server written in pure python, using the +`Tornado web framework `_, +with websocket support. The GraphTerm client uses standard +HTML5+Javascript+CSS. + +GraphTerm extends the ``xterm`` terminal API by adding a +new control sequence for programs to transmit a CGI-like HTTP response +through standard output (via a websocket) to be displayed in the +browser window. GraphTerm-aware programs can interact with the +user using HTML forms etc. + + +API for GraphTerm-aware programs +========================================== + +A `graphterm-aware program `_ +writes to to the standard output in a format similar to a HTTP +response, preceded and followed by +``xterm``-like *escape sequences*:: + + \x1b[?1155;h + {"content_type": "text/html", ...} + + + ... +
+ \x1b[?1155l + +where ```` denotes a numeric value stored in the environment +variable ``GRAPHTERM_COOKIE``. (The random cookie is a security +measure that prevents malicious files from accessing GraphTerm.) +The opening escape sequence is followed by a *dictionary* of header +names and values, using JSON format. This is followed by a blank line, +and then any data (such as the HTML fragment to be displayed). + +A `graphterm-aware program `_ +can be written in any language, much like a CGI script. +See the programs ``gls``, ``gvi``, ``gweather``, ``ec2launch`` and +``ec2list`` for examples of GraphTerm API usage. + + +Cloud integration +=============================== + +The GraphTerm distribution includes the scripts ``ec2launch, ec2list, ec2scp,`` +and ``ec2ssh`` to launch and monitor Amazon Web Services EC2 instances +to run GraphTerm in the "cloud". You will need to have an Amazon AWS +account to use these scripts, and also need to install the ``boto`` python module. +To create an instance, use the command:: + + ec2instance + +To *temporarily* run a publicly accessible GraphTerm server for +demonstration or teaching purposes, use the following command on the instance:: + + gtermserver --daemon=start --auth_code=none --host= + +*Note: This is totally insecure and should not be used for handling any sensitive information.* +Ensure that the security group associated with the cloud instance +allows access to inbound TCP port 22 (for SSH access), 8900 (for GraphTerm users to connect), and +port 8899 (for GraphTerm hosts to connect). Also, when using ``ec2scp`` and ``sc2ssh`` +to access the instance, ensure that you specify the appropriate login name (e.g., ``ubuntu`` +for Ubuntu distribution). +Secondary cloud instances should connect to the GraphTerm server on +the primary instance using the command:: + + gtermhost --daemon=start + +For increased security in a publicly-accessible server, you will need to use a cryptic authentication code, +and also use *https* instead of *http*, with SSL cettificates . Since GraphTerm is currently in +*alpha* status, security cannot be guaranteed even with these options enabled. +(To avoid these problems, use SSH port forwarding to access GraphTerm +on ``localhost`` whenever possble.) + +*otrace* integration +=============================== + +GraphTerm was originally developed as a graphical front-end for +`otrace `_, +an object-oriented python debugger. Use the ``--oshell`` +option when connecting a host to the server enables ``otrace`` +debugging features, allowing access to the innards of the +program running on the host. + + +Caveats and limitations +=============================== + + - *Reliability:* This software has not been subject to extensive testing. Use at your own risk. + + - *Platforms:* The ``GraphTerm`` client should work on most recent browsers that support Websockets, such as Google Chrome, Firefox, and Safari. The ``GraphTerm`` server is pure-python, but with some OS-specific calls for file, shell, and terminal-related operations. It has been tested only on Linux and Mac OS X so far. + + - *Current limitations:* + * Support for ``xterm`` escape sequences is incomplete. + * Most features of GraphTerm only work with the bash shell, not with C-shell, due the need for PROMPT_COMMAND to keep track of the current working directory. + * At the moment, you cannot customize the shell prompt. (You + should be able to in the future.) + +Credits +=============================== + +``GraphTerm`` is inspired by two earlier projects that implement the +terminal interface within the browser, +`XMLTerm `_ and +`AjaxTerm `_. +It borrows many of the ideas from *XMLTerm* and re-uses chunks of code from +*AjaxTerm*. + +The ``gls`` command uses icons from the `Tango Icon Library `_ + + Graphical editing uses the `Ajax.org Cloud9 Editor `_ + +The 3D perspective mode was inspired by Sean Slinsky's `Star Wars +Opening Crawl with CSS3 `_. + +``GraphTerm`` was developed as part of the `Mindmeldr `_ project, which is aimed at improving classroom interaction. + + +License +===================== + +``GraphTerm`` is distributed as open source under the `BSD-license `_. + diff --git a/SCREENSHOTS.rst b/SCREENSHOTS.rst new file mode 100644 index 0000000..30f08de --- /dev/null +++ b/SCREENSHOTS.rst @@ -0,0 +1,128 @@ +GraphTerm Screenshots +********************************************************************************* +.. sectnum:: +.. contents:: + +ls vs. gls +================================================== + +.. figure:: doc-images/gt-screen-ls-gls.png + :align: center + + Comparing plain vanilla ``ls`` command and the graphterm-aware ``gls``. + The icons and the blue filenames are clickable. (The icon display + is optional, and may be disabled.) + + .. + +.. raw:: html + +
+ + +stars3d theme, with icons enabled +================================================== + +.. figure:: doc-images/gt-screen-stars3d.png + :align: center + :width: 90% + :figwidth: 85% + + Showing output of the ``cat episode4.txt`` command below the + output of the ``gls`` command, using the 3D perspective theme. + This is actually a working theme, although it is meant for + primarily for "show". Scrolling through a large text file using the + ``vi`` editor in this theme gives a nice *roller coaster* effect! + (This screenshot was captured with Google Chrome running on + Mac OS X Lion, which supports hidden scrollbars. On other + software platforms, the scrollbar will be visible.) + + .. + +.. raw:: html + +
+ +Graphical weather forecast (using Google Weather API) +========================================================= + +.. figure:: doc-images/gt-screen-gweather.png + :align: center + + Showing the screen for the command ``gweather College Station`` to + illustrate inline HTML display. If the location is omitted, a HTML + form will be displayed to enter the location name. + + .. + + +.. raw:: html + +
+ +Text editing (emacs) +================================================== + +.. figure:: doc-images/gt-screen-emacs.png + :align: center + + Showing the screen for the command ``emacs gtermserver.py`` to + illustrate backwards compatibility with the traditional terminal interface. + + .. + + +.. raw:: html + +
+ +Graphical code editing using a "cloud" editor +================================================== + +.. figure:: doc-images/gt-screen-gvi.png + :align: center + + Showing the screen for the command ``gvi gtermserver.py`` to + illustrate graphical editing using the Ajax.org Cloud9 editor (ACE). + + .. + + +.. raw:: html + +
+ +Collapsed mode +================================================== + +.. figure:: doc-images/gt-screen-collapsed.png + :align: center + + Showing the screen when all command output is collapsed. Clicking + on any of the underlined prompts will display the command output. + Also note the *Bottom menubar*, which is enabled by clicking on + the last prompt. Clicking on *Control* and then any of the prompts + will cause the corresponding command to be pasted. + + .. + + +.. raw:: html + +
+ +Split scrolling +================================================== + +.. figure:: doc-images/gt-screen-split.png + :align: center + + Showing the split-screen scrolling mode, where the command + line is anchored at the bottom of the screen. Clicking on ``gls`` + output will paste filenames into the command line. + + .. + +.. raw:: html + +
diff --git a/doc-images/gt-screen-collapsed.png b/doc-images/gt-screen-collapsed.png new file mode 100644 index 0000000..663ad37 Binary files /dev/null and b/doc-images/gt-screen-collapsed.png differ diff --git a/doc-images/gt-screen-emacs.png b/doc-images/gt-screen-emacs.png new file mode 100644 index 0000000..c3bb016 Binary files /dev/null and b/doc-images/gt-screen-emacs.png differ diff --git a/doc-images/gt-screen-gvi.png b/doc-images/gt-screen-gvi.png new file mode 100644 index 0000000..5873f6e Binary files /dev/null and b/doc-images/gt-screen-gvi.png differ diff --git a/doc-images/gt-screen-gweather.png b/doc-images/gt-screen-gweather.png new file mode 100644 index 0000000..3f4fdf2 Binary files /dev/null and b/doc-images/gt-screen-gweather.png differ diff --git a/doc-images/gt-screen-ls-gls.png b/doc-images/gt-screen-ls-gls.png new file mode 100644 index 0000000..1af8f67 Binary files /dev/null and b/doc-images/gt-screen-ls-gls.png differ diff --git a/doc-images/gt-screen-split.png b/doc-images/gt-screen-split.png new file mode 100644 index 0000000..57bb899 Binary files /dev/null and b/doc-images/gt-screen-split.png differ diff --git a/doc-images/gt-screen-stars3d.png b/doc-images/gt-screen-stars3d.png new file mode 100644 index 0000000..8f2e6d3 Binary files /dev/null and b/doc-images/gt-screen-stars3d.png differ diff --git a/graphterm/__init__.py b/graphterm/__init__.py new file mode 100644 index 0000000..542aa4e --- /dev/null +++ b/graphterm/__init__.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# +# GraphTerm: A Graphical Terminal Interface +# +# GraphTerm was developed as part of the Mindmeldr project. +# Documentation can be found at http://info.mindmeldr.com/code/graphterm +# +# BSD License +# +# Copyright (c) 2012, Ramalingam Saravanan +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +"""GraphTerm: A Graphical Terminal Interface +""" diff --git a/graphterm/bin/ec2common.py b/graphterm/bin/ec2common.py new file mode 100755 index 0000000..b5ea61e --- /dev/null +++ b/graphterm/bin/ec2common.py @@ -0,0 +1,67 @@ +""" +ec2common: Common code to manage Amazon AWS EC2 instances +""" + +import boto +import os +import sys +import time + +from boto.route53.connection import Route53Connection + +boto_config = """Create the file ~/.boto containing: + [Credentials] + aws_access_key_id = ACCESS_KEY + aws_secret_access_key = SECRET_KEY +""" + +if not os.path.exists(os.path.expanduser("~/.boto")): + print >> sys.stderr, boto_config + sys.exit(1) + +def get_zone(zone_domain): + route53conn = Route53Connection() + + results = route53conn.get_all_hosted_zones() + zones = results['ListHostedZonesResponse']['HostedZones'] + + for zone in zones: + if zone['Name'] == zone_domain+".": + return route53conn, zone + + return (route53conn, None) + +def cname(route53conn, zone, domain_name, alt_name, ttl=60, remove=False): + from boto.route53.record import ResourceRecordSets + zone_id = zone['Id'].replace('/hostedzone/', '') + changes = ResourceRecordSets(route53conn, zone_id) + change = changes.add_change("DELETE" if remove else "CREATE", + name=domain_name, + type="CNAME", ttl=ttl) + if alt_name: + change.add_value(alt_name) + changes.commit() + +def get_ec2(): + return boto.connect_ec2() + +def kill(instance_ids=[]): + ec2 = get_ec2() + ec2.terminate_instances(instance_ids=instance_ids) + +def get_instance_props(instance_id=None): + ec2 = get_ec2() + + all_instances = ec2.get_all_instances() + props_list = [] + for res in all_instances: + iobj = res.instances[0] + if not instance_id or instance_id == iobj.id or any(tag.startswith(instance_id) for tag in iobj.tags): + props = {"id": iobj.id, + "public_dns": iobj.public_dns_name, + "key": iobj.key_name, + "state": iobj.state, + "tags": iobj.tags} + props_list.append(props) + + return props_list diff --git a/graphterm/bin/ec2launch b/graphterm/bin/ec2launch new file mode 100755 index 0000000..285f2b0 --- /dev/null +++ b/graphterm/bin/ec2launch @@ -0,0 +1,153 @@ +#!/usr/bin/env python +# + +""" +ec2launch: Launch Amazon AWS EC2 instance +""" + +import boto +import json +import os +import random +import sys +import time +from optparse import OptionParser + +import gtermapi +import ec2common + +ssh_dir = os.path.expanduser("~/.ssh") + +arg_list = [ +("arg1", "", "Instance name"), +("type", ("", "t1.micro", "m1.small", "m1.medium", "m1.large"), "Instance type"), +("key_name", "ec2key", "Instance management SSH key name"), +("ami", "ami-82fa58eb", "Instance OS (default: Ubuntu 12.04LTS)"), +("security", "default", "Security group"), +("domain", "", "Instance domain"), +] + +usage = "usage: %prog [-f] " +parser = OptionParser(usage=usage) +parser.add_option("-f", "--fullscreen", dest="fullscreen", default=False, + help="Fullscreen display", action="store_true") + +parser.add_option("-t", "--text", dest="text", default=False, + help="Text only", action="store_true") + +parser.add_option("", "--dry_run", dest="dry_run", default=False, + help="Dry run", action="store_true") + +form_html = gtermapi.add_options(parser, arg_list, title="Create Amazon EC2 instance") + +(options, args) = parser.parse_args() + +headers = {"content_type": "text/html"} +headers["x_gterm_response"] = "pagelet_fullscreen" +headers["x_gterm_parameters"] = {"scroll": "top", "current_directory": os.getcwd()} + +if args: + instance_tag = args[0] +elif options.domain: + instance_tag = options.domain +else: + if not gtermapi.Lterm_cookie or options.text: + print >> sys.stderr, usage + sys.exit(1) + headers["x_gterm_response"] = "pagelet_form" + sys.stdout.write(gtermapi.wrap(json.dumps(headers)+"\n\n"+form_html)) + sys.exit(1) + +if options.domain: + subdomain_name, sep, top_level_domain = options.domain.partition(".") +else: + subdomain_name, top_level_domain = "", "" + +key_file = os.path.join(ssh_dir, options.key_name+".pem") + +if not os.path.exists(ssh_dir): + print >> sys.stderr, "ec2launch: %s directory not found!" % ssh_dir + sys.exit(1) + +headers = {"content_type": "text/html"} +headers["x_gterm_response"] = "pagelet_fullscreen" +headers["x_gterm_parameters"] = {"scroll": "top", "current_directory": os.getcwd()} + +startup_script = """#!/bin/bash +set -e -x +apt-get update && apt-get upgrade -y +apt-get install -y python-setuptools +easy_install tornado +easy_install otrace +easy_install graphterm +sudo gterm_setup +""" + +# Connect to EC2 +ec2 = boto.connect_ec2() + +# Create key pair, if needed +if not os.path.exists(key_file): + key_pair = ec2.create_key_pair(options.key_name) + key_pair.save(ssh_dir) + os.chmod(key_file, 0600) + +if options.dry_run: + print >> sys.stderr, "run_instances:", dict(image_id=options.ami, + instance_type=options.type, + key_name=options.key_name, + security_groups=[options.security], + user_data=startup_script) + sys.exit(1) + +# Launch instance +reservation = ec2.run_instances(image_id=options.ami, + instance_type=options.type, + key_name=options.key_name, + security_groups=[options.security], + user_data=startup_script) +instance = reservation.instances[0] + +# Wait for instance to start running +Status_template = """Creating instance %s: status=%s (waiting %ds)""" + +status = instance.update() +headers["x_gterm_response"] = "pagelet_fullscreen" +start_time = time.time() +while status == "pending": + timesec = int(time.time() - start_time) + sys.stdout.write(gtermapi.wrap(json.dumps(headers)+"\n\n"+(Status_template % (instance_tag, status, timesec)))) + time.sleep(3) + status = instance.update() + +if status != "running": + print >> sys.stderr, "ec2launch: ERROR Failed to launch instance: %s" % status + sys.exit(1) + +# Tag instance +if instance_tag: + instance.add_tag(instance_tag) + +instance_id = reservation.id +instance_obj = None +all_instances = ec2.get_all_instances() +for r in all_instances: + if r.id == instance_id: + instance_obj = r.instances[0] + break + +if not instance_obj: + print >> sys.stderr, "ec2launch: ERROR Unable to find launched instance: %s" % status + sys.exit(1) + +instance_domain = instance_obj.public_dns_name + +if options.domain: + route53conn, zone = ec2common.get_zone(top_level_domain) + if not zone: + print >> sys.stderr, "No Route53 zone found for %s" % options.domain + + # Create new CNAME entry pointing to instance public DNS + ec2common.cname(route53conn, zone, options.domain, instance_domain) + +print "Created EC2 instance: id=%s, domain=%s" % (instance_id, instance_domain) diff --git a/graphterm/bin/ec2list b/graphterm/bin/ec2list new file mode 100755 index 0000000..24f6f24 --- /dev/null +++ b/graphterm/bin/ec2list @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# + +""" +ec2list: Launch Amazon AWS EC2 instances +""" + +import boto +import json +import os +import sys +import time +from optparse import OptionParser + +import ec2common + +usage = "usage: %prog [-f] []" +parser = OptionParser(usage=usage) +parser.add_option("-f", "--fullscreen", dest="fullscreen", default=False, + help="Fullscreen display", action="store_true") + +parser.add_option("", "--kill", dest="kill", default=False, + help="Kill single matching instance", action="store_true") + +parser.add_option("", "--killall", dest="killall", default=False, + help="Kill all matching instances", action="store_true") + + +(options, args) = parser.parse_args() + +prefix = args[0] if args else "" + +if not os.path.exists(os.path.expanduser("~/.boto")): + print >> sys.stderr, config_info + sys.exit(1) + +headers = {"content_type": "text/html"} +headers["x_gterm_response"] = "pagelet_fullscreen" +headers["x_gterm_parameters"] = {"scroll": "top", "current_directory": os.getcwd()} + +props_list = ec2common.get_instance_props(instance_id=prefix) + +Props_format = "Instance: id=%(id)s, domain=%(public_dns)s, key=%(key)s, tags=%(tags)s, state=%(state)s" +for props in props_list: + print Props_format % props + +if options.kill or options.killall: + if not props_list: + print >> sys.stderr, "No instances to kill" + sys.exit(1) + id_list = [x["id"] for x in props_list] + + if len(id_list) > 1 and not options.killall: + print >> sys.stderr, "Specify --killall to kill multiple instances" + sys.exit(1) + ec2common.kill(instance_ids=id_list) + print >> sys.stderr, "Killed", id_list diff --git a/graphterm/bin/ec2scp b/graphterm/bin/ec2scp new file mode 100755 index 0000000..075195b --- /dev/null +++ b/graphterm/bin/ec2scp @@ -0,0 +1,4 @@ +#!/bin/bash + +scp -i ~/.ssh/ec2key.pem $* + diff --git a/graphterm/bin/ec2ssh b/graphterm/bin/ec2ssh new file mode 100755 index 0000000..93a3646 --- /dev/null +++ b/graphterm/bin/ec2ssh @@ -0,0 +1,4 @@ +#!/bin/bash + +ssh -i ~/.ssh/ec2key.pem $* + diff --git a/graphterm/bin/gls b/graphterm/bin/gls new file mode 100755 index 0000000..4f6a12e --- /dev/null +++ b/graphterm/bin/gls @@ -0,0 +1,165 @@ +#!/usr/bin/env python +# + +""" +gls: graphterm-aware ls +""" + +import json +import mimetypes +import os +import sys +import xml.dom.minidom +from optparse import OptionParser + +Lterm_cookie = os.getenv("GRAPHTERM_COOKIE", "") +Html_escapes = ["\x1b[?1155;%sh" % Lterm_cookie, + "\x1b[?1155l"] + +SPECIAL_FILES = set(["..", ".", "~"]) + +glscmd = os.getenv("GRAPHTERM_LS_CMD", "") or "gls" +gvicmd = os.getenv("GRAPHTERM_VI_CMD", "") or "gvi" +gopencmd = os.getenv("GRAPHTERM_OPEN_CMD", "") or "gopen" + +FILE_TYPES = {"directory": ("/static/images/tango-folder.png", "cd %(path); "+glscmd+" -f"), + "executable": ("/static/images/tango-application-x-executable.png", ""), + "audiofile": ("/static/images/tango-audio-x-generic.png", gopencmd), + "htmlfile": ("/static/images/tango-text-html.png", gvicmd), + "imagefile": ("/static/images/tango-image-x-generic.png", gopencmd), + "plainfile": ("/static/images/tango-text-x-generic-template.png", gopencmd), + "textfile": ("/static/images/tango-text-x-generic.png", gvicmd), + "videofile": ("/static/images/tango-video-x-generic.png", gopencmd), +} + +IMGFORMAT = "" + +TXTFORMAT = "%(filename)s" + +def file2html(filepath, filename, filemode=0): + if filename in SPECIAL_FILES: + filetype = "directory" + elif os.path.lexists(filepath): + mimetype, encoding = mimetypes.guess_type(filename) + filetype = "plainfile" + if os.path.isdir(filepath): + filetype = "directory" + elif os.access(filepath, os.X_OK): + filetype = "executable" + elif mimetype: + if mimetype.startswith("audio/"): + filetype = "audiofile" + elif mimetype == "text/html": + filetype = "htmlfile" + elif mimetype.startswith("image/"): + filetype = "imagefile" + elif mimetype.startswith("text/") or mimetype == "application/javascript": + filetype = "textfile" + elif mimetype.startswith("video/"): + filetype = "videofile" + else: + return "", "" + + classes = "droppable" if filetype in ("directory", "executable") else "" + fileicon, filecmd = FILE_TYPES[filetype] + params = {"classes": classes, "filepath": filepath, "filename": filename, + "filetype": filetype, "fileicon": fileicon, "filecmd": filecmd} + + return IMGFORMAT % params, TXTFORMAT % params + +def files2html(file_list, ncols=4): + rows = [] + rowimg = [] + rowtxt = [] + + for j, fileinfo in enumerate(file_list): + if len(fileinfo) == 2: + fpath, fname, fsize, ftime, fmode, fuid, fgid = fileinfo[0], fileinfo[1], 0, 0, 0, 0, 0 + else: + fpath, fname, fsize, ftime, fmode, fuid, fgid = fileinfo + cellimg, celltxt = file2html(fpath, fname, fmode) + rowimg.append(cellimg) + rowtxt.append(celltxt) + if rowtxt and (not ((j+1) % ncols) or (j+1 == len(file_list))): + rows.append( '' + "".join(rowimg) ) + rows.append( '' + "".join(rowtxt) ) + rowimg = [] + rowtxt = [] + + return "\n".join(rows) + +def get_file_info(filename): + filename = os.path.expanduser(filename) + filepath = os.path.normcase(os.path.abspath(filename)) + if filename.startswith(".."): + filename = filepath + if os.path.exists(filepath): + fstat = os.stat(filepath) + elif os.path.lexists(filepath): + fstat = os.lstat(filepath) + else: + return (filepath, filename, 0, 0, 0, 0, 0) + return (filepath, filename, fstat.st_size, fstat.st_mtime, fstat.st_mode, fstat.st_uid, fstat.st_gid) + +def wrap(html): + return Html_escapes[0] + html + Html_escapes[1] + + +usage = "usage: %prog [-f] " +parser = OptionParser(usage=usage) +parser.add_option("-f", "--fullscreen", + action="store_true", dest="fullscreen", default=False, + help="Fullscreen display") +parser.add_option("-a", "--all", + action="store_true", dest="all", default=False, + help="Display all files, including hidden") +parser.add_option("-s", "--size", + action="store_true", dest="size", default=False, + help="Sort by file size") +parser.add_option("-t", "--timer", + action="store_true", dest="time", default=False, + help="Sort by time modified") + +(options, args) = parser.parse_args() + +home_dir = os.path.expanduser("~") +work_dir = os.getcwd() +parent_dir, dir_name = os.path.split(work_dir) + +special_dirs = [(parent_dir, ".."), + (work_dir, "."), + (home_dir, "~")] + +if not args: + args = os.listdir(work_dir) + if not options.all: + args = [x for x in args if not x.startswith(".")] + +File_list = [get_file_info(filename) for filename in args] + +if options.size: + File_list.sort(key=lambda x:x[2]) +elif options.time: + File_list.sort(key=lambda x:x[3]) +else: + File_list.sort(key=lambda x:x[1]) + +ncols = 4 + +Table_list = ['', + '' % (ncols,), + ] + +Table_list.append(files2html(special_dirs, ncols)) +Table_list.append(files2html(File_list, ncols)) + +Table_list.append('
') + +html = "\n".join(Table_list) + "\n" + +headers = {"content_type": "text/html"} +headers["x_gterm_response"] = "pagelet_fullscreen" if options.fullscreen else "pagelet" +headers["x_gterm_parameters"] = {"scroll": "top", "current_directory": work_dir} + +sys.stdout.write(wrap(json.dumps(headers)+"\n\n"+html)) + diff --git a/graphterm/bin/gls.sh b/graphterm/bin/gls.sh new file mode 100755 index 0000000..778d7c6 --- /dev/null +++ b/graphterm/bin/gls.sh @@ -0,0 +1,127 @@ +#!/bin/bash +# gls: a GraphTerm shell wrapper for the UNIX "ls" command +# Usage: gls + +# TEMPORARY: Ignores all arguments except -f and last directory + +options="" +response_type="pagelet" +dir="" +for arg in $*; do + if [ "$arg" == "-f" ]; then + response_type="pagelet_fullscreen" + elif [[ "$arg" == -* ]]; then + options="$options $arg" + else + dir="$arg" + fi +done + +if [ "$dir" != "" ]; then + cd $dir +fi + +if [ "$options" != "" ]; then + # Options encountered; default "ls" behaviour + /bin/ls $* + exit +fi + +ncols=4 + +echocmd1="echo -n" +##echocmd1="/bin/echo -e" +echocmd2="echo" + +rowimg="" +rowtxt="" + +if [ -z $GRAPHTERM_PROMPT ]; then + glscmd="~/meldr-hg/xmlterm/bin/gls" + gvicmd="~/meldr-hg/xmlterm/bin/gvi" +else + glscmd="gls" + gvicmd="gvi" +fi + +output="" +clickcmd="cd %(path); $glscmd -f" +files='.. . ~' +for file in $files; do + fileicon="/static/images/tango-folder.png" + filetype="specialfile" + + if [ "$file" == ".." ]; then + fullpath=$(dirname "$PWD") + elif [ "$file" == "." ]; then + fullpath="$PWD" + elif [ "$file" == '~' ]; then + fullpath="$HOME" + fi + + rowimg="${rowimg}" + + rowtxt="${rowtxt}${file}" + +done + +if [ "$rowtxt" != "" ]; then + output="$output $rowimg" + output="$output $rowtxt" + rowimg="" + rowtxt="" +fi + +ifile=0 +for file in *; do + fullpath="$PWD/$file" + if [ -d "$file" ]; then #directory + filetype="directory" + fileicon="/static/images/tango-folder.png" + clickcmd="cd %(path); $glscmd -f" + elif [ -x "$file" ]; then #executable + filetype="executable" + fileicon="/static/images/tango-application-x-executable.png" + clickcmd="" + else #plain file + filetype="plainfile" + fileicon="/static/images/tango-text-x-generic.png" + clickcmd="$gvicmd" + fi + + rowimg="${rowimg}" + + rowtxt="${rowtxt}${file}" + + (( ifile++ )) + + if [ $(( ifile % ncols )) -eq 0 ]; then + output="$output $rowimg" + output="$output $rowtxt" + rowimg="" + rowtxt="" + fi + +done + +output="$output $rowimg" +output="$output $rowtxt" + +headers='{"content_type": "text/html", "x_gterm_response": "'"${response_type}"'", "x_gterm_parameters": {"scroll": "top", "current_directory": "'"${PWD}"'"}}' + +esc=`printf "\033"` +nl=`printf "\012"` +graphterm_code="1155" +$echocmd1 "${esc}[?${graphterm_code};${GRAPHTERM_COOKIE}h" + +$echocmd1 "$headers" +$echocmd2 "" +$echocmd2 "" + +$echocmd2 '' +$echocmd2 "" + +$echocmd2 $output +$echocmd2 '
' +$echocmd1 "${esc}[?${graphterm_code}l" +echo diff --git a/graphterm/bin/gopen b/graphterm/bin/gopen new file mode 100755 index 0000000..9b88bf7 --- /dev/null +++ b/graphterm/bin/gopen @@ -0,0 +1,19 @@ +#!/bin/bash +# gopen: open a file +# Usage: gopen + +if [ $# -eq 0 ]; then + echo "Usage: gopen " + exit 1 +fi + +if which open > /dev/null; then + open "$1" +elif which xdg-open > /dev/null; then + xdg-open "$1" &> /dev/null & +elif which gnome-open > /dev/null; then + gnome-open "$1" +else + echo "No open command found!" + exit 1 +fi diff --git a/graphterm/bin/gtermapi.py b/graphterm/bin/gtermapi.py new file mode 100755 index 0000000..e3de722 --- /dev/null +++ b/graphterm/bin/gtermapi.py @@ -0,0 +1,59 @@ +""" +gtermapi: Common code gterm-aware programs +""" + +import os +import random + +Lterm_cookie = os.getenv("GRAPHTERM_COOKIE", "") +Html_escapes = ["\x1b[?1155;%sh" % Lterm_cookie, + "\x1b[?1155l"] + +def wrap(html): + return Html_escapes[0] + html + Html_escapes[1] + +Form_template = """
%s %s + +
""" + +Input_text_template = """%s""" + +Select_template = """%s""" +Select_option_template = """""" + +def create_input_html(id_suffix, arg_list): + input_list = [] + first_arg = True + for opt_name, opt_default, opt_help in arg_list: + if isinstance(opt_default, basestring): + opt_label = "" if opt_name.startswith("arg") else (opt_name+": ") + extras = ' autofocus="autofocus"' if first_arg else "" + + input_list.append(Input_text_template % (opt_help, opt_label, id_suffix, opt_name, opt_name, extras)) + elif isinstance(opt_default, (list, tuple)): + opt_list = [] + opt_sel = "selected" + for opt_value in opt_default: + opt_list.append(Select_option_template % (opt_value, opt_sel, opt_value or "Select...")) + opt_sel = "" + input_list.append(Select_template % (opt_help, opt_name+": ", id_suffix, opt_name, opt_name, + "\n".join(opt_list))) + first_arg = False + + return "\n".join(input_list) + +def create_form(id_suffix, arg_list, title=""): + opt_names = ",".join(x[0] for x in arg_list) + input_html = create_input_html(id_suffix, arg_list) + return Form_template % (id_suffix, title, input_html, id_suffix, "ec2launch -f", opt_names) + +def add_options(parser, arg_list, title=""): + """Returns form html, after adding options""" + for opt_name, opt_default, opt_help in arg_list: + default= opt_default[0] if isinstance(opt_default, (list, tuple)) else opt_default + parser.add_option("", "--"+opt_name, dest=opt_name, default=default, + help=opt_help) + id_suffix = "1%09d" % random.randrange(0, 10**9) + return create_form(id_suffix, arg_list, title=title) diff --git a/graphterm/bin/gvi b/graphterm/bin/gvi new file mode 100755 index 0000000..5cc66d7 --- /dev/null +++ b/graphterm/bin/gvi @@ -0,0 +1,48 @@ +#!/bin/bash +# gvi: a GraphTerm shell wrapper for editing files +# Usage: gvi + +options="" +file="" +for arg in $*; do + if [[ "$arg" == -* ]]; then + options="$options $arg" + else + file="$arg" + fi +done + +if [ "$file" == "" ]; then + echo "Usage: gvi " + exit 1 +fi + +if [[ "$file" == /* ]]; then + # Absolute path + file="$file" +else + # Relative path + file="$PWD/$file" +fi + +tailname=$(basename "$file") +filename="${tailname%.*}" +extension="${tailname##*.}" + +echocmd1="echo -n" +##echocmd1="/bin/echo -e" +echocmd2="echo" + +headers='{"content_type": "text/html", "x_gterm_response": "edit_file", "x_gterm_parameters": {"filepath": "'"${file}"'", "editor": "", "modify": "true", "command": "", "current_directory": "'"${PWD}"'"}}' + +esc=`printf "\033"` +nl=`printf "\012"` +cr=`printf "\015"` +graphterm_code="1155" +$echocmd1 "${esc}[?${graphterm_code};${GRAPHTERM_COOKIE}h" + +$echocmd1 "$headers" +$echocmd2 "" +$echocmd2 "" +$echocmd1 "${esc}[?${graphterm_code}l" +echo diff --git a/graphterm/bin/gweather b/graphterm/bin/gweather new file mode 100755 index 0000000..4747ba6 --- /dev/null +++ b/graphterm/bin/gweather @@ -0,0 +1,114 @@ +#!/usr/bin/env python +# + +""" +gweather: Display weather using Google Weather API +""" + +import json +import os +import random +import sys +import xml.dom.minidom +from optparse import OptionParser + +try: + from urllib.request import urlopen + from urllib.parse import urlencode +except ImportError: + from urllib import urlopen, urlencode + +Lterm_cookie = os.getenv("GRAPHTERM_COOKIE", "") +Html_escapes = ["\x1b[?1155;%sh" % Lterm_cookie, + "\x1b[?1155l"] + +def wrap(html): + return Html_escapes[0] + html + Html_escapes[1] + +Google_weather_url = "http://www.google.com/ig/api?" +Google_img_url = "http://www.google.com/ig/images/weather" + +title_template = """ +Current weather in %(city)s +""" +cur_template = """ +%(condition)s %(temp_f)s °F, %(condition)s +Forecast +""" + +fcst_template = """ +%(condition)s %(day_of_week)s: %(condition)s, Low %(low)s °F, High %(high)s °F +""" + +form_template = """
Please specify location for weather info: + + + +
""" + +usage = "usage: %prog [-f] " +parser = OptionParser(usage=usage) +parser.add_option("-f", "--fullscreen", + action="store_true", dest="fullscreen", default=False, + help="Fullscreen display") +parser.add_option("-t", "--text", + action="store_true", dest="text", default=False, + help="Plain text display") + +(options, args) = parser.parse_args() +location = " ".join(args) + +headers = {"content_type": "text/html"} +headers["x_gterm_response"] = "pagelet_fullscreen" +headers["x_gterm_parameters"] = {"scroll": "top", "current_directory": os.getcwd()} + +if not location: + if not Lterm_cookie or options.text: + print >> sys.stderr, "Please specify location" + sys.exit(1) + random_id = "1%09d" % random.randrange(0, 10**9) + form_html = form_template % (random_id, random_id) + headers["x_gterm_response"] = "pagelet_form" + print wrap(json.dumps(headers)+"\n\n"+form_html) + sys.exit(1) + +def xml2dict(root_elem, schema): + retval = {} + for key, value in schema.iteritems(): + lst = [] + retval[key] = lst + for elem in dom.documentElement.getElementsByTagName(key): + if isinstance(value, dict): + vals = xml2dict(elem, value) + else: + vals = {} + for key2 in value: + vals[key2] = elem.getElementsByTagName(key2)[0].getAttribute("data") + lst.append(vals) + return retval + +schema = {"forecast_information": ["city"], + "current_conditions": ["condition", "humidity", "icon", "temp_c", "temp_f", "wind_condition"], + "forecast_conditions": ["condition", "day_of_week", "low", "icon", "high"], + } + +url = Google_weather_url + urlencode({"weather": location}) + +weather_xml = urlopen(url).read() + +dom = xml.dom.minidom.parseString(weather_xml) + +weather_dict = xml2dict(dom, schema) + +html = title_template % weather_dict["forecast_information"][0] + cur_template % weather_dict["current_conditions"][0] +for fcst in weather_dict["forecast_conditions"]: + html += fcst_template % fcst + +html += "(Using the Google Weather API)" + +if not Lterm_cookie or options.text: + import lxml.html + sys.stdout.write(lxml.html.fromstring(html).text_content()) +else: + sys.stdout.write(wrap(json.dumps(headers)+"\r\n\r\n"+html)) + diff --git a/graphterm/bin/port_forward b/graphterm/bin/port_forward new file mode 100644 index 0000000..3569e10 --- /dev/null +++ b/graphterm/bin/port_forward @@ -0,0 +1,10 @@ +#!/bin/bash +# Forward specified port to 8900 +# + +if [ $# -ne 1 ]; then + echo "Usage: port_forward <80|443>" + exit 1 +fi + +iptables -t nat -A PREROUTING -p tcp --dport $1 -j REDIRECT --to 8900 diff --git a/graphterm/certs/cert_to_p12.csh b/graphterm/certs/cert_to_p12.csh new file mode 100755 index 0000000..9abe092 --- /dev/null +++ b/graphterm/certs/cert_to_p12.csh @@ -0,0 +1,17 @@ +#!/bin/csh + +if ( $#argv < 2 ) then + echo "Usage: cert_to_p12.csh " + exit 1 +endif + +set password=password +if ( $# > 2 ) then + set password=$3 +endif + +set name=$1:t +set name=$name:r + +echo openssl pkcs12 -export -in $1 -inkey $2 -out $name.p12 -passout pass:$password +openssl pkcs12 -export -in $1 -inkey $2 -out $name.p12 -passout pass:$password diff --git a/graphterm/certs/create_client_cert.csh b/graphterm/certs/create_client_cert.csh new file mode 100755 index 0000000..0f85f05 --- /dev/null +++ b/graphterm/certs/create_client_cert.csh @@ -0,0 +1,38 @@ +#!/bin/csh + +if ( $#argv < 2 ) then + echo "Usage: create_client_cert.csh [ []]" + exit 1 +endif + +set clientorg=GraphTerm +if ( $# > 2 ) then + set clientorg="$3" +endif + +set password="" +if ( $# > 3 ) then + set password="$4" +endif + +set certfile=$1 +set certprefix=$certfile:r + +set clientname=$2 +set clientprefix="${certprefix:t}-$clientname" + +set expdays=1024 + +echo openssl genrsa -out $clientprefix.key 1024 +openssl genrsa -out $clientprefix.key 1024 + +echo openssl req -new -key $clientprefix.key -out $clientprefix.csr -batch -subj "/O=$clientorg/CN=$clientname" +openssl req -new -key $clientprefix.key -out $clientprefix.csr -batch -subj "/O=$clientorg/CN=$clientname" + +echo openssl x509 -req -days $expdays -in $clientprefix.csr -CA $certprefix.crt -CAkey $certprefix.key -set_serial 01 -out $clientprefix.crt +openssl x509 -req -days $expdays -in $clientprefix.csr -CA $certprefix.crt -CAkey $certprefix.key -set_serial 01 -out $clientprefix.crt + +echo openssl pkcs12 -export -in $clientprefix.crt -inkey $clientprefix.key -out $clientprefix.p12 -passout pass:$password +openssl pkcs12 -export -in $clientprefix.crt -inkey $clientprefix.key -out $clientprefix.p12 -passout pass:$password + +echo "Created $clientprefix.key, $clientprefix.crt, $clientprefix.p12" diff --git a/graphterm/certs/create_server_cert.csh b/graphterm/certs/create_server_cert.csh new file mode 100755 index 0000000..45d6344 --- /dev/null +++ b/graphterm/certs/create_server_cert.csh @@ -0,0 +1,34 @@ +#!/bin/csh + +if ( $#argv < 1 ) then + echo "Usage: create_server_cert.csh [ []]" + exit 1 +endif + +set hostname=$1 + +set serverorg=GraphTerm +if ( $# > 1 ) then + set serverorg="$3" +endif + +set password="" +if ( $# > 2 ) then + set password="$3" +endif + +set expdays=1024 + +echo openssl genrsa -out $hostname.key 1024 +openssl genrsa -out $hostname.key 1024 + +echo openssl req -new -key $hostname.key -out $hostname.csr -batch -subj "/O=$serverorg/CN=$hostname" +openssl req -new -key $hostname.key -out $hostname.csr -batch -subj "/O=$serverorg/CN=$hostname" + +echo openssl x509 -req -days $expdays -in $hostname.csr -signkey $hostname.key -out $hostname.crt +openssl x509 -req -days $expdays -in $hostname.csr -signkey $hostname.key -out $hostname.crt + +echo openssl x509 -noout -fingerprint -in $hostname.crt +openssl x509 -noout -fingerprint -in $hostname.crt + +echo "Created $hostname.key, $hostname.crt" diff --git a/graphterm/certs/mac_import_cert.sh b/graphterm/certs/mac_import_cert.sh new file mode 100755 index 0000000..1ac3b01 --- /dev/null +++ b/graphterm/certs/mac_import_cert.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +if [ $# -eq 0 ]; then + echo "Usage: mac_import_cert.sh [clientname]" + exit 1 +fi + +certfile=$1 +clientname=gterm-local +if [ $# -gt 1 ]; then + clientname=$2 +fi + +keychain=$HOME/Library/Keychains/login.keychain + +if security find-certificate -c $clientname; then + security delete-certificate -c $clientname +fi + +echo security import $certfile -k $keychain -P password +security import $certfile -k $keychain -P password diff --git a/graphterm/daemon.py b/graphterm/daemon.py new file mode 100755 index 0000000..7d74f50 --- /dev/null +++ b/graphterm/daemon.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python + +# A simple unix/linux daemon in Python +# by Sander Marechal +# http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/ +# (Public domain) + +import sys, os, time, atexit +from signal import SIGTERM + +class Daemon(object): + """ + A generic daemon class. + + Usage: subclass the Daemon class and override the run() method + """ + def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + self.pidfile = pidfile + + def daemonize(self): + """ + do the UNIX double-fork magic, see Stevens' "Advanced + Programming in the UNIX Environment" for details (ISBN 0201563177) + http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 + """ + try: + pid = os.fork() + if pid > 0: + # exit first parent + sys.exit(0) + except OSError, e: + sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror)) + sys.exit(1) + + # decouple from parent environment + os.chdir("/") + os.setsid() + os.umask(0) + + # do second fork + try: + pid = os.fork() + if pid > 0: + # exit from second parent + sys.exit(0) + except OSError, e: + sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror)) + sys.exit(1) + + # redirect standard file descriptors + sys.stdout.flush() + sys.stderr.flush() + si = file(self.stdin, 'r') + so = file(self.stdout, 'a+') + se = file(self.stderr, 'a+', 0) + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + # write pidfile + atexit.register(self.delpid) + pid = str(os.getpid()) + file(self.pidfile,'w+').write("%s\n" % pid) + + def delpid(self): + os.remove(self.pidfile) + + def start(self): + """ + Start the daemon + """ + # Check for a pidfile to see if the daemon already runs + try: + pf = file(self.pidfile,'r') + pid = int(pf.read().strip()) + pf.close() + except IOError: + pid = None + + if pid: + message = "pidfile %s already exists. Daemon already running?\n" + sys.stderr.write(message % self.pidfile) + sys.exit(1) + + # Start the daemon + self.daemonize() + self.run() + + def stop(self): + """ + Stop the daemon + """ + # Get the pid from the pidfile + try: + pf = file(self.pidfile,'r') + pid = int(pf.read().strip()) + pf.close() + except IOError: + pid = None + + if not pid: + message = "pidfile %s does not exist. Daemon not running?\n" + sys.stderr.write(message % self.pidfile) + return # not an error in a restart + + # Try killing the daemon process + try: + while 1: + os.kill(pid, SIGTERM) + time.sleep(0.1) + except OSError, err: + err = str(err) + if err.find("No such process") > 0: + if os.path.exists(self.pidfile): + os.remove(self.pidfile) + else: + print str(err) + sys.exit(1) + + def restart(self): + """ + Restart the daemon + """ + self.stop() + self.start() + + def run(self): + """ + You should override this method when you subclass Daemon. It will be called after the process has been + daemonized by start() or restart(). + """ + +class ServerDaemon(Daemon): + def __init__(self, pidfile, run_function): + self.run_function = run_function + super(ServerDaemon, self).__init__(pidfile) + + def run(self): + self.run_function() + + def daemon_run(self, action): + if action == "start": + self.start() + elif action == "stop": + self.stop() + elif action == "restart": + self.restart() + elif action == "status": + status = "Running" if os.path.exists(self.pidfile) else "Stopped" + print >> sys.stderr, status + else: + print >> sys.stderr, "Daemon option must be one of start/stop/restart/status - ", action + sys.exit(1) diff --git a/graphterm/episode4.txt b/graphterm/episode4.txt new file mode 100644 index 0000000..a1b379d --- /dev/null +++ b/graphterm/episode4.txt @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Episode IV + A NEW HOPE + +It is a period of civil war. Rebel +spaceships, striking from a +hidden base, have won their +first victory against the evil +Galactic Empire. + +During the battle, Rebel spies +managed to steal secret plans to +the Empire's ultimate weapon, +the DEATH STAR, an armored +space station with enough +power to destroy an entire +planet. + +Pursued by the Empire's sinister +agents, Princess Leia races +home aboard her starship, +custodian of the stolen plans +that can save her people and +restore freedom to the galaxy.... + + diff --git a/graphterm/gterm.py b/graphterm/gterm.py new file mode 100755 index 0000000..45f2606 --- /dev/null +++ b/graphterm/gterm.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python + +"""gterm: GraphTerm client launcher +""" + +import hashlib +import hmac +import logging +import os +import Queue +import random +import subprocess +import sys +import threading + +import tornado.httpclient + +Http_addr = "localhost" +Http_port = 8900 + +App_dir = os.path.join(os.getenv("HOME"), ".graphterm") +Gterm_secret_file = os.path.join(App_dir, "graphterm_secret") + +def command_output(command_args, **kwargs): + """ Executes a command and returns the string tuple (stdout, stderr) + keyword argument timeout can be specified to time out command (defaults to 1 sec) + """ + timeout = kwargs.pop("timeout", 1) + def command_output_aux(): + try: + proc = subprocess.Popen(command_args, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + return proc.communicate() + except Exception, excp: + return "", str(excp) + if not timeout: + return command_output_aux() + + exec_queue = Queue.Queue() + def execute_in_thread(): + exec_queue.put(command_output_aux()) + thrd = threading.Thread(target=execute_in_thread) + thrd.start() + try: + return exec_queue.get(block=True, timeout=timeout) + except Queue.Empty: + return "", "Timed out after %s seconds" % timeout + +def getuid(pid): + """Return uid of running process""" + command_args = ["lsof", "-a", "-p", str(pid), "-d", "cwd", "-Fu"] + std_out, std_err = command_output(command_args, timeout=1) + if std_err: + logging.warning("getuid: ERROR %s", std_err) + return None + try: + return int(std_out.split("\n")[1][1:]) + except Exception, excp: + logging.warning("getuid: ERROR %s", excp) + return None + +def auth_request(http_addr, http_port, nonce, timeout=None, client_auth=False, protocol="http"): + """Simulate user form submission by executing a HTTP request""" + + cert_dir = App_dir + server_name = "localhost" + client_prefix = server_name + "-gterm-local" + ca_certs = cert_dir+"/"+server_name+".crt" + + ssl_options = {} + if client_auth: + client_cert = cert_dir+"/"+client_prefix+".crt" + client_key = cert_dir+"/"+client_prefix+".key" + ssl_options.update(client_cert=client_cert, client_key=client_key) + + url = "%s://%s:%s/_auth/?nonce=%s" % (protocol, http_addr, http_port, nonce) + request = tornado.httpclient.HTTPRequest(url, validate_cert=True, ca_certs=ca_certs, + **ssl_options) + http_client = tornado.httpclient.HTTPClient() + try: + response = http_client.fetch(request) + if response.error: + print >> sys.stderr, "HTTPClient ERROR response.error ", response.error + return None + return response.body + except tornado.httpclient.HTTPError, excp: + print >> sys.stderr, "HTTPClient ERROR ", excp + return None + +def auth_token(secret, connection_id, client_nonce, server_nonce): + """Return (client_token, server_token)""" + SIGN_SEP = "|" + prefix = SIGN_SEP.join([connection_id, client_nonce, server_nonce]) + SIGN_SEP + return [hmac.new(str(secret), prefix+conn_type, digestmod=hashlib.sha256).hexdigest()[:24] for conn_type in ("client", "server")] + +def main(): + global Http_addr, Http_port + from optparse import OptionParser + usage = "usage: gterm [-h ... options]" + parser = OptionParser(usage=usage) + + parser.add_option("", "--https", dest="https", action="store_true", + help="Use SSL (TLS) connections for security") + parser.add_option("", "--server_auth", dest="server_auth", action="store_true", + help="Authenticate server before opening gterm window") + parser.add_option("", "--client_cert", dest="client_cert", default="", + help="Path to client CA cert (or '.')") + parser.add_option("", "--term_type", dest="term_type", default="", + help="Terminal type (linux/screen/xterm) NOT YET IMPLEMENTED") + + (options, args) = parser.parse_args() + protocol = "https" if options.https else "http" + + if options.server_auth: + if not os.path.exists(Gterm_secret_file): + print >> sys.stderr, "gterm: Server not running (no secret file); use 'gtermserver' command to start it." + sys.exit(1) + + try: + with open(Gterm_secret_file) as f: + Http_port, Gterm_pid, Gterm_secret = f.read().split() + Http_port = int(Http_port) + Gterm_pid = int(Gterm_pid) + except Exception, excp: + print >> sys.stderr, "gterm: Error in reading %s: %s" % (Gterm_secret_file, excp) + sys.exit(1) + + if os.getuid() != getuid(Gterm_pid): + print >> sys.stderr, "gterm: Server not running (invalid pid); use 'gtermserver' command to start it." + sys.exit(1) + + client_nonce = "1%018d" % random.randrange(0, 10**18) # 1 prefix to keep leading zeros when stringified + + resp = auth_request(Http_addr, Http_port, client_nonce, protocol=protocol) + if not resp: + print >> sys.stderr, "gterm: Auth HTTTP Request failed" + sys.exit(1) + + server_nonce, received_token = resp.split(":") + client_token, server_token = auth_token(Gterm_secret, "graphterm", client_nonce, server_nonce) + if received_token != client_token: + print >> sys.stderr, "gterm: Server failed to authenticate itself" + sys.exit(1) + + # TODO: Send server token to server in URL to authenticate + ##print >> sys.stderr, "**********snonce", server_nonce, client_token, server_token + + url = "%s://%s:%d" % (protocol, Http_addr, Http_port) + if sys.platform.startswith("linux"): + command_args = ["xdg-open", url] + else: + command_args = ["open", url] + + std_out, std_err = command_output(command_args, timeout=5) + if std_err: + print >> sys.stderr, "gterm: ERROR in opening browser window '%s' - %s\n Check if server is running. If not, start it with 'gtermserver' command." % (" ".join(command_args), std_err) + sys.exit(1) + + # TODO: Create minimal browser window (without URL box etc.) + # by searching directly for browser executables, or using open, xdg-open, or gnome-open + # For security, closing websocket should close out (or at least about:blank) the terminal window + # (to prevent reconnecting to malicious server) + +if __name__ == "__main__": + main() diff --git a/graphterm/gterm_setup.py b/graphterm/gterm_setup.py new file mode 100755 index 0000000..8cc41b9 --- /dev/null +++ b/graphterm/gterm_setup.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +import os, sys + +BINDIR = "bin" +Exec_path = os.path.join(os.path.dirname(__file__), BINDIR) + +def setup_bindir(): + print >> sys.stderr, "Configuring", Exec_path + for dirpath, dirnames, filenames in os.walk(Exec_path): + for filename in filenames: + if not filename.startswith(".") and not filename.endswith(".pyc"): + os.chmod(os.path.join(dirpath, filename), 0555) + +def main(): + setup_bindir() + +if __name__ == "__main__": + main() diff --git a/graphterm/gtermhost.py b/graphterm/gtermhost.py new file mode 100755 index 0000000..a76166a --- /dev/null +++ b/graphterm/gtermhost.py @@ -0,0 +1,387 @@ +#!/usr/bin/env python + +"""gtermhost: GraphTerm host connector +""" + +import cgi +import functools +import logging +import otrace +import os +import random +import signal +import sys +import threading +import time +import urllib + +import lineterm +import packetserver + +RETRY_SEC = 15 + +OSHELL_NAME = "osh" + +##SHELL_CMD = "bash -l" +SHELL_CMD = "/bin/bash -l" + +# Short prompt (long prompt with directory metadata fills most of row) +##PROMPT_PREFIX = '' # Unique prompt prefix +PROMPT_PREFIX = '' # No unique prefix necessary for bash (using PROMPT_COMMAND) +PROMPT_SUFFIX = '$' +SHELL_PROMPT = [PROMPT_PREFIX, '\W', PROMPT_SUFFIX] + +HTML_ESCAPES = ["\x1b[?1155;", "h", + "\x1b[?1155l"] + +class HtmlWrapper(object): + """ Wrapper for HTML output + """ + def __init__(self, lterm_cookie): + self.lterm_cookie = lterm_cookie + + def wrap(self, html, msg_type=""): + return HTML_ESCAPES[0] + self.lterm_cookie + HTML_ESCAPES[1] + html + HTML_ESCAPES[-1] + +class TerminalClient(packetserver.RPCLink, packetserver.PacketClient): + _all_connections = {} + first_terminal = [True] + def __init__(self, host, port, command=SHELL_CMD, lterm_cookie="", io_loop=None, ssl_options={}, + term_type="", lterm_logfile=""): + super(TerminalClient, self).__init__(host, port, io_loop=io_loop, + ssl_options=ssl_options, max_packet_buf=3, + reconnect_sec=RETRY_SEC, server_type="frame") + self.term_type = term_type + self.lterm_cookie = lterm_cookie + self.lterm_logfile = lterm_logfile + self.command = command + self.terms = {} + self.lineterm = None + + def shutdown(self): + print >> sys.stderr, "Shutting down client connection %s -> %s:%s" % (self.connection_id, self.host, self.port) + if self.lineterm: + self.lineterm.shutdown() + self.lineterm = None + super(TerminalClient, self).shutdown() + + def add_oshell(self): + self.add_term(OSHELL_NAME, 0, 0) + + def add_term(self, term_name, height, width): + if term_name not in self.terms: + self.send_request_threadsafe("terminal_update", term_name, True) + self.terms[term_name] = (height, width) + + def remove_term(self, term_name): + try: + del self.terms[term_name] + except Exception: + pass + self.send_request_threadsafe("terminal_update", term_name, False) + + if not self.lineterm: + return + if not term_name: + self.lineterm.kill_all() + else: + self.lineterm.kill_term(term_name) + + def xterm(self, term_name="", height=25, width=80, command=SHELL_CMD): + self.first_terminal[0] = False + if not self.lineterm: + self.lineterm = lineterm.Multiplex(self.screen_callback, command=command, + cookie=self.lterm_cookie, prompt=SHELL_PROMPT, + term_type=self.term_type, logfile=self.lterm_logfile) + term_name = self.lineterm.terminal(term_name, height=height, width=width) + self.add_term(term_name, height, width) + return term_name + + def screen_callback(self, term_name, command, arg): + # Invoked in lineterm thread; schedule callback in ioloop + self.send_request_threadsafe("response", term_name, [["terminal", command, arg]]) + + def remote_response(self, term_name, message_list): + self.send_request_threadsafe("response", term_name, message_list) + + def remote_request(self, term_name, req_list): + """ + Setup commands: + reconnect + + Input commands: + incomplete_input + input + open_terminal + click_paste {command:, clear_last:, normalize:, enter:} + get_finder + save_file + + Output commands: + completed_input + prompt + stdin + stdout + stderr + """ + try: + resp_list = [] + for cmd in req_list: + action = cmd.pop(0) + + if action == "reconnect": + if self.lineterm: + self.lineterm.reconnect(term_name) + + elif action == "open_terminal": + if self.lineterm: + name = self.lineterm.terminal(cmd[0][0], command=cmd[0][1]) + self.remote_response(term_name, [["open", name, ""]]) + + elif action == "set_size": + if term_name != OSHELL_NAME: + self.xterm(term_name, cmd[0][0], cmd[0][1]) + + elif action == "kill_term": + self.remove_term(term_name) + + elif action == "keypress": + if self.lineterm: + self.lineterm.term_write(term_name, str(cmd[0])) + + elif action == "save_file": + if self.lineterm: + self.lineterm.save_file(term_name, cmd[0], cmd[1]) + + elif action == "click_paste": + # click_paste: text, file_uri, {command:, clear_last:, normalize:, enter:} + if self.lineterm: + self.lineterm.click_paste(term_name, cmd[0], cmd[1], cmd[2]) + + elif action == "clear_last_entry": + if self.lineterm: + self.lineterm.clear_last_entry(term_name, long(cmd[0])) + + elif action == "get_finder": + if self.lineterm: + self.lineterm.get_finder(term_name, cmd[0], cmd[1]) + + elif action == "incomplete_input": + cmd_incomplete = str(cmd[0]) + dummy, sep, text = cmd_incomplete.rpartition(" ") + options = otrace.OShell.instance.completer(text, 0, line=cmd_incomplete, all=True) + if text: + options = [cmd_incomplete[:-len(text)]+option for option in options] + else: + options = [cmd_incomplete+option for option in options] + resp_list.append(["completed_input", options]) # Not escaped; handle as text + + elif action == "input": + cmd_input = str(cmd[0]).lstrip() # Unescaped text + here_doc = cmd[1] + entry_list = [] + + if cmd_input == "cat episode4.txt": + # Easter egg + std_out, std_err = Episode4, "" + else: + std_out, std_err = otrace.OShell.instance.execute(cmd_input, here_doc=here_doc) + resp_list.append(["input", cmd[0]]) # Not escaped; handle as text + + prompt, cur_dir_path = otrace.OShell.instance.get_prompt() + resp_list.append(["prompt", cgi.escape(prompt), "file://"+urllib.quote(cur_dir_path)]) + + auth_html = False + if self.lterm_cookie and std_out.startswith(HTML_ESCAPES[0]): + auth_prefix = HTML_ESCAPES[0]+self.lterm_cookie+HTML_ESCAPES[1] + auth_html = std_out.startswith(auth_prefix) + if auth_html: + offset = len(auth_prefix) + else: + # Unauthenticated html + offset = std_out.find(HTML_ESCAPES[1])+len(HTML_ESCAPES[1]) + + if std_out.endswith(HTML_ESCAPES[-1]): + html_output = std_out[offset:-len(HTML_ESCAPES[-1])] + elif std_out.endswith(HTML_ESCAPES[-1]+"\n"): + html_output = std_out[offset:-len(HTML_ESCAPES[-1])-1] + else: + html_output = std_out[offset:] + + headers, content = lineterm.parse_headers(html_output) + + if auth_html: + resp_list.append(["html_output", content]) + else: + # Unauthenticated; extract plain text from html + try: + import lxml.html + std_out = lxml.html.fromstring(content).text_content() + except Exception: + std_out = content + + if not auth_html: + entry_list.append('
')
+                        if std_out and std_out != "_NoPrompt_":
+                            entry_list.append('%s' % cgi.escape(std_out))
+                        if std_err:
+                            entry_list.append('%s' % cgi.escape(std_err))
+                        entry_list.append('
') + resp_list.append(["output", "\n".join(entry_list)]) + + elif action == "errmsg": + logging.warning("remote_request: ERROR %s", cmd[0]) + else: + raise Exception("Invalid action: "+action) + self.remote_response(term_name, resp_list); + except Exception, excp: + import traceback + errmsg = "%s\n%s" % (excp, traceback.format_exc()) + print >> sys.stderr, "TerminalClient.remote_request: "+errmsg + self.remote_response(term_name, [["errmsg", errmsg]]) + ##self.shutdown() + + +class GTCallbackMixin(object): + """ GT callback implementation + """ + oshell_client = None + def set_client(self, oshell_client): + self.oshell_client = oshell_client + + def logmessage(self, log_level, msg, exc_info=None, logtype="", plaintext=""): + # If log_level is None, always display message + if self.oshell_client and (log_level is None or log_level >= self.log_level): + self.oshell_client.remote_response(OSHELL_NAME, [["log", logtype, log_level, msg]]) + + if logtype or log_level is None: + sys.stderr.write((plaintext or msg)+"\n") + + def editback(self, content, filepath="", filetype="", editor="", modify=False): + if editor and editor != "web": + return otrace.TraceCallback.editback(self, content, filepath=filepath, filetype=filetype, + editor=editor, modify=modify) + params = {"editor": editor, "modify": modify, "command": "edit -f "+filepath if modify else "", + "filepath": filepath, "filetype": filetype} + self.oshell_client.remote_response(OSHELL_NAME, [["edit", params, content]]) + return (None, None) + +if otrace: + class GTCallback(GTCallbackMixin, otrace.TraceCallback): + pass +else: + class GTCallback(GTCallbackMixin): + pass + +Lterm_cookie = None +def gterm_shutdown(trace_shell=None): + TerminalClient.shutdown_all() + if trace_shell: + trace_shell.close() + +def gterm_connect(host_name, server_addr, server_port=8899, shell_cmd=SHELL_CMD, connect_kw={}, + oshell_globals=None, oshell_unsafe=False, oshell_workdir="", oshell_init=""): + """ Returns (host_connection, lterm_cookie, trace_shell) + """ + lterm_cookie = "1%015d" % random.randrange(0, 10**15) # 1 prefix to keep leading zeros when stringified + + host_connection = TerminalClient.get_client(host_name, + connect=(server_addr, server_port, shell_cmd, lterm_cookie), + connect_kw=connect_kw) + + if oshell_globals: + host_connection.add_oshell() + gterm_callback = GTCallback() + gterm_callback.set_client(host_connection) + otrace.OTrace.setup(callback_handler=gterm_callback) + otrace.OTrace.html_wrapper = HtmlWrapper(lterm_cookie) + trace_shell = otrace.OShell(locals_dict=oshell_globals, globals_dict=oshell_globals, + allow_unsafe=oshell_unsafe, work_dir=oshell_workdir, + add_env={"GRAPHTERM_COOKIE": lterm_cookie}, init_file=oshell_init) + else: + trace_shell = None + + return (host_connection, lterm_cookie, trace_shell) + +def run_host(options, args): + global IO_loop, Gterm_host, Lterm_cookie, Trace_shell, Xterm, Killterm + import tornado.ioloop + server_addr = args[0] + host_name = args[1] + protocol = "https" if options.https else "http" + + oshell_globals = globals() if options.oshell else None + Gterm_host, Lterm_cookie, Trace_shell = gterm_connect(host_name, server_addr, + server_port=options.server_port, + oshell_globals=oshell_globals, + oshell_unsafe=True) + Xterm = Gterm_host.xterm + Killterm = Gterm_host.remove_term + + def host_shutdown(): + gterm_shutdown(Trace_shell) + IO_loop.stop() + + def sigterm(signal, frame): + logging.warning("SIGTERM signal received") + host_shutdown() + signal.signal(signal.SIGTERM, sigterm) + + IO_loop = tornado.ioloop.IOLoop.instance() + try: + if not Trace_shell: + IO_loop.start() + else: + ioloop_thread = threading.Thread(target=IO_loop.start) + ioloop_thread.start() + time.sleep(1) # Time to start thread + + print >> sys.stderr, "\nType ^D^C to exit" + Trace_shell.loop() + except KeyboardInterrupt: + print >> sys.stderr, "Interrupted" + + finally: + try: + pass + except Exception: + pass + + if Trace_shell: + IO_loop.add_callback(host_shutdown) + else: + host_shutdown() + +def main(): + from optparse import OptionParser + usage = "usage: gtermhost [-h ... options] " + parser = OptionParser(usage=usage) + + parser.add_option("", "--server_port", dest="server_port", default=8899, + help="server port (default: 8899)", type="int") + + parser.add_option("", "--oshell", dest="oshell", action="store_true", + help="Activate otrace/oshell") + + parser.add_option("", "--https", dest="https", action="store_true", + help="Use SSL (TLS) connections for security") + + + parser.add_option("", "--daemon", dest="daemon", default="", + help="daemon=start/stop/restart/status") + + (options, args) = parser.parse_args() + if len(args) != 2 and options.daemon != "stop": + print >> sys.stderr, usage + sys.exit(1) + + if not options.daemon: + run_host(options, args) + else: + from daemon import ServerDaemon + pidfile = "/tmp/gtermhost.pid" + daemon = ServerDaemon(pidfile, functools.partial(run_host, options, args)) + daemon.daemon_run(options.daemon) + +if __name__ == "__main__": + main() diff --git a/graphterm/gtermserver.py b/graphterm/gtermserver.py new file mode 100755 index 0000000..47985b7 --- /dev/null +++ b/graphterm/gtermserver.py @@ -0,0 +1,723 @@ +#!/usr/bin/env python + +"""gtermserver: WebSocket server for GraphTerm +""" + +import cgi +import collections +import functools +import hashlib +import hmac +import json +import logging +import os +import Queue +import random +import shlex +import ssl +import stat +import subprocess +import sys +import threading +import time +import traceback +import urllib +import urlparse +import uuid + +try: + import otrace +except ImportError: + otrace = None + +import gtermhost +import lineterm +import packetserver + +import tornado.httpserver +import tornado.ioloop +import tornado.web +import tornado.websocket + +try: + from collections import OrderedDict +except ImportError: + from ordereddict import OrderedDict + +App_dir = os.path.join(os.getenv("HOME"), ".graphterm") +File_dir = os.path.dirname(__file__) +if File_dir == ".": + File_dir = os.getcwd() # Need this for daemonizing to work? + +Doc_rootdir = os.path.join(File_dir, "www") +Default_auth_file = os.path.join(App_dir, "graphterm_auth") +Gterm_secret_file = os.path.join(App_dir, "graphterm_secret") + +Gterm_secret = "1%018d" % random.randrange(0, 10**18) # 1 prefix to keep leading zeros when stringified + +MAX_COOKIE_STATES = 100 +MAX_WEBCASTS = 500 + +COOKIE_NAME = "GRAPHTERM_AUTH" +COOKIE_TIMEOUT = 10800 + +HEX_DIGITS = 16 + +PROTOCOL = "http" + +SUPER_USERS = set(["root"]) +LOCAL_HOST = "local" + +First_terminal = True + +def cgi_escape(s): + return cgi.escape(s) if s else "" + +Episode4 = """ + + + Episode IV + A NEW HOPE + +It is a period of civil war. Rebel +spaceships, striking from a +hidden base, have won their +first victory against the evil +Galactic Empire. + +During the battle, Rebel spies +managed to steal secret plans to +the Empire's ultimate weapon, +the DEATH STAR, an armored +space station with enough +power to destroy an entire +planet. + +Pursued by the Empire's sinister +agents, Princess Leia races +home aboard her starship, +custodian of the stolen plans +that can save her people and +restore freedom to the galaxy.... + +""" + +def command_output(command_args, **kwargs): + """ Executes a command and returns the string tuple (stdout, stderr) + keyword argument timeout can be specified to time out command (defaults to 15 sec) + """ + timeout = kwargs.pop("timeout", 15) + def command_output_aux(): + try: + proc = subprocess.Popen(command_args, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, **kwargs) + return proc.communicate() + except Exception, excp: + return "", str(excp) + if not timeout: + return command_output_aux() + + exec_queue = Queue.Queue() + def execute_in_thread(): + exec_queue.put(command_output_aux()) + thrd = threading.Thread(target=execute_in_thread) + thrd.start() + try: + return exec_queue.get(block=True, timeout=timeout) + except Queue.Empty: + return "", "Timed out after %s seconds" % timeout + +server_cert_gen_cmds = [ + 'openssl genrsa -out %(hostname)s.key %(keysize)d', + 'openssl req -new -key %(hostname)s.key -out %(hostname)s.csr -batch -subj "/O=GraphTerm/CN=%(hostname)s"', + 'openssl x509 -req -days %(expdays)d -in %(hostname)s.csr -signkey %(hostname)s.key -out %(hostname)s.crt', + 'openssl x509 -noout -fingerprint -in %(hostname)s.crt', + ] + +client_cert_gen_cmds = [ + 'openssl genrsa -out %(clientprefix)s.key %(keysize)d', + 'openssl req -new -key %(clientprefix)s.key -out %(clientprefix)s.csr -batch -subj "/O=GraphTerm/CN=%(clientname)s"', + 'openssl x509 -req -days %(expdays)d -in %(clientprefix)s.csr -CA %(hostname)s.crt -CAkey %(hostname)s.key -set_serial 01 -out %(clientprefix)s.crt', + "openssl pkcs12 -export -in %(clientprefix)s.crt -inkey %(clientprefix)s.key -out %(clientprefix)s.p12 -passout pass:%(clientpassword)s" + ] + +def ssl_cert_gen(hostname="localhost", clientname="gterm-local", cwd=None, new=False): + """Return fingerprint of self-signed server certficate, creating a new one, if need be""" + params = {"hostname": hostname, "keysize": 1024, "expdays": 1024, + "clientname": clientname, "clientprefix":"%s-%s" % (hostname, clientname), + "clientpassword": "password",} + cmd_list = server_cert_gen_cmds if new else server_cert_gen_cmds[-1:] + for cmd in cmd_list: + cmd_args = shlex.split(cmd % params) + std_out, std_err = command_output(cmd_args, cwd=cwd, timeout=15) + if std_err: + logging.warning("gtermserver: SSL keygen %s %s", std_out, std_err) + fingerprint = std_out + if new: + for cmd in client_cert_gen_cmds: + cmd_args = shlex.split(cmd % params) + std_out, std_err = command_output(cmd_args, cwd=cwd, timeout=15) + if std_err: + logging.warning("gtermserver: SSL client keygen %s %s", std_out, std_err) + return fingerprint + +class GTSocket(tornado.websocket.WebSocketHandler): + _all_websockets = {} + _all_users = collections.defaultdict(dict) + _control_set = collections.defaultdict(set) + _watch_set = collections.defaultdict(set) + _counter = [0] + _webcast_paths = OrderedDict() + _cookie_states = OrderedDict() + _auth_users = OrderedDict() + _auth_code = uuid.uuid4().hex[:HEX_DIGITS] + + @classmethod + def get_auth_code(cls): + return cls._auth_code + + @classmethod + def set_auth_code(cls, value): + cls._auth_code = value + + def __init__(self, *args, **kwargs): + self.request = args[1] + try: + self.client_cert = self.request.get_ssl_certificate() + except Exception: + self.client_cert = "" + + self.common_name = "" + if self.client_cert: + try: + subject = dict([x[0] for x in self.client_cert["subject"]]) + self.common_name = subject.get("commonName") + except Exception, excp: + logging.warning("gtermserver: client_cert ERROR %s", excp) + logging.warning("gtermserver: client_cert=%s, name=%s", self.client_cert, self.common_name) + rpath, sep, self.request_query = self.request.uri.partition("?") + self.req_path = "/".join(rpath.split("/")[2:]) # Strip out /_websocket from path + if self.req_path.endswith("/"): + self.req_path = self.req_path[:-1] + + super(GTSocket, self).__init__(*args, **kwargs) + + self.authorized = None + self.remote_path = None + self.controller = False + + def allow_draft76(self): + return True + + def open(self): + need_code = bool(self._auth_code) + need_user = need_code ##bool(self._auth_users) + + user = "" + try: + if not self._auth_code: + self.authorized = {"user": "", "time": time.time(), "state_id": ""} + + elif COOKIE_NAME in self.request.cookies: + state_id = self.request.cookies[COOKIE_NAME].value + if state_id in self._cookie_states: + state_value = self._cookie_states[state_id] + if (time.time() - state_value["time"]) > COOKIE_TIMEOUT: + del self._cookie_states[state_id] + else: + self.authorized = state_value + + webcast_auth = False + if not self.authorized: + query_data = {} + if self.request_query: + try: + query_data = urlparse.parse_qs(self.request_query) + except Exception: + pass + + user = query_data.get("user", [""])[0] + code = query_data.get("code", [""])[0] + + expect_code = self._auth_code + if self._auth_users: + if user in self._auth_users: + expect_code = self._auth_users[user] + else: + self.write_json([["authenticate", need_user, need_code, "Invalid user" if user else ""]]) + self.close() + return + + if code == self._auth_code or code == expect_code: + state_id = uuid.uuid4().hex[:HEX_DIGITS] + self.authorized = {"user": user, "time": time.time(), "state_id": state_id} + if len(self._cookie_states) >= MAX_COOKIE_STATES: + self._cookie_states.pop(last=False) + self._cookie_states[state_id] = self.authorized + + elif self.req_path in self._webcast_paths: + self.authorized = {"user": user, "time": time.time(), "state_id": ""} + webcast_auth = True + + if not self.authorized: + self.write_json([["authenticate", need_user, need_code, "Authentication failed; retry"]]) + self.close() + return + + comps = self.req_path.split("/") + if len(comps) < 1 or not comps[0]: + host_list = TerminalConnection.get_connection_ids() + if self._auth_users and self.authorized["user"] not in SUPER_USERS: + if LOCAL_HOST in host_list: + host_list.remove(LOCAL_HOST) + host_list.sort() + self.write_json([["host_list", self.authorized["state_id"], host_list]]) + self.close() + return + + host = comps[0] + if host == LOCAL_HOST and self._auth_users and self.authorized["user"] not in SUPER_USERS: + self.write_json([["abort", "Local host access not allowed for user %s" % self.authorized["user"]]]) + self.close() + return + + conn = TerminalConnection.get_connection(host) + if not conn: + self.write_json([["abort", "Invalid host"]]) + self.close() + return + + if len(comps) < 2 or not comps[1]: + term_list = list(conn.term_set) + term_list.sort() + self.write_json([["term_list", self.authorized["state_id"], host, term_list]]) + self.close() + return + + term_name = comps[1] + if term_name == "new": + term_name = conn.remote_terminal_update() + self.write_json([["redirect", "/"+host+"/"+term_name]]) + self.close() + + path = host + "/" + term_name + + option = comps[2] if len(comps) > 2 else "" + + if option == "kill": + kill_remote(path) + return + + self.oshell = (term_name == gtermhost.OSHELL_NAME) + + self._counter[0] += 1 + self.websocket_id = self._counter[0] + + self._watch_set[path].add(self.websocket_id) + if webcast_auth: + self.controller = False + elif option == "share": + self.controller = True + self._control_set[path].add(self.websocket_id) + elif option == "steal": + self.controller = True + self._control_set[path] = set([self.websocket_id]) + elif option == "watch" or (path in self._control_set and self._control_set[path]): + self.controller = False + else: + self.controller = True + self._control_set[path] = set([self.websocket_id]) + + self.remote_path = path + self._all_websockets[self.websocket_id] = self + if user: + self._all_users[user][self.websocket_id] = self + + TerminalConnection.send_to_connection(host, "request", term_name, [["reconnect"]]) + + self.write_json([["setup", {"host": host, "term": term_name, "oshell": self.oshell, + "controller": self.controller, + "first_terminal": gtermhost.TerminalClient.first_terminal[0], + "state_id": self.authorized["state_id"]}]]) + except Exception, excp: + logging.warning("GTSocket.open: ERROR %s", excp) + self.close() + + def on_close(self): + if self.authorized: + user = self.authorized["user"] + if user: + user_sockets = self._all_users.get(user) + if user_sockets: + try: + del user_sockets[self.websocket_id] + except Exception: + pass + + if self.remote_path in self._control_set: + self._control_set[self.remote_path].discard(self.websocket_id) + if self.remote_path in self._watch_set: + self._watch_set[self.remote_path].discard(self.websocket_id) + try: + del self._all_websockets[self.websocket_id] + except Exception: + pass + + def write_json(self, data): + try: + self.write_message(json.dumps(data)) + except Exception, excp: + logging.error("write_json: ERROR %s", excp) + try: + # Close websocket on write error + self.close() + except Exception: + pass + + def on_message(self, message): + ##logging.warning("GTSocket.on_message: %s", message) + if not self.remote_path: + return + + controller = (self.remote_path in self._control_set and self.websocket_id in self._control_set[self.remote_path]) + + if not controller: + self.write_json([["errmsg", "ERROR: Remote path %s not under control" % self.remote_path]]) + return + + remote_host, term_name = self.remote_path.split("/") + conn = TerminalConnection.get_connection(remote_host) + if not conn: + self.write_json([["errmsg", "ERROR: Remote host %s not connected" % remote_host]]) + return + + req_list = [] + try: + msg_list = json.loads(message) + for msg in msg_list: + if msg[0] == "reconnect_host": + # Close host connection (should automatically reconnect) + conn.on_close() + + elif msg[0] == "webcast": + if controller: + # Only controller can webcast + if self.remote_path in self._webcast_paths: + del self._webcast_paths[self.remote_path] + if len(self._webcast_paths) > MAX_WEBCASTS: + self._webcast_paths.pop(last=False) + if msg[1]: + self._webcast_paths[self.remote_path] = time.time() + + else: + req_list.append(msg) + + TerminalConnection.send_to_connection(remote_host, "request", term_name, req_list) + except Exception, excp: + logging.warning("GTSocket.on_message: ERROR %s", excp) + self.write_json([["errmsg", str(excp)]]) + return + +def xterm(command="", name=None, host="localhost", port=8900): + """Create new terminal""" + pass + +def kill_remote(path): + if path == "*": + TerminalClient.shutdown_all() + return + host, term_name = path.split("/") + if term_name == "*": term_name = "" + try: + TerminalConnection.send_to_connection(host, "request", term_name, json.dumps([["kill_term"]])) + except Exception, excp: + pass + +class TerminalConnection(packetserver.RPCLink, packetserver.PacketConnection): + _all_connections = {} + def __init__(self, stream, address, server_address, ssl_options={}): + super(TerminalConnection, self).__init__(stream, address, server_address, server_type="frame", + ssl_options=ssl_options, max_packet_buf=2) + self.term_set = set() + self.term_count = 0 + + def shutdown(self): + print >> sys.stderr, "Shutting down server connection %s <- %s" % (self.connection_id, self.source) + super(TerminalConnection, self).shutdown() + + def on_close(self): + print >> sys.stderr, "Closing server connection %s <- %s" % (self.connection_id, self.source) + super(TerminalConnection, self).on_close() + + def handle_close(self): + pass + + def remote_terminal_update(self, term_name=None, add_flag=True): + """If term_name is None, generate new terminal name and return it""" + if not term_name: + while True: + self.term_count += 1 + term_name = "tty"+str(self.term_count) + if term_name not in self.term_set: + break + + if add_flag: + self.term_set.add(term_name) + else: + self.term_set.discard(term_name) + return term_name + + def remote_response(self, term_name, msg_list): + ws_list = GTSocket._watch_set.get(self.connection_id+"/"+term_name) + if not ws_list: + return + for ws_id in ws_list: + ws = GTSocket._all_websockets.get(ws_id) + if ws: + try: + ws.write_message(json.dumps(msg_list)) + except Exception, excp: + logging.error("remote_response: ERROR %s", excp) + try: + # Close websocket on write error + ws.close() + except Exception: + pass + + +def run_server(options, args): + global IO_loop, Http_server, Local_client, Lterm_cookie, Trace_shell + import signal + + def auth_token(secret, connection_id, client_nonce, server_nonce): + """Return (client_token, server_token)""" + SIGN_SEP = "|" + prefix = SIGN_SEP.join([connection_id, client_nonce, server_nonce]) + SIGN_SEP + return [hmac.new(str(secret), prefix+conn_type, digestmod=hashlib.sha256).hexdigest()[:24] for conn_type in ("client", "server")] + + class AuthHandler(tornado.web.RequestHandler): + def get(self): + self.set_header("Content-Type", "text/plain") + client_nonce = self.get_argument("nonce", "") + if not client_nonce: + raise tornado.web.HTTPError(401) + server_nonce = "1%018d" % random.randrange(0, 10**18) # 1 prefix to keep leading zeros when stringified + try: + client_token, server_token = auth_token(Gterm_secret, "graphterm", client_nonce, server_nonce) + except Exception: + raise tornado.web.HTTPError(401) + + # TODO NOTE: Save server_token to authenticate next connection + + self.set_header("Content-Type", "text/plain") + self.write(server_nonce+":"+client_token) + + try: + # Create App directory + os.mkdir(App_dir, 0700) + except OSError: + if os.stat(App_dir).st_mode != 0700: + # Protect App directory + os.chmod(App_dir, 0700) + + auth_file = "" + random_auth = False + if not options.auth_code: + # Default (random) auth code + random_auth = True + auth_code = GTSocket.get_auth_code() + elif options.auth_code == "none": + # No auth code + auth_code = "" + GTSocket.set_auth_code(auth_code) + else: + # Specified auth code + auth_code = options.auth_code + GTSocket.set_auth_code(auth_code) + + http_port = options.port + http_host = options.host + internal_host = options.internal_host or http_host + internal_port = options.internal_port or http_port-1 + + handlers = [] + if options.server_auth: + handlers += [(r"/_auth/.*", AuthHandler)] + with open(Gterm_secret_file, "w") as f: + f.write("%d %d %s\n" % (http_port, os.getpid(), Gterm_secret)) + os.chmod(Gterm_secret_file, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR) + + if options.auth_users: + for user in options.auth_users.split(","): + if user: + if random_auth: + # Personalized user auth codes + GTSocket._auth_users[user] = hmac.new(str(random_auth), user, digestmod=hashlib.sha256).hexdigest()[:HEX_DIGITS] + else: + # Same auth code for all users + GTSocket._auth_users[user] = GTSocket.get_auth_code() + + if auth_code: + if os.getenv("HOME", ""): + auth_file = Default_auth_file + try: + with open(auth_file, "w") as f: + f.write("%s://%s:%d/?code=%s\n" % (PROTOCOL, http_host, http_port, auth_code)); + if GTSocket._auth_users: + for user, key in GTSocket._auth_users.items(): + f.write("%s %s://%s:%d/?user=%s&code=%s\n" % (user, PROTOCOL, http_host, http_port, user, key)); + os.chmod(auth_file, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR) + except Exception: + logging.warning("Failed to create auth file: %s", auth_file) + auth_file = "" + + handlers += [(r"/_websocket/.*", GTSocket), + (r"/static/(.*)", tornado.web.StaticFileHandler, {"path": Doc_rootdir}), + (r"/().*", tornado.web.StaticFileHandler, {"path": Doc_rootdir, "default_filename": "index.html"}), + ] + + application = tornado.web.Application(handlers) + + logging.warning("DocRoot: "+Doc_rootdir); + + IO_loop = tornado.ioloop.IOLoop.instance() + + ssl_options = None + if options.https or options.client_cert: + cert_dir = App_dir + server_name = "localhost" + certfile = cert_dir+"/"+server_name+".crt" + keyfile = cert_dir+"/"+server_name+".key" + + new = not (os.path.exists(certfile) and os.path.exists(keyfile)) + fingerprint = ssl_cert_gen(server_name, cwd=cert_dir, new=new) + if not fingerprint: + print >> sys.stderr, "gtermserver: Failed to generate server SSL certificate" + sys.exit(1) + print >> sys.stderr, fingerprint + + ssl_options = {"certfile": certfile, "keyfile": keyfile} + if options.client_cert: + if options.client_cert == ".": + ssl_options["ca_certs"] = certfile + elif not os.path.exists(options.client_cert): + print >> sys.stderr, "Client cert file %s not found" % options.client_cert + sys.exit(1) + else: + ssl_options["ca_certs"] = options.client_cert + ssl_options["cert_reqs"] = ssl.CERT_REQUIRED + + internal_server_ssl = {"certfile": certfile, "keyfile": keyfile} if options.internal_https else None + internal_client_ssl = {"cert_reqs": ssl.CERT_REQUIRED, "ca_certs": certfile} if options.internal_https else None + TerminalConnection.start_tcp_server(internal_host, internal_port, io_loop=IO_loop, ssl_options=internal_server_ssl) + + if options.internal_https: + # Internal https causes tornado to loop (client fails to connect to server) + # Connecting to internal https from another process seems to be OK. + # Need to rewrite packetserver.PacketConnection to use tornado.netutil.TCPServer + Local_client, Lterm_cookie, Trace_shell = None, None, None + else: + oshell_globals = globals() if otrace and options.oshell else None + Local_client, Lterm_cookie, Trace_shell = gtermhost.gterm_connect(LOCAL_HOST, + internal_host, + server_port=internal_port, + connect_kw={"ssl_options": internal_client_ssl, + "term_type": options.term_type, + "lterm_logfile": options.lterm_logfile}, + oshell_globals=oshell_globals, + oshell_unsafe=True) + xterm = Local_client.xterm + killterm = Local_client.remove_term + + Http_server = tornado.httpserver.HTTPServer(application, ssl_options=ssl_options) + Http_server.listen(http_port, address=http_host) + logging.warning("Http_server listening on %s:%s" % (http_host, http_port)) + logging.warning("Auth code = %s %s" % (GTSocket.get_auth_code(), auth_file)) + + def test_fun(): + raise Exception("TEST EXCEPTION") + + def stop_server(): + gtermhost.gterm_shutdown(Trace_shell) + Http_server.stop() + IO_loop.stop() + + def sigterm(signal, frame): + logging.warning("SIGTERM signal received") + stop_server() + signal.signal(signal.SIGTERM, sigterm) + + try: + if not Trace_shell: + IO_loop.start() + else: + ioloop_thread = threading.Thread(target=IO_loop.start) + ioloop_thread.start() + time.sleep(1) # Time to start thread + print >> sys.stderr, "Listening on %s:%s" % (http_host, http_port) + + print >> sys.stderr, "\nType ^D^C to stop server" + Trace_shell.loop() + except KeyboardInterrupt: + print >> sys.stderr, "Interrupted" + + finally: + try: + if options.server_auth: + os.remove(Gterm_secret_file) + except Exception: + pass + IO_loop.add_callback(stop_server) + +def main(): + from optparse import OptionParser + + usage = "usage: gtermserver [-h ... options]" + parser = OptionParser(usage=usage) + + parser.add_option("", "--auth_code", dest="auth_code", default="", + help="Authentication code (default: random value; specify 'none' for no auth)") + + parser.add_option("", "--auth_users", dest="auth_users", default="", + help="Comma-separated list of authenticated user names") + + parser.add_option("", "--host", dest="host", default="localhost", + help="Hostname (or IP address) (default: localhost)") + parser.add_option("", "--port", dest="port", default=8900, + help="IP port (default: 8900)", type="int") + + parser.add_option("", "--internal_host", dest="internal_host", default="", + help="internal host name (or IP address) (default: external host name)") + parser.add_option("", "--internal_port", dest="internal_port", default=0, + help="internal port (default: PORT-1)", type="int") + + parser.add_option("", "--oshell", dest="oshell", action="store_true", + help="Activate otrace/oshell") + parser.add_option("", "--https", dest="https", action="store_true", + help="Use SSL (TLS) connections for security") + parser.add_option("", "--internal_https", dest="internal_https", action="store_true", + help="Use https for internal connections") + parser.add_option("", "--server_auth", dest="server_auth", action="store_true", + help="Enable server authentication by gterm clients") + parser.add_option("", "--client_cert", dest="client_cert", default="", + help="Path to client CA cert (or '.')") + parser.add_option("", "--term_type", dest="term_type", default="", + help="Terminal type (linux/screen/xterm)") + parser.add_option("", "--lterm_logfile", dest="lterm_logfile", default="", + help="Lineterm logfile") + + parser.add_option("", "--daemon", dest="daemon", default="", + help="daemon=start/stop/restart/status") + + (options, args) = parser.parse_args() + + if not options.daemon: + run_server(options, args) + else: + from daemon import ServerDaemon + pidfile = "/tmp/gtermserver.pid" + daemon = ServerDaemon(pidfile, functools.partial(run_server, options, args)) + daemon.daemon_run(options.daemon) + +if __name__ == "__main__": + main() diff --git a/graphterm/lineterm.py b/graphterm/lineterm.py new file mode 100755 index 0000000..24b37dd --- /dev/null +++ b/graphterm/lineterm.py @@ -0,0 +1,1618 @@ +#!/usr/bin/env python + +""" Lineterm: Line-oriented pseudo-tty wrapper +Derived from the public-domain Ajaxterm code, v0.11 (2008-11-13). + https://github.com/antonylesuisse/qweb + http://antony.lesuisse.org/software/ajaxterm/ +The contents of this file remain in the public-domain. +""" + +from __future__ import with_statement + +import array,cgi,copy,fcntl,glob,logging,mimetypes,optparse,os,pty,random,re,signal,select,sys,threading,time,termios,tty,struct,pwd + +import json +import Queue +import shlex +import subprocess +import traceback + +MAX_SCROLL_LINES = 500 + +IDLE_TIMEOUT = 300 # Idle timeout in seconds +UPDATE_INTERVAL = 0.05 # Fullscreen update time interval +TERM_TYPE = "linux" # "screen" would be a better default terminal, but arrow keys do not always work + +COPY_ENV = ["HOME", "LOGNAME", "PATH", "SECURITYSESSIONID", "SHELL", "SSH_AUTH_SOCK", "USER", "USERNAME"] + +ALTERNATE_SCREEN_CODES = (47, 1047, 1049) # http://rtfm.etla.org/xterm/ctlseq.html +GRAPHTERM_SCREEN_CODES = (1150, 1155) + +FILE_EXTENSIONS = {"css": "css", "htm": "html", "html": "html", "js": "javascript", "py": "python", + "xml": "xml"} + +FILE_COMMANDS = set(["cd", "cp", "mv", "rm", "gls", "gopen", "gvi"]) +COMMAND_DELIMITERS = "<>;" + +# Scroll lines array components +JINDEX = 0 +JOFFSET = 1 +JDIR = 2 +JMARKUP = 3 +JLINE = 4 + +Log_ignored = False +MAX_LOG_CHARS = 8 + +BINDIR = "bin" +Exec_path = os.path.join(os.path.dirname(__file__), BINDIR) + +def dump(data, trim=False): + """Return string from array of int data, trimming NULs, if need be""" + line = "".join(chr(i & 255) for i in data) + return line.rstrip("\x00") if trim else line + +def prompt_offset(line, prompt, meta=None): + """Return offset at end of prompt (not including trailing space), or zero""" + offset = 0 + if meta or (prompt and prompt[0] and line.startswith(prompt[0])): + end_offset = line.find(prompt[2]) + if end_offset >= 0: + offset = end_offset + len(prompt[2]) + return offset + +def command_output(command_args, **kwargs): + """ Executes a command and returns the string tuple (stdout, stderr) + keyword argument timeout can be specified to time out command (defaults to 15 sec) + """ + timeout = kwargs.pop("timeout", 15) + def command_output_aux(): + try: + proc = subprocess.Popen(command_args, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + return proc.communicate() + except Exception, excp: + return "", str(excp) + if not timeout: + return command_output_aux() + + exec_queue = Queue.Queue() + def execute_in_thread(): + exec_queue.put(command_output_aux()) + thrd = threading.Thread(target=execute_in_thread) + thrd.start() + try: + return exec_queue.get(block=True, timeout=timeout) + except Queue.Empty: + return "", "Timed out after %s seconds" % timeout + +def is_executable(filepath): + return os.path.isfile(filepath) and os.access(filepath, os.X_OK) + +def which(filepath, add_path=[]): + filedir, filename = os.path.split(filepath) + if filedir: + if is_executable(filepath): + return filepath + else: + for path in os.environ["PATH"].split(os.pathsep) + add_path: + whichpath = os.path.join(path, filepath) + if is_executable(whichpath): + return whichpath + return None + +def getcwd(pid): + """Return working directory of running process""" + if sys.platform.startswith("linux"): + command_args = ["pwdx", str(pid)] + else: + command_args = ["lsof", "-a", "-p", str(pid), "-d", "cwd", "-Fn"] + std_out, std_err = command_output(command_args, timeout=1) + if std_err: + logging.warning("getcwd: ERROR %s", std_err) + return "" + try: + if sys.platform.startswith("linux"): + return std_out.split()[1] + else: + return std_out.split("\n")[1][1:] + except Exception, excp: + logging.warning("getcwd: ERROR %s", excp) + return "" + +def parse_headers(text): + """Parse gterm output and return (headers, content)""" + headers = {"content_type": "text/html", "x_gterm_response": "", + "x_gterm_parameters": {}} + content = text + if text.startswith("<"): + # Raw HTML + return (headers, content) + + # "MIME headers" + head_str, sep, tail_str = text.partition("\r\n\r\n") + if not sep: + head_str, sep, tail_str = text.partition("\n\n") + if not sep: + head_str, sep, tail_str = text.partition("\r\r") + if sep: + if head_str.startswith("{"): + # JSON headers + try: + headers = json.loads(head_str) + content = tail_str + except Exception, excp: + content = str(excp) + headers["json_error"] = "JSON parse error" + headers["content_type"] = "text/plain" + else: + # Parse mime headers: "-" -> "_" (TO DO) + pass + + if "x_gterm_response" not in headers: + headers["x_gterm_response"] = "" + if "x_gterm_parameters" not in headers: + headers["x_gterm_parameters"] = {} + + return (headers, content) + + +def shplit(line, delimiters=COMMAND_DELIMITERS, final_delim="&", index=None): + """Split shell command line, returning all components as a list, including separators + """ + if not line: + return [] + comps = shlex.split(line) + indices = [] + buf = line + offset = 0 + for comp in comps: + ncomp = len(buf) - len(buf.lstrip()) + if ncomp: + indices.append(offset+ncomp) + offset += ncomp + buf = buf[ncomp:] + ncomp = len(comp) + while True: + try: + temcomp = shlex.split(buf[:ncomp])[0] + except Exception: + temcomp = None + if temcomp == comp: + break + ncomp += 1 + if ncomp > len(buf): + raise Exception("shplit ERROR ('%s','%s')" % (comp, buf)) + comp = buf[:ncomp] + buf = buf[ncomp:] + + if delimiters: + tembuf = comp.replace(" ", ".").replace(delimiters[0], " ") + indices += shplit(tembuf, delimiters=delimiters[1:], index=offset) + else: + indices.append(offset+ncomp) + + offset += ncomp + + if buf: + indices.append(offset+len(buf)) + + if index is None: + jprev = 0 + retval = [] + for j in indices: + retval.append(line[jprev:j]) + jprev = j + if final_delim and retval and retval[-1].endswith(final_delim) and retval[-1] != final_delim: + retval[-1] = retval[-1][:-len(final_delim)] + retval.append(final_delim) + + return retval + else: + return [j+index for j in indices] + +FILE_URI_PREFIX = "file://" +def split_file_uri(uri): + """Return triplet [hostname, filename, fullpath] for file://host/path URIs + If not file URI, returns [] + """ + if not uri.startswith(FILE_URI_PREFIX): + return [] + hostPath = uri[len(FILE_URI_PREFIX):] + comps = hostPath.split("/") + return [comps[0], comps[-1], "/"+"/".join(comps[1:])] + +def normalize_file_uri(file_uri, cwd): + filepath = split_file_uri(file_uri)[2] + if filepath == cwd: + return "." + else: + cwd_prefix = cwd+"/" + if filepath[:len(cwd_prefix)] == cwd_prefix: + return filepath[len(cwd_prefix):] + else: + return filepath + +def prompt_markup(text, entry_index, current_dir): + return '%s' % (entry_index, current_dir, cgi.escape(text)) + +def plain_markup(text, command=False): + cmd_class = " gterm-command" if command else "" + return '%s' % (cmd_class, cgi.escape(text),) + +def path_markup(text, current_dir, command=False): + cmd_class = " gterm-command" if command else "" + fullpath = os.path.join(current_dir, text) + return '%s' % (cmd_class, fullpath, "path", "xpaste", cgi.escape(text)) + +def command_markup(entry_index, current_dir, pre_offset, offset, line): + marked_up = prompt_markup(line[pre_offset:offset], entry_index, current_dir) + try: + comps = shplit(line[offset:]) + except Exception: + return marked_up + line[offset:] + + while comps and not comps[0].strip(): + marked_up += comps.pop(0) + if not comps: + return marked_up + cmd = comps.pop(0) + if cmd.startswith("./") and current_dir: + marked_up += path_markup(cmd[2:], current_dir, command=True) + else: + marked_up += plain_markup(cmd, command=True) + file_command = cmd in FILE_COMMANDS + for comp in comps: + if not comp.strip(): + # Space + marked_up += comp + elif comp[0] in COMMAND_DELIMITERS: + marked_up += plain_markup(comp) + if comp[0] == ";": + file_command = False + elif file_command and current_dir and comp[0] != "-": + marked_up += path_markup(comp, current_dir) + else: + marked_up += plain_markup(comp) + return marked_up + + +class ScreenBuf(object): + def __init__(self, prompt): + self.prompt = prompt + self.pre_offset = len(prompt[0]) if prompt else 0 + self.width = None + self.height = None + self.cursorx = None + self.cursory = None + self.main_screen = None + self.alt_screen = None + self.current_scroll_count = 0 + self.last_scroll_count = 0 + self.entry_index = 0 + self.scroll_lines = [] + self.cleared_current_dir = None + self.cleared_last = False + self.full_update = True + + # Init 0-256 to latin1 and html translation table + self.trl1="" + for i in range(256): + if i==10: + self.trl1+=chr(i) + elif i<32: + self.trl1+=" " + elif i<127 or i>160: + self.trl1+=chr(i) + else: + self.trl1+="?" + self.trhtml="" + for i in range(256): + if i==0x0a or (i>32 and i<127) or i>160: + self.trhtml+=chr(i) + elif i<=32: + self.trhtml+="\xa0" + else: + self.trhtml+="?" + + def reconnect(self): + self.last_scroll_count = self.current_scroll_count - len(self.scroll_lines) + self.full_update = True + + def clear_last_entry(self, last_entry_index=None): + if not self.scroll_lines or self.entry_index <= 0: + return + n = len(self.scroll_lines)-1 + entry_index, offset, dir, markup, line = self.scroll_lines[n] + if self.entry_index != entry_index: + return + if last_entry_index and last_entry_index != entry_index: + return + self.entry_index -= 1 + while n > 0 and self.scroll_lines[n-1][JINDEX] == entry_index: + n -= 1 + self.current_scroll_count -= len(self.scroll_lines) - n + self.cleared_last = True + if self.cleared_current_dir is None: + self.cleared_current_dir = self.scroll_lines[n][JDIR] + self.scroll_lines[n:] = [] + if self.last_scroll_count > self.current_scroll_count: + self.last_scroll_count = self.current_scroll_count + + def scroll_buf_up(self, line, meta, offset=0): + current_dir = "" + current_markup = None + if offset: + # Prompt line (i.e., command line) + self.entry_index += 1 + current_dir = meta or "" + current_markup = command_markup(self.entry_index, current_dir, self.pre_offset, offset, line) + if not self.cleared_last: + self.cleared_current_dir = None + self.cleared_last = False + self.current_scroll_count += 1 + self.scroll_lines.append([self.entry_index, offset, current_dir, current_markup, line]) + if len(self.scroll_lines) > MAX_SCROLL_LINES: + entry_index, offset, dir, markup, line = self.scroll_lines.pop(0) + while self.scroll_lines and self.scroll_lines[0][JINDEX] == entry_index: + self.scroll_lines.pop(0) + + def update(self, active_rows, width, height, cursorx, cursory, main_screen, + alt_screen=None, prompt=[]): + """ Returns full_update, update_rows, update_scroll + """ + full_update = self.full_update + self.full_update = False + + if width != self.width or height != self.height: + self.width = width + self.height = height + full_update = True + + if (alt_screen and not self.alt_screen) or (not alt_screen and self.alt_screen): + full_update = True + + if alt_screen: + screen = alt_screen + old_screen = self.alt_screen + row_count = height + else: + screen = main_screen + old_screen = self.main_screen + row_count = active_rows + + cursor_moved = (cursorx != self.cursorx or cursory != self.cursory) + update_rows = [] + + for j in range(row_count): + new_row = screen.data[width*j:width*(j+1)] + if full_update or old_screen is None: + row_update = True + else: + row_update = (new_row != old_screen.data[width*j:width*(j+1)]) + if row_update or (cursor_moved and (cursory == j or self.cursory == j)): + offset = prompt_offset(dump(new_row), prompt, screen.meta[j]) + update_rows.append([j, offset, "", None, self.dumprichtext(new_row, trim=True)]) + + self.cursorx = cursorx + self.cursory = cursory + self.main_screen = main_screen.make_copy() if main_screen else None + self.alt_screen = alt_screen.make_copy() if alt_screen else None + if self.last_scroll_count < self.current_scroll_count: + update_scroll = self.scroll_lines[self.last_scroll_count-self.current_scroll_count:] + else: + update_scroll = [] + self.last_scroll_count = self.current_scroll_count + return full_update, update_rows, update_scroll + + def dumplatin1(self, data, trim=False): + return dump(data, trim=trim).translate(self.trl1) + + def dumprichtext(self, data, trim=False): + span = "" + span_list = [] + style_list = [] + span_style, span_bg, span_fg = 0x0007, -1, -1 + for i in data: + q, c = divmod(i, 256) + if span_style != q: + if span: + span_list.append((style_list, span.translate(self.trl1))) + span = "" + span_style = q + style_list = [] + if span_style & 0x0008: + style_list.append("bold") + if span_style & 0x0700: + style_list.append("inverse") + span += chr(c) + if span: + if trim: + span = span.rstrip("\x00") + if span: + span_list.append((style_list, span.translate(self.trl1))) + return span_list + + def dumphtml(self, data, trim=False, color=1): + h=self.height + w=self.width + r="" + span="" + span_bg,span_fg=-1,-1 + for i in range(h*w): + q,c=divmod(data[i],256) + if color: + bg,fg=divmod(q,256) + else: + bg,fg=0,7 + if i==self.cursor_y*w+self.cursor_x: + bg,fg=1,7 + if (bg!=span_bg or fg!=span_fg or i==h*w-1): + if len(span): + r+='%s'%(span_fg,span_bg,cgi.escape(span.translate(self.trhtml))) + span="" + span_bg,span_fg=bg,fg + span+=chr(c) + if i%w==w-1: + span+='\n' + r='
%s
'%r + if self.last_html==r: + return '' + else: + self.last_html=r +# print >> sys.stderr, "lineterm: dumphtml ", self + return r + + def __repr__(self): + d = self.dumplatin1(self.main_screen.data) + r = "" + for i in range(self.height): + r += "|%s|\n"%d[self.width*i:self.width*(i+1)] + return r + +class Screen(object): + def __init__(self, width, height, data=None, meta=None): + self.width = width + self.height = height + self.data = data or array.array('i', [0x000000]*(width*height)) + self.meta = [None] * height + + def make_copy(self): + return Screen(self.width, self.height, data=copy.copy(self.data), meta=copy.copy(self.meta)) + +class Terminal(object): + def __init__(self, term_name, fd, pid, screen_callback, height=25, width=80, cookie=0, + prompt=[], logfile=""): + self.term_name = term_name + self.fd = fd + self.pid = pid + self.screen_callback = screen_callback + self.width = width + self.height = height + self.cookie = cookie + self.prompt = prompt + self.logfile = logfile + self.init() + self.reset() + self.output_time = time.time() + self.screen_buf = ScreenBuf(prompt) + self.buf = "" + self.alt_mode = False + self.screen = self.main_screen + self.trim_first_prompt = bool(prompt) + self.logchars = 0 + + def init(self): + self.esc_seq={ + "\x00": None, + "\x05": self.esc_da, + "\x07": None, + "\x08": self.esc_0x08, + "\x09": self.esc_0x09, + "\x0a": self.esc_0x0a, + "\x0b": self.esc_0x0a, + "\x0c": self.esc_0x0a, + "\x0d": self.esc_0x0d, + "\x0e": None, + "\x0f": None, + "\x1b#8": None, + "\x1b=": None, + "\x1b>": None, + "\x1b(0": None, + "\x1b(A": None, + "\x1b(B": None, + "\x1b[c": self.esc_da, + "\x1b[0c": self.esc_da, + "\x1b[>c": self.esc_sda, + "\x1b[>0c": self.esc_sda, + "\x1b[5n": self.esc_sr, + "\x1b[6n": self.esc_cpr, + "\x1b[x": self.esc_tpr, + "\x1b]R": None, + "\x1b7": self.esc_save, + "\x1b8": self.esc_restore, + "\x1bD": self.esc_ind, + "\x1bE": self.esc_nel, + "\x1bH": None, + "\x1bM": self.esc_ri, + "\x1bN": None, + "\x1bO": None, + "\x1bZ": self.esc_da, + "\x1ba": None, + "\x1bc": self.reset, + "\x1bn": None, + "\x1bo": None, + } + + for k,v in self.esc_seq.items(): + if v==None: + self.esc_seq[k] = self.esc_ignore + # regex + d={ + r'\[\??([0-9;]*)([@ABCDEFGHJKLMPXacdefghlmnqrstu`])' : self.csi_dispatch, + r'\]([^\x07]+)\x07' : self.esc_ignore, + } + + self.esc_re=[] + for k,v in d.items(): + self.esc_re.append((re.compile('\x1b'+k), v)) + # define csi sequences + self.csi_seq={ + '@': (self.csi_at,[1]), + '`': (self.csi_G,[1]), + 'J': (self.csi_J,[0]), + 'K': (self.csi_K,[0]), + } + + for i in [i[4] for i in dir(self) if i.startswith('csi_') and len(i)==5]: + if not self.csi_seq.has_key(i): + self.csi_seq[i] = (getattr(self,'csi_'+i),[1]) + + def reset(self, s=""): + self.update_time = 0 + self.needs_updating = True + self.main_screen = Screen(self.width, self.height) + self.alt_screen = Screen(self.width, self.height) + self.scroll_top = 0 + self.scroll_bot = self.height-1 + self.cursor_x_bak = self.cursor_x = 0 + self.cursor_y_bak = self.cursor_y = 0 + self.cursor_eol = 0 + self.style = 0x000700 + self.outbuf = "" + self.last_html = "" + self.active_rows = 0 + self.gterm_code = None + self.gterm_buf = None + self.gterm_entry_index = None + self.gterm_validated = False + + def resize(self, height, width): + reset_flag = (self.width != width or self.height != height) + if reset_flag: + self.scroll_screen() + min_width = min(self.width, width) + saved_line = None + if self.active_rows: + # Check first active line for prompt + line = dump(self.main_screen.data[:min_width]) + if prompt_offset(line, self.prompt, self.main_screen.meta[0]): + saved_line = [len(line.rstrip('\x00')), self.main_screen.meta[0], self.main_screen.data[:min_width]] + self.width = width + self.height = height + self.reset() + + if saved_line: + # Restore saved line + self.active_rows = 1 + self.cursor_x = saved_line[0] + self.main_screen.meta[0] = saved_line[1] + self.main_screen.data[:min_width] = saved_line[2] + + self.screen = self.alt_screen if self.alt_mode else self.main_screen + self.needs_updating = True + + def reconnect(self): + self.screen_buf.reconnect() + self.needs_updating = True + + def clear_last_entry(self, last_entry_index=None): + self.screen_buf.clear_last_entry(last_entry_index=last_entry_index) + + def scroll_screen(self, scroll_rows=None): + if scroll_rows == None: + scroll_rows = 0 + for j in range(self.active_rows-1,-1,-1): + line = dump(self.main_screen.data[self.width*j:self.width*(j+1)]) + if prompt_offset(line, self.prompt, self.main_screen.meta[j]): + # Move rows before last prompt to buffer + scroll_rows = j + break + if not scroll_rows: + return + + # Move scrolled active rows to buffer + for cursor_y in range(scroll_rows): + row = self.main_screen.data[self.width*cursor_y:self.width*cursor_y+self.width] + self.screen_buf.scroll_buf_up(self.screen_buf.dumplatin1(row, trim=True), + self.main_screen.meta[cursor_y], + offset=prompt_offset(dump(row), self.prompt, self.main_screen.meta[cursor_y])) + + # Scroll and zero rest of screen + if scroll_rows < self.active_rows: + self.poke(0, 0, self.peek(scroll_rows, 0, self.active_rows-1, self.width)) + self.active_rows = self.active_rows - scroll_rows + if self.active_rows: + self.screen.meta[0:self.active_rows] = self.screen.meta[scroll_rows:scroll_rows+self.active_rows] + self.zero_lines(self.active_rows, self.height-1) + self.cursor_y = max(0, self.cursor_y - scroll_rows) + if not self.active_rows: + self.cursor_x = 0 + self.cursor_eol = 0 + + def update(self): + self.update_time = time.time() + self.needs_updating = False + + alt_screen = self.alt_screen if self.alt_mode else None + if not self.alt_mode: + self.scroll_screen() + + full_update, update_rows, update_scroll = self.screen_buf.update(self.active_rows, self.width, self.height, + self.cursor_x, self.cursor_y, + self.main_screen, + alt_screen=alt_screen, + prompt=self.prompt) + pre_offset = len(self.prompt[0]) if self.prompt else 0 + self.screen_callback(self.term_name, "row_update", + [self.alt_mode, full_update, self.active_rows, + self.width, self.height, + self.cursor_x, self.cursor_y, pre_offset, + update_rows, update_scroll]) + + def zero(self, y1, x1, y2, x2, screen=None): + if screen is None: screen = self.screen + w = self.width*(y2-y1) + x2 - x1 + 1 + z = array.array('i', [0x000000]*w) + screen.data[self.width*y1+x1:self.width*y2+x2+1] = z + + def zero_lines(self, y1, y2): + self.zero(y1, 0, y2, self.width-1) + self.screen.meta[y1:y2+1] = [None]*(y2+1-y1) + + def zero_screen(self): + self.zero_lines(0, self.height-1) + + def peek(self, y1, x1, y2, x2): + return self.screen.data[self.width*y1+x1:self.width*y2+x2] + + def poke(self, y, x, s): + pos = self.width*y + x + self.screen.data[pos:pos+len(s)] = s + if not self.alt_mode: + self.active_rows = max(y+1, self.active_rows) + + def scroll_up(self, y1, y2): + self.poke(y1, 0, self.peek(y1+1, 0, y2, self.width)) + self.screen.meta[y1:y2] = self.screen.meta[y1+1:y2+1] + self.zero_lines(y2, y2) + + def scroll_down(self, y1, y2): + self.poke(y1+1, 0, self.peek(y1, 0, y2-1, self.width)) + self.screen.meta[y1+1:y2+1] = self.screen.meta[y1:y2] + self.zero_lines(y1, y1) + + def scroll_right(self, y, x): + self.poke(y, x+1, self.peek(y, x, y, self.width-1)) + self.zero(y, x, y, x) + + def cursor_down(self): + if self.cursor_y >= self.scroll_top and self.cursor_y <= self.scroll_bot: + self.cursor_eol = 0 + q, r = divmod(self.cursor_y+1, self.scroll_bot+1) + if q: + if not self.alt_mode: + self.screen_buf.scroll_buf_up(self.screen_buf.dumplatin1(self.peek(self.scroll_top, 0, self.scroll_top, self.width), trim=True), self.screen.meta[self.scroll_top]) + self.scroll_up(self.scroll_top, self.scroll_bot) + self.cursor_y = self.scroll_bot + else: + self.cursor_y = r + + if not self.alt_mode: + self.active_rows = max(self.cursor_y+1, self.active_rows) + + def cursor_right(self): + q, r = divmod(self.cursor_x+1, self.width) + if q: + self.cursor_eol = 1 + else: + self.cursor_x = r + + def expect_prompt(self, current_directory): + if not self.active_rows or self.cursor_y+1 == self.active_rows: + self.screen.meta[self.cursor_y] = current_directory + + def echo(self, c): + if self.logfile and self.logchars < MAX_LOG_CHARS: + with open(self.logfile, "a") as logf: + if not self.logchars: + logf.write("TXT:") + logf.write(c) + self.logchars += 1 + if self.logchars == MAX_LOG_CHARS: + logf.write("\n") + if self.cursor_eol: + self.cursor_down() + self.cursor_x = 0 + self.screen.data[(self.cursor_y*self.width)+self.cursor_x] = self.style|ord(c) + self.cursor_right() + if not self.alt_mode: + self.active_rows = max(self.cursor_y+1, self.active_rows) + + def esc_0x08(self, s): + """Backspace""" + self.cursor_x = max(0,self.cursor_x-1) + + def esc_0x09(self, s): + """Tab""" + x = self.cursor_x+8 + q, r = divmod(x, 8) + self.cursor_x = (q*8)%self.width + + def esc_0x0a(self,s): + """Newline""" + self.cursor_down() + + def esc_0x0d(self,s): + """Carriage Return""" + self.cursor_eol = 0 + self.cursor_x = 0 + + def esc_save(self, s): + self.cursor_x_bak = self.cursor_x + self.cursor_y_bak = self.cursor_y + + def esc_restore(self,s): + self.cursor_x = self.cursor_x_bak + self.cursor_y = self.cursor_y_bak + self.cursor_eol = 0 + if not self.alt_mode: + self.active_rows = max(self.cursor_y+1, self.active_rows) + + def esc_da(self, s): + """Send primary device attributes""" + self.outbuf = "\x1b[?6c" + + def esc_sda(self, s): + """Send secondary device attributes""" + self.outbuf = "\x1b[>0;0;0c" + + def esc_tpr(self, s): + """Send Terminal Parameter Report""" + self.outbuf = "\x1b[0;0;0;0;0;0;0x" + + def esc_sr(self, s): + """Send Status Report""" + self.outbuf = "\x1b[0n" + + def esc_cpr(self, s): + """Send Cursor Position Report""" + self.outbuf = "\x1b[%d;%dR" % (self.cursor_y+1, self.cursor_x+1) + + def esc_nel(self, s): + """Next Line (NEL)""" + self.cursor_down() + self.cursor_x = 0 + + def esc_ind(self, s): + """Index (IND)""" + self.cursor_down() + + def esc_ri(self, s): + """Reverse Index (RI)""" + if self.cursor_y == self.scroll_top: + self.scroll_down(self.scroll_top, self.scroll_bot) + else: + self.cursor_y = max(self.scroll_top, self.cursor_y-1) + + if not self.alt_mode: + self.active_rows = max(self.cursor_y+1, self.active_rows) + + def esc_ignore(self,*s): + if Log_ignored or self.logfile: + print >> sys.stderr, "lineterm:ignore: %s"%repr(s) + + def csi_dispatch(self,seq,mo): + # CSI sequences + s = mo.group(1) + c = mo.group(2) + f = self.csi_seq.get(c, None) + if f: + try: + l = [int(i) for i in s.split(';')] + except ValueError: + l = [] + if len(l)==0: + l = f[1] + f[0](l) + elif Log_ignored or self.logfile: + print >> sys.stderr, 'lineterm: csi ignore', s, c + + def csi_at(self, l): + for i in range(l[0]): + self.scroll_right(self.cursor_y, self.cursor_x) + + def csi_A(self, l): + """Cursor up (default 1)""" + self.cursor_y = max(self.scroll_top, self.cursor_y-l[0]) + + def csi_B(self, l): + """Cursor down (default 1)""" + self.cursor_y = min(self.scroll_bot, self.cursor_y+l[0]) + if not self.alt_mode: + self.active_rows = max(self.cursor_y+1, self.active_rows) + + def csi_C(self, l): + """Cursor forward (default 1)""" + self.cursor_x = min(self.width-1, self.cursor_x+l[0]) + self.cursor_eol = 0 + + def csi_D(self, l): + """Cursor backward (default 1)""" + self.cursor_x = max(0, self.cursor_x-l[0]) + self.cursor_eol = 0 + + def csi_E(self, l): + """Cursor next line (default 1)""" + self.csi_B(l) + self.cursor_x = 0 + self.cursor_eol = 0 + + def csi_F(self, l): + """Cursor preceding line (default 1)""" + self.csi_A(l) + self.cursor_x = 0 + self.cursor_eol = 0 + + def csi_G(self, l): + """Cursor Character Absolute [column]""" + self.cursor_x = min(self.width, l[0])-1 + + def csi_H(self, l): + """Cursor Position [row;column]""" + if len(l) < 2: l=[1,1] + self.cursor_x = min(self.width, l[1])-1 + self.cursor_y = min(self.height, l[0])-1 + self.cursor_eol = 0 + if not self.alt_mode: + self.active_rows = max(self.cursor_y+1, self.active_rows) + + def csi_J(self, l): + """Erase in Display""" + if l[0]==0: + # Erase below (default) + if not self.cursor_x: + self.zero_lines(self.cursor_y, self.height-1) + else: + self.zero(self.cursor_y, self.cursor_x, self.height-1, self.width-1) + elif l[0]==1: + # Erase above + if self.cursor_x==self.width-1: + self.zero_lines(0, self.cursor_y) + else: + self.zero(0, 0, self.cursor_y, self.cursor_x) + elif l[0]==2: + # Erase all + self.zero_screen() + + def csi_K(self, l): + """Erase in Line""" + if l[0]==0: + # Erase to right (default) + self.zero(self.cursor_y, self.cursor_x, self.cursor_y, self.width-1) + elif l[0]==1: + # Erase to left + self.zero(self.cursor_y, 0, self.cursor_y, self.cursor_x) + elif l[0]==2: + # Erase all + self.zero_lines(self.cursor_y, self.cursor_y) + + def csi_L(self, l): + """Insert lines (default 1)""" + for i in range(l[0]): + if self.cursor_y=self.scroll_top and self.cursor_y<=self.scroll_bot: + for i in range(l[0]): + self.scroll_up(self.cursor_y, self.scroll_bot) + def csi_P(self, l): + """Delete characters (default 1)""" + w, cx, cy = self.width, self.cursor_x, self.cursor_y + end = self.peek(cy, cx, cy, w) + self.csi_K([0]) + self.poke(cy, cx, end[l[0]:]) + + def csi_X(self, l): + """Erase characters (default 1)""" + self.zero(self.cursor_y, self.cursor_x, self.cursor_y, self.cursor_x+l[0]) + + def csi_a(self, l): + """Cursor forward (default 1)""" + self.csi_C(l) + + def csi_c(self, l): + """Send Device attributes""" + #'\x1b[?0c' 0-8 cursor size + pass + + def csi_d(self, l): + """Vertical Position Absolute [row]""" + self.cursor_y = min(self.height, l[0])-1 + if not self.alt_mode: + self.active_rows = max(self.cursor_y+1, self.active_rows) + + def csi_e(self, l): + """Cursor down""" + self.csi_B(l) + + def csi_f(self, l): + """Horizontal and Vertical Position [row;column]""" + self.csi_H(l) + + def csi_h(self, l): + """Set private mode""" + if l[0] in GRAPHTERM_SCREEN_CODES: + if not self.alt_mode: + self.gterm_code = l[0] + self.gterm_validated = (len(l) >= 2 and str(l[1]) == self.cookie) + self.gterm_buf = [] + self.gterm_entry_index = self.screen_buf.entry_index+1 + if self.gterm_code != GRAPHTERM_SCREEN_CODES[0]: + self.scroll_screen(self.active_rows) + if self.logfile: + with open(self.logfile, "a") as logf: + logf.write("GTERMMODE\n") + + elif l[0] in ALTERNATE_SCREEN_CODES: + self.alt_mode = True + self.screen = self.alt_screen + self.style = 0x000700 + self.zero_screen() + if self.logfile: + with open(self.logfile, "a") as logf: + logf.write("ALTMODE\n") + elif l[0] == 4: + pass +# print "insert on" + + def csi_l(self, l): + """Reset private mode""" + if l[0] in GRAPHTERM_SCREEN_CODES: + pass # No-op (mode already exited in escape) + + elif l[0] in ALTERNATE_SCREEN_CODES: + self.alt_mode = False + self.screen = self.main_screen + self.style = 0x000700 + self.cursor_y = max(0, self.active_rows-1) + self.cursor_x = 0 + if self.logfile: + with open(self.logfile, "a") as logf: + logf.write("NORMODE\n") + elif l[0] == 4: + pass +# print "insert off" + + def csi_m(self, l): + """Select Graphic Rendition""" + for i in l: + if i==0 or i==39 or i==49 or i==27: + # Normal + self.style = 0x000700 + elif i==1: + # Bold + self.style = (self.style|0x000800) + elif i==7: + # Inverse + self.style = 0x070000 + elif i>=30 and i<=37: + # Foreground Black(30), Red, Green, Yellow, Blue, Magenta, Cyan, White + c = i-30 + self.style = (self.style&0xff08ff)|(c<<8) + elif i>=40 and i<=47: + # Background Black(30), Red, Green, Yellow, Blue, Magenta, Cyan, White + c = i-40 + self.style = (self.style&0x00ffff)|(c<<16) +# else: +# print >> sys.stderr, "lineterm: CSI style ignore",l,i +# print >> sys.stderr, 'lineterm: style: %r %x'%(l, self.style) + + def csi_r(self, l): + """Set scrolling region [top;bottom]""" + if len(l)<2: l = [1, self.height] + self.scroll_top = min(self.height-1, l[0]-1) + self.scroll_bot = min(self.height-1, l[1]-1) + self.scroll_bot = max(self.scroll_top, self.scroll_bot) + + def csi_s(self, l): + self.esc_save(0) + + def csi_u(self, l): + self.esc_restore(0) + + def escape(self): + e = self.buf + if len(e)>32: + if Log_ignored or self.logfile: + print >> sys.stderr, "lineterm: escape error %r"%e + self.buf = "" + elif e in self.esc_seq: + if self.logfile: + with open(self.logfile, "a") as logf: + logf.write("SQ%02x%s\n" % (ord(e[0]), e[1:])) + self.esc_seq[e](e) + self.buf = "" + self.logchars = 0 + else: + for r,f in self.esc_re: + mo = r.match(e) + if mo: + if self.logfile: + with open(self.logfile, "a") as logf: + logf.write("RE%02x%s\n" % (ord(e[0]), e[1:])) + f(e,mo) + self.buf = "" + self.logchars = 0 + break +# if self.buf=='': print >> sys.stderr, "lineterm: ESC %r\n"%e + + def gterm_append(self, s): + prefix, sep, suffix = s.partition('\x1b') + self.gterm_buf.append(prefix) + if not sep: + return "" + retval = sep + suffix + # ESCAPE sequence encountered; terminate + if self.gterm_code == GRAPHTERM_SCREEN_CODES[0]: + # Handle prompt command output + current_dir = "".join(self.gterm_buf) + if current_dir: + self.expect_prompt(current_dir) + elif self.gterm_buf: + # graphterm output ("pagelet") + self.update() + gterm_output = "".join(self.gterm_buf).lstrip() + headers, content = parse_headers(gterm_output) + response_type = headers["x_gterm_response"] + response_params = headers["x_gterm_parameters"] + if self.gterm_validated: + if response_type == "edit_file": + filepath = response_params.get("filepath", "") + try: + if "filetype" not in response_params: + basename, extension = os.path.splitext(filepath) + if extension: + response_params["filetype"] = FILE_EXTENSIONS.get(extension[1:].lower(), "") + else: + response_params["filetype"] = "" + with open(filepath) as f: + content = f.read() + except Exception, excp: + content = "ERROR in opening file '%s': %s" % (filepath, excp) + headers["x_gterm_response"] = "error_message" + headers["x_gterm_parameters"] = {} + headers["content_type"] = "text/plain" + elif response_type != "edit_file": + # Display non-validated input as plain text + headers["x_gterm_response"] = "pagelet" + headers["x_gterm_parameters"] = {} + try: + import lxml.html + content = lxml.html.fromstring(content).text_content() + headers["content_type"] = "text/plain" + except Exception: + content = cgi.escape(content) + if self.gterm_validated or response_type != "edit_file": + params = {"validated": self.gterm_validated, "headers": headers} + self.screen_callback(self.term_name, "graphterm_output", [params, content]) + self.gterm_code = None + self.gterm_buf = None + self.gterm_validated = False + self.gterm_entry_index = None + return retval + + def save_file(self, filepath, filedata): + status = "" + try: + with open(filepath, "w") as f: + f.write(filedata) + except Exception, excp: + status = str(excp) + self.screen_callback(self.term_name, "save_status", [filepath, status]) + + def get_finder(self, kind, directory=""): + test_finder_head = """ ++""" + test_finder_row = """ + + +
+
%(filename)s +""" + test_finder_tail = """ +
+""" + if not self.active_rows: + # Not at command line + return + directory = directory or self.screen.meta[self.active_rows-1] or getcwd(self.pid) + row_content = test_finder_row % {"fullpath": directory, + "filetype": "directory", + "clickcmd": "cd %(path); gls -f", + "fileicon": "/static/images/tango-folder.png", + "filename": "."} + content = "\n".join([test_finder_head] + 40*[row_content] + [test_finder_tail]) + headers = {"content_type": "text/html", + "x_gterm_response": "display_finder", + "x_gterm_parameters": {"finder_type": kind, "current_directory": ""}} + params = {"validated": self.gterm_validated, "headers": headers} + self.screen_callback(self.term_name, "graphterm_output", [params, content]) + + def click_paste(self, text, file_uri="", options={}): + """Paste text or filename (and command) into command line. + Different behavior depending upon whether command line is empty or not. + If not text, create text from filepath, normalizing if need be. + options = {command: "", clear_last: 0/n, normalize: null/true/false, enter: false/true + If clear_last and command line is empty, clear last entry (can also be numeric string). + Normalize may be None (for default behavior), False or True. + if enter, append newline to text. + """ + if not self.active_rows: + # Not at command line + return + command = options.get("command", "") + dest_uri = options.get("dest_uri", "") + clear_last = options.get("clear_last", 0) + normalize = options.get("normalize", None) + enter = options.get("enter", False) + + line = dump(self.peek(self.active_rows-1, 0, self.active_rows-1, self.width), trim=True) + cwd = self.screen.meta[self.active_rows-1] or getcwd(self.pid) + offset = prompt_offset(line, self.prompt, cwd) + + try: + clear_last = int(clear_last) if clear_last else 0 + except Exception, excp: + logging.warning("click_paste: ERROR %s", excp) + clear_last = 0 + if clear_last and offset and offset == len(line.rstrip()): + # Empty command line; clear last entry + self.screen_buf.clear_last_entry(clear_last) + + space_prefix = "" + command_prefix = "" + expect_filename = False + if not text and file_uri: + text = split_file_uri(file_uri)[2] + + if offset: + # At command line + if normalize is None and cwd and (not self.screen_buf.cleared_last or self.screen_buf.cleared_current_dir is None or self.screen_buf.cleared_current_dir == cwd): + # Current directory known and no entries cleared + # or first cleared entry had same directory as current; normalize + normalize = True + + if self.cursor_y == self.active_rows-1: + pre_line = line[:self.cursor_x] + else: + pre_line = line + pre_line = pre_line[offset:] + if pre_line and pre_line[0] == " ": + # Strip space associated with prompt + pre_line = pre_line[1:] + if not pre_line.strip(): + # Empty/blank command line + if command: + # Command to precede filename + command_prefix = command + expect_filename = True + elif text: + # Use text as command + if not pre_line and not which(text, add_path=[Exec_path]): + raise Exception("Command '%s' not found" % text) + command_prefix = text.replace(" ", "\\ ") + text = "" + if command_prefix and command_prefix[-1] != " ": + command_prefix += " " + else: + # Non-empty command line; expect filename + expect_filename = True + if pre_line[-1] != " ": + space_prefix = " " + + if cwd and normalize and expect_filename and file_uri: + # Check if file URI represents subdirectory of CWD + normpath = normalize_file_uri(file_uri, cwd) + if not normpath.startswith("/"): + text = normpath + + if text or command_prefix: + text = text.replace(" ", "\\ ") + if expect_filename and command_prefix.find("%(path)") >= 0: + paste_text = command_prefix.replace("%(path)", text) + else: + paste_text = command_prefix+space_prefix+text+" " + if dest_uri: + if paste_text and paste_text[-1] != " ": + paste_text += " " + paste_text += normalize_file_uri(dest_uri, cwd) + if enter and offset and not pre_line and command: + # Empty command line with pasted command + paste_text += "\n" + try: + os.write(self.fd, paste_text) + except Exception, excp: + print >> sys.stderr, "lineterm: Error in click_paste: %s" % excp + + def write(self, s): + self.output_time = time.time() + if self.gterm_buf is not None: + s = self.gterm_append(s) + if not s: + return + self.needs_updating = True + + for k, i in enumerate(s): + if self.gterm_buf is not None: + self.write(s[k:]) + return + if len(self.buf) or (i in self.esc_seq): + self.buf += i + self.escape() + elif i == '\x1b': + self.buf += i + else: + self.echo(i) + + def read(self): + b = self.outbuf + self.outbuf = "" + return b + + +class Multiplex(object): + def __init__(self, screen_callback, command=None, cookie=0, prompt=[], logfile="", + term_type="linux", app_name="graphterm"): + """ prompt = [prefix, format, suffix] + """ + ##signal.signal(signal.SIGCHLD, signal.SIG_IGN) + self.screen_callback = screen_callback + self.command = command + self.cookie = cookie + self.prompt = prompt + self.logfile = logfile + self.term_type = term_type + self.app_name = app_name + self.proc = {} + self.lock = threading.RLock() + self.thread = threading.Thread(target=self.loop) + self.alive = 1 + self.check_kill_idle = False + self.name_count = 0 + self.thread.start() + + def terminal(self, term_name=None, command="", height=25, width=80): + """Create new pty, return tty name, or just return name for existing pty""" + command = command or self.command + with self.lock: + if not term_name: + self.name_count += 1 + term_name = "tty%s" % self.name_count + + if term_name in self.proc: + self.set_size(term_name, height, width) + return term_name + + pid, fd = pty.fork() + if pid==0: + try: + fdl = [int(i) for i in os.listdir('/proc/self/fd')] + except OSError: + fdl = range(256) + for i in [i for i in fdl if i>2]: + try: + os.close(i) + except OSError: + pass + if command: + comps = command.split() + if comps and re.match(r'^[/\w]*/(ba|c|k|tc)?sh$', comps[0]): + cmd = comps + else: + cmd = ['/bin/sh', '-c', command] + elif os.getuid()==0: + cmd = ['/bin/login'] + else: + sys.stdout.write("Login: ") + login = sys.stdin.readline().strip() + if re.match('^[0-9A-Za-z-_. ]+$',login): + cmd = ['ssh'] + cmd += ['-oPreferredAuthentications=keyboard-interactive,password'] + cmd += ['-oNoHostAuthenticationForLocalhost=yes'] + cmd += ['-oLogLevel=FATAL'] + cmd += ['-F/dev/null', '-l', login, 'localhost'] + else: + os._exit(0) + env = {} + for var in COPY_ENV: + val = os.getenv(var) + if val is not None: + env[var] = val + if var == "PATH": + # Prepend app bin directory to path + env[var] = Exec_path + ":" + env[var] + env["COLUMNS"] = str(width) + env["LINES"] = str(height) + env["TERM"] = self.term_type or TERM_TYPE + env["GRAPHTERM_COOKIE"] = str(self.cookie) + if self.prompt: + env["GRAPHTERM_PROMPT"] = "".join(self.prompt) + " " + ##env["PROMPT_COMMAND"] = "export PS1=$GRAPHTERM_PROMPT; unset PROMPT_COMMAND" + env["PROMPT_COMMAND"] = 'export PS1=$GRAPHTERM_PROMPT; echo -n "\033[?%s;${GRAPHTERM_COOKIE}h$PWD\033[?%s;l"' % (GRAPHTERM_SCREEN_CODES[0], GRAPHTERM_SCREEN_CODES[0]) + + # cd to HOME + os.chdir(os.path.expanduser("~")) + os.execvpe(cmd[0], cmd, env) + else: + fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK) + self.proc[term_name] = Terminal(term_name, fd, pid, self.screen_callback, height=height, width=width, + cookie=self.cookie, prompt=self.prompt, logfile=self.logfile) + self.set_size(term_name, height, width) + return term_name + + def set_size(self, term_name, height, width): + # python bug http://python.org/sf/1112949 on amd64 + term = self.proc.get(term_name) + if term: + term.resize(height, width) + fcntl.ioctl(term.fd, struct.unpack('i',struct.pack('I',termios.TIOCSWINSZ))[0], + struct.pack("HHHH",height,width,0,0)) + + def term_names(self): + with self.lock: + return self.proc.keys() + + def running(self): + with self.lock: + return self.alive + + def shutdown(self): + with self.lock: + if not self.alive: + return + self.alive = 0 + self.kill_all() + + def kill_term(self, term_name): + with self.lock: + term = self.proc.get(term_name) + if term: + # "Idle" terminal + term.output_time = 0 + self.check_kill_idle = True + + def kill_all(self): + with self.lock: + for term in self.proc.values(): + # "Idle" terminal + term.output_time = 0 + self.check_kill_idle = True + + def kill_idle(self): + # Kill all "idle" terminals + with self.lock: + cur_time = time.time() + for term_name in self.term_names(): + term = self.proc.get(term_name) + if term: + if (cur_time-term.output_time) > IDLE_TIMEOUT: + try: + os.close(term.fd) + os.kill(term.pid, signal.SIGTERM) + except (IOError, OSError): + pass + try: + del self.proc[term_name] + except Exception: + pass + logging.warning("kill_idle: %s", term_name) + + def term_read(self, term_name): + with self.lock: + term = self.proc.get(term_name) + if not term: + return + try: + data = os.read(term.fd, 65536) + if not data: + print >> sys.stderr, "lineterm: EOF in reading from %s; closing it" % term_name + self.term_update(term_name) + self.kill_term(term_name) + return + if term.trim_first_prompt: + term.trim_first_prompt = False + # Fix for the very first prompt not being set + if data.startswith("> "): + data = data[2:] + elif data.startswith("\r\x1b[K> "): + data = data[6:] + + term.write(data) + reply = term.read() + if reply: + os.write(term.fd, reply) + except (KeyError, IOError, OSError): + print >> sys.stderr, "lineterm: Error in reading from %s; closing it" % term_name + self.kill_term(term_name) + + def term_write(self, term_name, data): + with self.lock: + term = self.proc.get(term_name) + if not term: + return + try: + os.write(term.fd, data) + except (IOError, OSError): + print >> sys.stderr, "lineterm: Error in writing to %s; closing it" % term_name + self.kill_term(term_name) + + def term_update(self, term_name): + with self.lock: + term = self.proc.get(term_name) + if term: + term.update() + + def dump(self, term_name, data, trim=False, color=1): + with self.lock: + term = self.proc.get(term_name) + if not term: + return "" + try: + return term.screen_buf.dumplatin1(data, trim=trim) + except KeyError: + return "ERROR in dump" + + def save_file(self, term_name, filepath, filedata): + with self.lock: + term = self.proc.get(term_name) + if not term: + return + term.save_file(filepath, filedata) + + def get_finder(self, term_name, kind, directory=""): + with self.lock: + term = self.proc.get(term_name) + if not term: + return + term.get_finder(kind, directory=directory) + + def click_paste(self, term_name, text, file_uri="", options={}): + with self.lock: + term = self.proc.get(term_name) + if not term: + return + term.click_paste(text, file_uri=file_uri, options=options) + + def reconnect(self, term_name): + with self.lock: + term = self.proc.get(term_name) + if not term: + return + term.reconnect() + + def clear_last_entry(self, term_name, last_entry_index=None): + with self.lock: + term = self.proc.get(term_name) + if not term: + return + term.clear_last_entry(last_entry_index=last_entry_index) + + def loop(self): + while self.running(): + try: + fd_dict = dict((term.fd, name) for name, term in self.proc.items()) + if not fd_dict: + time.sleep(0.02) + continue + inputs, outputs, errors = select.select(fd_dict.keys(), [], [], 0.02) + for fd in inputs: + try: + self.term_read(fd_dict[fd]) + except Exception, excp: + traceback.print_exc() + term_name = fd_dict[fd] + logging.warning("Multiplex.loop: INTERNAL READ ERROR (%s) %s", term_name, excp) + self.kill_term(term_name) + cur_time = time.time() + for term_name in fd_dict.values(): + term = self.proc.get(term_name) + if term: + if (term.needs_updating or term.output_time > term.update_time) and cur_time-term.update_time > UPDATE_INTERVAL: + try: + self.term_update(term_name) + except Exception, excp: + traceback.print_exc() + logging.warning("Multiplex.loop: INTERNAL UPDATE ERROR (%s) %s", term_name, excp) + self.kill_term(term_name) + if self.check_kill_idle: + self.check_kill_idle = False + self.kill_idle() + + if len(inputs): + time.sleep(0.002) + except Exception, excp: + traceback.print_exc() + logging.warning("Multiplex.loop: ERROR %s", excp) + break + self.kill_all() + +if __name__ == "__main__": + ## Code to test LineTerm on reguler terminal + ## Re-size terminal to 80x25 before testing + + # Determine terminal width, height + height, width = struct.unpack("hh", fcntl.ioctl(pty.STDOUT_FILENO, termios.TIOCGWINSZ, "1234")) + if not width or not height: + try: + height, width = [int(os.getenv(var)) for var in ("LINES", "COLUMNS")] + except Exception: + height, width = 25, 80 + + Prompt = "> " + Log_file = "term.log" + Log_file = "" + def screen_callback(term_name, command, arg): + if command == "row_update": + alt_mode, reset, active_rows, width, height, cursorx, cursory, pre_offset, update_rows, update_scroll = arg + for row_num, row_offset, row_dir, row_markup, row_span in update_rows: + row_str = "".join(x[1] for x in row_span) + sys.stdout.write("\x1b[%d;%dH%s" % (row_num+1, 0, row_str)) + sys.stdout.write("\x1b[%d;%dH" % (row_num+1, len(row_str)+1)) + sys.stdout.write("\x1b[K") + if not alt_mode and active_rows < height and cursory+1 < height: + # Erase below + sys.stdout.write("\x1b[%d;%dH" % (cursory+2, 0)) + sys.stdout.write("\x1b[J") + sys.stdout.write("\x1b[%d;%dH" % (cursory+1, cursorx+1)) + ##if Log_file: + ## with open(Log_file, "a") as logf: + ## logf.write("CALLBACK:(%d,%d) %d\n" % (cursorx, cursory, active_rows)) + sys.stdout.flush() + + Line_term = Multiplex(screen_callback, "sh", cookie=1, logfile=Log_file) + Term_name = Line_term.terminal(height=height, width=width) + + Term_attr = termios.tcgetattr(pty.STDIN_FILENO) + try: + tty.setraw(pty.STDIN_FILENO) + expectEOF = False + while True: + ##data = raw_input(Prompt) + ##Line_term.write(data+"\n") + data = os.read(pty.STDIN_FILENO, 1024) + if ord(data[0]) == 4: + if expectEOF: raise EOFError + expectEOF = True + else: + expectEOF = False + if not data: + raise EOFError + Line_term.term_write(Term_name, data) + except EOFError: + Line_term.shutdown() + finally: + # Restore terminal attributes + termios.tcsetattr(pty.STDIN_FILENO, termios.TCSANOW, Term_attr) diff --git a/graphterm/ordereddict.py b/graphterm/ordereddict.py new file mode 100644 index 0000000..5b0303f --- /dev/null +++ b/graphterm/ordereddict.py @@ -0,0 +1,127 @@ +# Copyright (c) 2009 Raymond Hettinger +# +# 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. + +from UserDict import DictMixin + +class OrderedDict(dict, DictMixin): + + def __init__(self, *args, **kwds): + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__end + except AttributeError: + self.clear() + self.update(*args, **kwds) + + def clear(self): + self.__end = end = [] + end += [None, end, end] # sentinel node for doubly linked list + self.__map = {} # key --> [key, prev, next] + dict.clear(self) + + def __setitem__(self, key, value): + if key not in self: + end = self.__end + curr = end[1] + curr[2] = end[1] = self.__map[key] = [key, curr, end] + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + dict.__delitem__(self, key) + key, prev, next = self.__map.pop(key) + prev[2] = next + next[1] = prev + + def __iter__(self): + end = self.__end + curr = end[2] + while curr is not end: + yield curr[0] + curr = curr[2] + + def __reversed__(self): + end = self.__end + curr = end[1] + while curr is not end: + yield curr[0] + curr = curr[1] + + def popitem(self, last=True): + if not self: + raise KeyError('dictionary is empty') + if last: + key = reversed(self).next() + else: + key = iter(self).next() + value = self.pop(key) + return key, value + + def __reduce__(self): + items = [[k, self[k]] for k in self] + tmp = self.__map, self.__end + del self.__map, self.__end + inst_dict = vars(self).copy() + self.__map, self.__end = tmp + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def keys(self): + return list(self) + + setdefault = DictMixin.setdefault + update = DictMixin.update + pop = DictMixin.pop + values = DictMixin.values + items = DictMixin.items + iterkeys = DictMixin.iterkeys + itervalues = DictMixin.itervalues + iteritems = DictMixin.iteritems + + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, self.items()) + + def copy(self): + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + d = cls() + for key in iterable: + d[key] = value + return d + + def __eq__(self, other): + if isinstance(other, OrderedDict): + if len(self) != len(other): + return False + for p, q in zip(self.items(), other.items()): + if p != q: + return False + return True + return dict.__eq__(self, other) + + def __ne__(self, other): + return not self == other diff --git a/graphterm/otrace.py b/graphterm/otrace.py new file mode 100644 index 0000000..fc58de2 --- /dev/null +++ b/graphterm/otrace.py @@ -0,0 +1,5603 @@ +#!/usr/bin/env python +# +# otrace: An object-oriented debugger for nonlinear tracing +# +# otrace was developed as part of the Mindmeldr project. +# Documentation can be found at http://info.mindmeldr.com/code/otrace +# +# BSD License +# +# Copyright (c) 2012, Ramalingam Saravanan +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +"""An object-oriented debugger for nonlinear tracing + +Installation +============ + +Ensure that ``otrace.py`` is in the python module load path. +(For python 2.6 or earlier, you will also need ``ordereddict.py``.) + +Usage +====== + +*otrace* may be used as: + - a tracing tool for debugging web servers and interactive programs + - a console or dashboard for monitoring production servers + - a teaching tool for exploring the innards of a program + - a code patching tool for unit testing + +*otrace* does not consume any resources until some tracing action is +initiated. So it can be included in production code without any +performance penalty. +*otrace* works well with detached server processes (*daemons*) +via the GNU `screen `_ +utility that emulates a terminal. + +*otrace* is meant to be used in conjunction with an *event loop*, which +is usually present in programs that interact with users such as web +servers or GUI applications. *otrace* takes control of the terminal, +and would not work very well with programs that read user input +directly from the terminal (or standard input). + +To use *otrace*, simply ``import otrace`` and instantiate the class ``otrace.OShell``, +which provides a unix-like shell interface to interact with a running +program via the terminal. + +Here is a simple server example:: + + import BaseHTTPServer + from SimpleHTTPServer import SimpleHTTPRequestHandler + from otrace import OShell, traceassert + + http_server = BaseHTTPServer.HTTPServer(("", 8888), SimpleHTTPRequestHandler) + oshell = OShell(locals_dict=locals(), globals_dict=globals(), + new_thread=True, allow_unsafe=True, init_file="server.trc") + try: + oshell.loop() + http_server.serve_forever() # Main event loop + except KeyboardInterrupt: + oshell.shutdown() + +*Usage notes:* + + - If you run in *oshell* in its own daemon thread as shown above, use + the ^C sequence to abort the main thread, and call ``OShell.shutdown`` + from main thread to cleanup terminal I/O etc. + + - If you run *oshell* in the main thread and the event loop in a + separate thread, ^C will abort and cleanup *oshell*. You may need to + shutdown the event loop cleanly after that. + + - Install the python ``readline`` module to enable *TAB* command completion. + + - To start a detached server (daemon) process, use the command: + ``screen -d -m -S ...`` + To attach a terminal to this process, use: + ``screen -r `` + + - By default, *otrace* logs to the ``logging`` module. Subclass + ``TraceCallback``, overriding the methods ``callback`` and ``returnback`` + to implement your own logging (see ``DefaultCallback`` for a simple example) + +Synopsis +========= + +*otrace* uses a *Virtual Directory Shell Interface* which maps all the +objects in a a running python program to a virtual filesystem mounted in +the directory ``/osh`` (sort of like the unix ``/proc`` filesystem, if you are +familiar with it). Each module, class, method, function, and variable in the global namespace +is mapped to a virtual file within this directory. +For example, a class ``TestClass`` in the ``globals()`` dictionary can be accessed as:: + + /osh/globals/TestClass + +and a method ``test_method`` can be accessed as:: + + /osh/globals/TestClass/test_method + +and so on. + +*otrace* provides a unix shell-like interface, *oshell*, with commands +such as ``cd``, ``ls``, ``view``, and ``edit`` that can be used navigate, view, +and edit the virtual files. Editing a function or method +"`monkey patches `_" it, +allowing the insertion of ``print`` statements etc. in the running program. + +The ``trace`` command allows dynamic tracing of function or method invocations, +return values, and exceptions. This is accomplished by +dynamically *decorating* (or *wrapping*) the function to be traced. +When a trace condition is satisfied, the function-wrapper saves *context information*, such as +arguments and return values, in a newly created virtual directory in:: + + /osh/recent/* + +These *trace context* directories can be navigated just like +``/osh/globals/*``. (If there are too many trace contexts, the oldest +ones are deleted, unless they have been explicitly *saved*.) + +*oshell* allows standard unix shell commands to be interspersed with +*oshell*-specific commands. The path of the "current working directory" +determines which of the these two types of commands will be executed. +If the current working directory is not in ``/osh/*``, the command is +treated as a standard unix shell command (except for ``cd``, which is +always handled by *oshell*.) + +Credits +======== + +*otrace* was developed as part of the `Mindmeldr `_ project, which is aimed at improving classroom interaction. + +*otrace* was inspired by the following: + - the tracing module `echo.py `_ written by Thomas Guest . This nifty little program uses decorators to trace function calls. + + - the python ``dir()`` function, which treats objects as directories. If objects are directories, then shouldn't we be able to inspect them using the familiar ``cd`` and ``ls`` unix shell commands? + + - the unix `proc `_ filesystem, which cleverly maps non-file data to a filesystem interface mounted at ``/proc`` + + - the movie `Being John Malkovich `_ (think of ``/osh`` as the portal to the "mind" of a running program) + +License +========= +*otrace* is distributed as open source under the `BSD-license `_. + +""" + +from __future__ import with_statement + +import cgi +import cgitb +import codeop +import collections +import copy +import cPickle +import datetime +import functools +import hashlib +import hmac +import inspect +import logging +import logging.handlers +import os +import os.path +import pprint +import Queue +import random +import re +import select +import shlex +import sqlite3 +import StringIO +import struct +import subprocess +import sys +import tempfile +import threading +import time +import traceback +import types +import urllib +import weakref + +Banner_messages = [] + +# Save terminal attributes before using readline +# (Need this to restore terminal attributes after abnormal exit from oshell thread) +try: + import termios + Term_attr = termios.tcgetattr(sys.stdin.fileno()) +except Exception: + Term_attr = None + +try: + import readline # this allows raw_input to handle line editing + if readline.__doc__ and "libedit" in readline.__doc__: + # http://stackoverflow.com/questions/7116038/python-tab-completion-mac-osx-10-7-lion + readline.parse_and_bind("bind ^I rl_complete") +except Exception: + readline = None + mod_name = "pyreadline" if sys.platform.startswith("win") else "readline" + Banner_messages.append(" Please install '%s' module for TAB completion (e.g. 'easy_install %s')" % (mod_name, mod_name)) + + +try: + from collections import OrderedDict +except ImportError: + from ordereddict import OrderedDict +try: + from collections import MutableMapping +except ImportError: + from UserDict import DictMixin as MutableMapping + + +OTRACE_VERSION = "0.30" + +__all__ = ["OTrace", "OShell", "OTraceException"] + +EXEC_TIMEOUT = 10 # Execution timeout (in sec) +REPEAT_COUNT = 10000 # Default repeat count + +# Path separator +PATH_SEP = "/" + +BACKSLASH = "\\" + +ENTITY_CHAR = ":" + +BASE_DIR = "osh" + +BASE_OFFSET = 1 +BASE1_OFFSET = 2 +BASE2_OFFSET = 3 +TRACE_OFFSET = 6 + +MAX_PICKLE_CHECK_DEPTH = 3 # Max depth to check for pickleability +MAX_PICKLE_DATA_LENGTH = 10000 # Max length for individual pickled component data length + +ALL_DIR = "all" +BROWSER_DIR = "browser" +DATABASE_DIR = "db" +GLOBALS_DIR = "globals" +LOCALS_DIR = "locals" +PATCHES_DIR = "patches" +PICKLED_DIR = "pickled" +RECENT_DIR = "recent" +SAVED_DIR = "saved" +WEB_DIR = "web" + +LAZY_DIRS = [DATABASE_DIR, PICKLED_DIR] # Lazy data loading directories + +DIR_LIST = [ALL_DIR, BROWSER_DIR, DATABASE_DIR, GLOBALS_DIR, LOCALS_DIR, PATCHES_DIR, PICKLED_DIR, RECENT_DIR, SAVED_DIR, WEB_DIR] + +OT_DIRS = set(DIR_LIST) + +DIR_PREFIX = dict((dir_name, PATH_SEP + BASE_DIR + PATH_SEP + dir_name + PATH_SEP) for dir_name in DIR_LIST) + +BREAK_ACTIONS = ["break", "ipdb", "pdb"] +TRACE_ACTIONS = BREAK_ACTIONS + ["hold", "tag"] + +TRACE_INFO = "__trc" +DOWN_STACK = "__down" +UP_STACK = "__up" +SHOW_HIDDEN = set(["__call__", TRACE_INFO, DOWN_STACK, UP_STACK]) +IGNORE_FUNCNAMES = set(["otrace_function_call", "otrace_wrapped"]) + +ASSIGNMENT_RE = re.compile(r"\s*[a-zA-Z][a-zA-Z0-9_\.]*\s*=[^=]") + +EXEC_PREFIX = "!" + +NEWCONTEXT_PREFIX = "~~" +GLOBALS_PREFIX = "~~g" +WORKDIR_PREFIX = "~~w" + +TRACE_ID_PREFIX = "~" +TRACE_ID_SEP = ":" +TRACE_LABEL_PREFIX = ":" +TRACE_LOG_PREFIX = "log:" + +CLEAR_SCREEN_SEQUENCE = "\x1b[H\x1b[J" # [H[J +ALT_SCREEN_ONSEQ = "\x1b[?1049h" +ALT_SCREEN_OFFSEQ = "\x1b[?1049l" + +INAME = 0 +ISUBDIR = 1 + +FILE_EXTENSIONS = {"css": "css", "htm": "html", "html": "html", "js": "javascript", "py": "python", + "xml": "xml"} + +DOC_URL = "http://info.mindmeldr.com/code/otrace" +DEFAULT_BANNER = """ ***otrace object shell (v%s)*** (type 'help' for info)""" % OTRACE_VERSION + +Help_params = OrderedDict() +Help_params["allow_xml"] = "Allow output markup for display in browser (if supported)" +Help_params["append_traceback"] = "Append traceback information to exceptions" +Help_params["assert_context"]= "No. of lines of context retrieved for traceassert (0 for efficiency)" +Help_params["auto_lock"] = "Automatically lock after specified idle time (in seconds), if password is set" +Help_params["deep_copy"] = "Create deep copies of arguments and local variables for 'snapshots'" +Help_params["editor"] = "Editor to use for editing patches or viewing source" +Help_params["exec_lock"] = "Execute code within re-entrant lock" +Help_params["log_format"] = "Format for log messages" +Help_params["log_level"] = "Logging level (10=>DEBUG, 20=>INFO, 30=>WARNING ...; see logging module)" +Help_params["log_remote"] = "IP address or domain (:port) for remote logging (default port: 9020)" +Help_params["log_truncate"] = "No. of characters to display for log messages (default: 72)" +Help_params["max_recent"] = "Maximum number of entries to keep in /osh/recent" +Help_params["osh_bin"] = "Path to prepend to $PATH to use custom commands" +Help_params["password"] = "Encrypted access password (use otrace.encrypt_password to create it)" +Help_params["pickle_file"] = "Name of file to save pickled trace contexts" +Help_params["pretty_print"] = "Use pprint.pformat rather than print to display expressions" +Help_params["repeat_interval"] = "Command repeat interval (sec)" +Help_params["safe_mode"] = "Safe mode (disable code modification and execution)" +Help_params["save_tags"] = "Automatically save all tag contexts" +Help_params["trace_active"] = "Activate tracing (can be used to force/suppress tracing)" +Help_params["trace_related"] = "Automatically trace calls related to tagged objects" +Help_params["unpickle_file"] = "Name of file to read pickled trace contexts from" + +Set_params = {} +Set_params["allow_xml"] = True +Set_params["append_traceback"] = False +Set_params["assert_context"] = 0 +Set_params["auto_lock"] = 0 +Set_params["deep_copy"] = False +Set_params["editor"] = "" +Set_params["exec_lock"] = False +Set_params["log_format"] = None # placeholder +Set_params["log_level"] = None # placeholder +Set_params["log_remote"] = None # placeholder +Set_params["log_truncate"] = None # placeholder +Set_params["max_recent"] = 10 +Set_params["osh_bin"] = "" +Set_params["password"] = "" +Set_params["pickle_file"] = "" +Set_params["pretty_print"] = False +Set_params["repeat_interval"] = 0.2 +Set_params["safe_mode"] = True +Set_params["save_tags"] = False +Set_params["trace_active"] = None # placeholder +Set_params["trace_related"]= False +Set_params["unpickle_file"]= None # placeholder + +Trace_rlock = threading.RLock() +Pickle_rlock = threading.RLock() + +if sys.version_info[0] < 3: + def encode(s): + return s + def decode(s): + return s +else: + def encode(s): + return s.encode("utf-8") + def decode(s): + return s.decode("utf-8") + +def encrypt_password(password, salt=None, hexdigits=16): + """Encrypt password, returning encrypted password prefixed with salt, + which defaults to a random value + """ + if not salt: + salt = "%015d" % random.randrange(0, 10**15) + elif ":" in salt: + raise Exception("Colon not allowed in salt") + encrypted = hmac.new(encode(salt), encode(password), digestmod=hashlib.sha256).hexdigest()[:hexdigits] + return salt+":"+encrypted + +def verify_password(password, encrypted_password): + salt, sep, _ = encrypted_password.partition(":") + if not salt: + raise Exception("No salt in encrypted password") + return encrypted_password == encrypt_password(password, salt=salt) + +class OSDirectory(object): + def __init__(self, path=None): + self.path = path + +class AltHandler(Exception): + pass + +def is_absolute_path(path): + if path.startswith(PATH_SEP): + return True + if os.sep == BACKSLASH and re.match(r"[a-zA-Z]:", path): + # Windows absolute path + return True + return False + +def os_path(path): + """Convert unix-style path to OS path""" + if os.sep == BACKSLASH: + # Windows path + if path.startswith(PATH_SEP): + comps = path[1:].split(PATH_SEP) + comps[0] = "c:\\" + comps[0] + else: + comps = path.split(PATH_SEP) + path = os.path.join(*comps) + return path + +def expanduser(filepath): + if filepath.startswith(GLOBALS_PREFIX): + filepath = DIR_PREFIX[GLOBALS_DIR][:-1]+filepath[len(GLOBALS_PREFIX):] + + elif filepath.startswith(WORKDIR_PREFIX): + dir_path = (OShell.instance and OShell.instance.work_dir) or os.getcwd() + filepath = dir_path+filepath[len(WORKDIR_PREFIX):] + + elif filepath.startswith(NEWCONTEXT_PREFIX): + # Change to top directory of newest context + return PATH_SEP + PATH_SEP.join(OTrace.recent_pathnames) + + return os.path.expanduser(filepath) + +def expandpath(filepath): + """Return expanded filepath (absolute path or assumed relative to work directory)""" + return expanduser(filepath if is_absolute_path(filepath) or filepath.startswith(NEWCONTEXT_PREFIX) or filepath.startswith("~") else WORKDIR_PREFIX+PATH_SEP+filepath) + +def otrace_pformat(*args, **kwargs): + if Set_params["pretty_print"]: + return "\n".join(pprint.pformat(arg, **kwargs) for arg in args) + else: + return " ".join(str(arg) for arg in args) + +def de_indent(lines): + """Remove global indentation""" + out_lines = [] + indent = None + for line in lines: + temline = line.lstrip() + if indent is None and temline and not temline.startswith("#") and not temline.startswith("@"): + # First non-blank, non-comment, non-decorator line; initialize global indentation + indent = len(line) - len(temline) + + if indent and len(line) - len(temline) >= indent: + # Strip global indentation from line + out_lines.append(line[indent:]) + elif not temline.startswith("@"): + # Skip leading decorators (like @classmethod, @staticmethod) + # to get code for pure function + out_lines.append(line) + return out_lines + +def pythonize(args): + """Convert shell-style space-separated, unquoted arguments to + python-style comma-separated, quoted arguments""" + arg_list = [] + for arg in args: + if "=" in arg: + kw, sep, arg = arg.partition("=") + prefix = kw+sep + else: + prefix = "" + if arg and (arg.isdigit() or arg[0] in "+-" and arg[1:].isdigit()): + arg_list.append(prefix+arg) + else: + arg_list.append(prefix+repr(arg)) + return ", ".join(arg_list) + +def strip_compare_op(prop_name): + """Return (stripped_prop_name, compare_op) for suffixed property names of the form "arg1!=", "arg2<" etc.""" + cmp_op = "" + if prop_name.endswith("="): + cmp_op = "=" + prop_name = prop_name[:-1] + if prop_name[-1] in ("=", "!", "<", ">"): + cmp_op = prop_name[-1] + cmp_op + prop_name = prop_name[:-1] + + if not cmp_op or cmp_op == "=": + cmp_op = "==" + + return (prop_name.strip(), cmp_op) + +def compare(value1, op_str, value2): + """Return True or False for comparison using operator.""" + return ( (op_str == "==" and value1 == value2) or + (op_str == "!=" and value1 != value2) or + (op_str == "<=" and value1 <= value2) or + (op_str == ">=" and value1 >= value2) or + (op_str == "<" and value1 < value2) or + (op_str == ">" and value1 > value2) ) + +def match_parse(match_str, delimiter=","): + """Parse match dict components of the form: var1.comp1==value1,var2!=value2,... where values with commas/spaces must be quoted.""" + match_dict = {} + invalid_tokens = [] + lex = shlex.shlex(match_str, None, True) + lex.whitespace = delimiter + lex.whitespace_split = True + while True: + token = lex.get_token() + if token is None: + break + if not token: + continue + re_match = re.match(r"^([\w\.]+)(==|!=|<=|>=|<|>)(.+)$", token) + if not re_match: + invalid_tokens.append(token) + continue + value = re_match.group(3) + if value and (value[0].isdigit() or value[0] in "+-" and value[1:2].isdigit()): + try: + if "." in value: + value = float(value) + else: + value = long(value) + except Exception: + invalid_tokens.append(token) + value = None + elif value in ("True", "False"): + value = (value == "True") + elif value == "None": + value = None + + match_dict[re_match.group(1)+re_match.group(2)] = value + + if invalid_tokens and re.search(r"[=<>]", match_str): + raise Exception("Invalid match components: " + ",".join(invalid_tokens)) + + return match_dict + + +def get_obj_properties(value, full_path=None): + """Return (python_mime_type, command) for object value""" + opts = "" + if full_path and OShell.instance: + + if len(full_path) > BASE_OFFSET and full_path[BASE_OFFSET] in OShell.instance.lazy_dirs: + # In database + base_subdir = full_path[BASE_OFFSET] + if len(full_path) == BASE1_OFFSET+OShell.instance.lazy_dirs[base_subdir].root_depth: + # Need to "cdls" to load database entries + return ("object", "cdls") + opts += " -v" + + if value is None or isinstance(value, (basestring, bool, complex, float, int, list, tuple)): + return ("value", "pr") + elif inspect.isfunction(value) or inspect.ismethod(value): + return ("function", "view -i") + else: + return ("object", "cdls"+opts) + + +def format_traceback(exc_info=None): + """Return string describing exception.""" + try: + etype, value, tb = exc_info if exc_info else sys.exc_info() + tblist = traceback.extract_tb(tb) + del tblist[:1] # Remove self-reference to tracing code in traceback + fmtlist = traceback.format_list(tblist) + if fmtlist: + fmtlist.insert(0, "Traceback (most recent call last):\n") + fmtlist[len(fmtlist):] = traceback.format_exception_only(etype, value) + finally: + tblist = tb = None + return "".join(fmtlist) + +def get_naked_function(method): + """Return function object associated with a method.""" + if inspect.isfunction(method): + return method + return getattr(method, "__func__", None) + +def ismethod_or_function(method): + return inspect.isfunction(get_naked_function(method)) + +def get_method_type(parent_cls, method): + """Return 'instancemethod'/'classmethod'/'staticmethod' """ + # Get class attribute directly + attr_value = parent_cls.__dict__[method.__name__] + if inspect.isfunction(attr_value): + # Undecorated function => instance method + return "instancemethod" + # Decorated function; return type + return type(attr_value).__name__ + +class OTraceException(Exception): + pass + +class TraceDict(dict): + # Subclass dict to allow a weak reference to be created to it + # Also implements *_trc methods for trace info + def has_trc(self, trace_attr): + """Returns True if self[TRACE_INFO] has attribute.""" + return TRACE_INFO in self and trace_attr in self[TRACE_INFO] + + def get_trc(self, trace_attr, default=None): + """Return self[TRACE_INFO][trace_attr] or default.""" + if TRACE_INFO in self: + return self[TRACE_INFO].get(trace_attr, default) + else: + return default + + def set_trc(self, trace_attr, value): + """Set self[TRACE_INFO][trace_attr] to value.""" + if TRACE_INFO not in self: + self[TRACE_INFO] = {} + self[TRACE_INFO][trace_attr] = value + +class MappingDict(MutableMapping): + pass + +class ObjectDict(MappingDict): + """Wrapper to make an object appear like a dict.""" + def __init__(self, obj): + self._obj = obj + + def copy(self): + return ObjectDict(self._obj) + + def keys(self): + return dir(self._obj) + + def __contains__(self, key): + return hasattr(self._obj, key) + + def __getitem__(self, key): + if not hasattr(self._obj, key): + raise KeyError(key) + return getattr(self._obj, key) + + def __iter__(self): + return self.keys().__iter__ + + def __len__(self): + return len(self.keys()) + + def __setitem__(self, key, value): + setattr(self._obj, key, value) + + def __delitem__(self, key): + if not hasattr(self._obj, key): + raise KeyError(key) + delattr(self._obj, key) + + +class ListDict(MappingDict): + """Wrapper to make a list/tuple appear like a dict.""" + def __init__(self, lst): + self._lst = lst + + def copy(self): + return ListDict(self._lst) + + def keys(self): + return [str(x) for x in range(len(self._lst))] + + def __contains__(self, key): + try: + key = int(key) + except Exception: + return False + return (key >= 0) and (key < len(self._lst)) + + def __getitem__(self, key): + if not self.__contains__(key): + raise KeyError(key) + return self._lst[int(key)] + + def __iter__(self): + for x in self.keys(): + yield x + + def __len__(self): + return len(self._lst) + + def __setitem__(self, key, value): + if not self.__contains__(key): + raise KeyError(key) + self._lst[int(key)] = value + + def __delitem__(self, key): + if not self.__contains__(key): + raise KeyError(key) + del self._lst[int(key)] + +class LineList(list): + def __str__(self): + s = [str(x) for x in self] + return "".join(x if x.endswith("\n") else x+"\n" for x in s) + +def setTerminalEcho(enabled): + if not Term_attr: + return + fd = sys.stdin.fileno() + iflag, oflag, cflag, lflag, ispeed, ospeed, cc = termios.tcgetattr(fd) + if enabled: + lflag |= termios.ECHO + else: + lflag &= ~termios.ECHO + new_attr = [iflag, oflag, cflag, lflag, ispeed, ospeed, cc] + termios.tcsetattr(fd, termios.TCSANOW, new_attr) + +### http://stackoverflow.com/questions/566746/how-to-get-console-window-width-in-python + +def getTerminalSize(): + """Return (lines:int, cols:int)""" + if not Term_attr: + return(25, 80) + + def ioctl_GWINSZ(fd): + import fcntl + return struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234")) + + # Try stdin, stdout, stderr + for fd in (0, 1, 2): + try: + return ioctl_GWINSZ(fd) + except Exception: + pass + # Try os.ctermid() + try: + fd = os.open(os.ctermid(), os.O_RDONLY) + try: + return ioctl_GWINSZ(fd) + finally: + os.close(fd) + except Exception: + pass + # Try `stty size` (commented out to avoid popen) + ##try: + ## return tuple(int(x) for x in os.popen("stty size", "r").read().split()) + ##except Exception: + ## pass + # + # Try environment variables + try: + return tuple(int(os.getenv(var)) for var in ("LINES", "COLUMNS")) + except Exception: + pass + # return default. + return (25, 80) + +def read_password(prompt="Password:"): + """Read password, with no echo, from stdin""" + setTerminalEcho(False) + try: + password = raw_input(prompt) + except ValueError: + raise EOFError + sys.stdout.write("\n") + setTerminalEcho(True) + return password + +def check_for_hold(self_arg): + """Return callable function if self_arg has a hold, else return None.""" + return getattr(self_arg, OTrace.hold_attr, None) + +def resume_from_hold(self_arg): + # Executed in otrace thread; should insert resume callback in event loop and return immediately + if hasattr(self_arg, OTrace.resume_attr): + callback = getattr(self_arg, OTrace.resume_attr) + delattr(self_arg, OTrace.resume_attr) + if callback: + try: + callback() + return True + except Exception: + return False + return False + +class TraceInterpreter(object): + """Class to execute code interactively using argument values as the local context.""" + def __init__(self): + self.compile = codeop.CommandCompiler() + + def evaluate(self, expression, locals_dict={}, globals_dict={}, print_out=False): + """ Evaluate expression; return output string, if print_out is True. + + Returns tuple (out_str, err_str), with err_str == "" for successful execution, + and err_str == None for incomplete expression + """ + + _stdout = StringIO.StringIO() + _stderr = StringIO.StringIO() + locals_dict["_stdout"] = _stdout + locals_dict["_stderr"] = _stderr + + prefix = "print >>_stdout, " if print_out else "" + result = self.exec_source(prefix+expression, locals_dict=locals_dict, globals_dict=globals_dict) + + del locals_dict["_stdout"] + del locals_dict["_stderr"] + + out_str = _stdout.getvalue() + err_str = _stderr.getvalue() + + _stdout.close() + _stderr.close() + + if result is None: + return ("", None) + + return (out_str, err_str + result) + + def exec_source(self, source, filename="", symbol="single", locals_dict={}, globals_dict={}): + """Execute source code. + + Returns None if code is incomplete, + null string on successful execution of code, + or a string describing a syntax error or other exeception. + All exceptions, excepting for SystemExit, are caught. + """ + try: + # Compile code + code = self.compile(source, filename, symbol) + except (OverflowError, SyntaxError, ValueError): + # Syntax error + etype, value, last_traceback = sys.exc_info() + return ".".join(traceback.format_exception_only(etype, value)) + + if code is None: + # Incomplete code + return None + + try: + if globals_dict is locals_dict: + exec code in locals_dict + else: + exec code in globals_dict, locals_dict + return "" # Successful execution + except SystemExit: + raise + except Exception: + return format_traceback() # Error in execution + +class TraceConsole(object): + """Console for trace interpreter (similar to code.interact). + + Runs in separate thread (as a daemon) + """ + prompt1 = "> " + prompt2 = "... " + + def __init__(self, globals_dict={}, locals_dict={}, banner=DEFAULT_BANNER, + echo_callback=None, db_interface=None, web_interface=None, no_input=False, new_thread=False, + _stdin=sys.stdin, _stdout=sys.stdout, _stderr=sys.stderr): + """Create console instance. + + If echo_callback is specified, it should be a callable, + and it will be called with stdout and stderr string data to echo output. + If new_thread, a new (daemon) thread is created for TraceConsole, + else TraceConsole runs in current thread, blocking it. + """ + self.globals_dict = globals_dict + self.locals_dict = locals_dict + self.banner = banner + self.echo_callback = echo_callback + self.lazy_dirs = {PICKLED_DIR: PickleInterface} + if db_interface: + self.lazy_dirs[DATABASE_DIR] = db_interface + self.web_interface = web_interface + self.no_input = no_input + self.thread = threading.Thread(target=self.run) if new_thread else None + if self.thread: + self.thread.setDaemon(True) + + self._stdin = _stdin + self._stdout = _stdout + self._stderr = _stderr + + self.interpreter = TraceInterpreter() + self.resetbuffer() + + self.feed_lines = [] + self.last_input_time = 0 + + self.set_repeat(None) + self.repeat_alt_screen = 0 + + self.started = False + self.suspend_input = False + self.shutting_down = False + + def set_repeat(self, line=None): + if line and Set_params["repeat_interval"]: + self.repeat_line = line + self.repeat_count = REPEAT_COUNT + self.repeat_interval = Set_params["repeat_interval"] + else: + self.repeat_line = None + self.repeat_count = 0 + self.repeat_interval = None + + def has_trc(self, trace_attr): + """ Return True if self.locals_dict[TRACE_INFO] has attribute.""" + return self.locals_dict and TRACE_INFO in self.locals_dict and trace_attr in self.locals_dict[TRACE_INFO] + + def get_trc(self, trace_attr, default=None): + """ Return self.locals_dict[TRACE_INFO][trace_attr] or default.""" + if self.locals_dict and TRACE_INFO in self.locals_dict: + return self.locals_dict[TRACE_INFO].get(trace_attr, default) + else: + return default + + def set_trc(self, trace_attr, value): + """ Set self.locals_dict[TRACE_INFO][trace_attr] to value.""" + if TRACE_INFO not in self.locals_dict: + self.locals_dict[TRACE_INFO] = {} + self.locals_dict[TRACE_INFO][trace_attr] = value + + def loop(self): + """Start run loop.""" + if self.thread: + self.thread.start() + else: + self.run() + + def run(self): + self.started = True + banner = self.banner + for msg in Banner_messages: + banner += "\n" + msg + banner += "\n ^C to terminate program" + self.interact(banner) + + def resetbuffer(self): + self.buffer = [] + + @classmethod + def invoke_debugger(cls, action="pdb"): + if cls.instance: + cls.instance.suspend_input = True + + if action == "ipdb": + try: + import ipdb + ipdb.set_trace() + except ImportError: + import pdb + pdb.set_trace() + else: + import pdb + pdb.set_trace() + + if cls.instance: + cls.instance.suspend_input = False + + def interact(self, banner=""): + if banner: + self.std_output("%s\n" % str(banner)) + + more = False + noprompt = False + while not self.shutting_down: + try: + if noprompt or self.repeat_interval: + prompt = "" + elif more: + prompt = self.prompt2 + else: + prompt = self.prompt1 + + try: + if self.feed_lines: + # Read from feed buffer + line = self.feed_lines.pop(0) + if line and line[-1] == "\n": + line = line[:-1] + if line and line[-1] == "\r": + line = line[:-1] + + if line.startswith("noecho "): + line = line[len("noecho "):] + else: + # Echo line + self.std_output(prompt+line+"\n") + else: + if self.no_input: + return + if self.suspend_input: + self.std_output("***OShell suspended for pdb\n") + while self.suspend_input: + time.sleep(1) + # Read input line + line = self.std_input(prompt, timeout=self.repeat_interval) + if self.repeat_interval: + if line is not None: + self.set_repeat(None) + else: + line = self.repeat_line + self.repeat_count -= 1 + if self.repeat_count <= 0: + self.set_repeat(None) + if line is None: + continue + except EOFError: + self.std_output("\n") + break + + try: + noprompt = False + out_str, err_str = self.parse(line, batch=bool(self.repeat_interval)) + if out_str == "_NoPrompt_": + noprompt = True + out_str = None + else: + if self.repeat_interval: + if err_str: + self.set_repeat(None) + elif out_str: + out_str = CLEAR_SCREEN_SEQUENCE + out_str + if self.repeat_alt_screen == 1: + self.repeat_alt_screen = 2 + out_str = ALT_SCREEN_ONSEQ + out_str + else: + if self.repeat_alt_screen == 2: + self.repeat_alt_screen = 0 + out_str = ALT_SCREEN_OFFSEQ + out_str + + more = (err_str is None) + if not more and out_str: + if out_str[-1] == "\n": + self.std_output(out_str) + else: + self.std_output(out_str+"\n") + + if err_str: + self.err_output(err_str+"\n") + except Exception: + self.err_output(format_traceback()) + + except KeyboardInterrupt: + if not self.thread: + raise + self.resetbuffer() + more = False + self.std_output("\nKeyboardInterrupt: Type Control-D to quit\n") + self.shutdown() + + def stuff_lines(self, lines): + """Accept list of lines for processing as input (skipping lines starting with '#')""" + self.feed_lines += [line for line in lines if line.strip() and line.strip()[0] != "#"] + + def close(self): + """Closes console input (thread-safe).""" + if not self.started: + return + + try: + self._stdin.close() + except Exception: + pass + + def shutdown(self): + # Override in subclass, if needed + if self.shutting_down: + return + self.shutting_down = True + if self.thread: + if self.thread.isAlive(): + try: + self.thread._Thread__stop() + print >> sys.stderr, "Killed TraceConsole thread" + except Exception: + pass + + # Restore terminal attributes + if Term_attr: + termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, Term_attr) + + def switch_screen(self, logtype=""): + """Return (prefix, suffix) escape sequences for alt screen switching""" + if logtype != "trace": + if self.repeat_alt_screen == 2: + return ALT_SCREEN_OFFSEQ, ALT_SCREEN_ONSEQ + else: + return "", "" + + # Trace log; end repeat command + if self.repeat_interval: + self.set_repeat(None) + + if self.repeat_alt_screen == 2: + # Exit alt screen + self.repeat_alt_screen = 0 + return ALT_SCREEN_OFFSEQ, "" + else: + return "", "" + + def parse(self, line): + # Override in subclass, if needed + return self.push(line) + + def push(self, line, batch=False): + """Execute command line. + + The line should not have a trailing newline; it may have internal newlines. + Returns (out_str, err_str) + If batch, a complete line of non-interactive expression is evaluated, + without outputting to stdout. + If not batch, output is sent to stdout. + out_str is None if not batch, else it contains the evaluated expression output. + err_str is None if code is incomplete, + is null string on successful execution of code, + or a string describing a syntax error or other exeception. + """ + if not Set_params["exec_lock"]: + return self.push_aux(line, batch=batch) + + with Trace_rlock: + return self.push_aux(line, batch=batch) + + def push_aux(self, line, batch=False): + if batch: + self.resetbuffer() + out_str, err_str = self.interpreter.evaluate(line, locals_dict=self.locals_dict, + globals_dict=self.globals_dict, print_out=True) + else: + self.buffer.append(line) + source = "\n".join(self.buffer) + out_str = None + err_str = self.interpreter.exec_source(source, locals_dict=self.locals_dict, + globals_dict=self.globals_dict) + if err_str is not None: + self.resetbuffer() + return out_str, err_str + + def std_output(self, data, flush=False): + self._stdout.write(data) + if self.echo_callback: + self.echo_callback(data) + if flush: + self._stdout.flush() + + def err_output(self, data, flush=False): + self._stderr.write(data) + if self.echo_callback: + self.echo_callback(data) + if flush: + self._stderr.flush() + + def std_input(self, prompt="", timeout=None): + """Override for a different input source. + If not timeout and sys.stdin, use raw_input, else use select. + Returns None on timeout and raises EOFError on close + """ + if Set_params["auto_lock"] and not timeout: + timeout = Set_params["auto_lock"] + + if not timeout and self._stdin is sys.stdin: + try: + input_data = raw_input(prompt) + except ValueError: + raise EOFError + self.last_input_time = time.time() + return input_data + + if prompt: + self._stdout.write(prompt) + self._stdout.flush() + + # Note: using select messes up the prompt! + read_list, _, _ = select.select([self._stdin], [], [], timeout) + if read_list: + self.last_input_time = time.time() + return self._stdin.readline() + elif Set_params ["password"] and Set_params["auto_lock"] and (time.time() - self.last_input_time) > Set_params["auto_lock"]: + # Trigger auto lock + return "lock\n" + else: + return None + +class OShell(TraceConsole): + """Object-oriented shell + """ + html_fmt = '%s' + + commands = { +"alias": +"""alias name command ... # Define alias name for command + +alias name # Display alias for name +alias name "" # Clear alias for name +""", + +"cd": +"""cd [path] # Change current working directory to "path" + +Special paths: ".."=>parent, "/"=>root, "~"=>home, "~~"=>most recent trace, + "~~g"=>/osh/globals, "~~w"=>work directory +If current directory is /osh/*, user input is interpreted as oshell commands. +If current directory is not /osh/*, user input is interpreted as unix shell commands. + +If no path is specified, current default path is used, as determined below: +If in a trace context (/osh/recent/*), the default path is the topmost directory of the trace context. +If in /osh/*, the default path is /osh/globals. +If not in /osh/*, the default path is the user's home directory (~). +""", + +"cdls": +"""cdls [pathname] # cd to "pathname" and list "files" (cd+ls)""", + +"del": +"""del [trace_id1..] # Delete trace context""", + +"dn": +"""dn # Command alias to move one level down in stack frames in a trace context (to a newer frame)""", + +"edit": +"""edit [-f] (filename|class[.method]) [< readfile] # Edit/patch file/method/function + +Names of patched methods are saved is /osh/patches. +Specify "-f" option to modify already patched code. +If readfile is specified for input redirection, read text from file to patch code. +""", + +"exec": +'exec python_code # Execute python code (also '+EXEC_PREFIX+""") """, + +"help": +"""help [command|*] # Display help information + +help * # Help info on all commands +help command # Help info on command +""", + +"lock": +"""lock # Lock terminal until password is entered""", + +"ls": +"""ls [-acflmtv] [-(.|..|.baseclass)] [pathname1|*] # List pathname values (or all pathnames in current "directory") + +pathname can be omitted or include simple shell wildcard patterns (e.g., "*", "abc*", ...) +-a List special attributes of the form __*__ +-c List classes +-f List function/method names +-l "Long listing" => name=value ... +-m List modules +-t Order trace context listing by time +-v List variables + +-. Exclude attributes of current class +-.. Exclude attributes of parent class +- Exclude attributes of baseclass +""", + +"popd": +"""popd [-a] # Pop directory stack and revert to previous directory""", + +"pr": +"""pr python_expression # Print value of expression (DEFAULT COMMAND) + +The command prefix "pr" may be omitted, and is assumed by default. +""", + +"pushd": +"""pushd [path] # Push current directory into stack and cd to path""", + +"pwd": +"""pwd [-a] # Print current working "directory" + +-a Print all paths in stack; top first +""", + +"quit": +"""quit # Quit shell""", + +"repeat": +"""repeat command # Repeat command till new user input is received""", + +"resume": +"""resume [trace_id1..] # Resume from breakpoint""", + +"rm": +"""rm [-r] [pathname1..] # Delete entities corresponding to pathnames (if supported) + +-r recursive remove all child entities +""", + +"save": +"""save [trace_id1..] # Save current or specified trace context + +The trace context is saved to /osh/saved +""", + +"set": +"""set [parameter [value]] # Set (or display) parameter + +set # Display all parameter values +set parameter # Display current value of parameter +set parameter "" # Clear parameter value + +PARAMETERS""", + +"source": +"""source filename # Read commands from file""", + +"swapd": +"""swapd # Swap current work dir with top of directory stack""", + +"tag": +"""tag [(object|.) [tag_str|id|time]] # Tag object for tracing (default tag: id(object))""", + +"trace": +"""trace [-a (break|ipdb|pdb|hold|tag)] [-c call|return|all|tag|comma_sep_arg_match_conditions] [-n +/-count] ([class.][method]|db_key|*) # Enable tracing for class/method/key on matching condition + +-a break|ipdb|pdb|hold|tag Action to be taken when trace condition is satisfied: + break => stop until resume command + ipdb => start ipdb + pdb => start pdb + hold => asynchronously hold this request (if supported) + tag => tag self argument on method return using a string describing matched trace conditions +-c call|return|all|tag|comma_sep_arg_match_conditions Condition to match for tracing: + call => before function call, + return => after function return, + all => before call and and after return, + tagged[argname] => match if any (or argname) argument has a tag + argname1.comp1==value1,argname2!=value2,... => on argument value match (values with commas/spaces must be quoted; the special argument name 'return' may also be used) +-n +/-count If count > 0, stop tracing after count matches; if count > 0, start tracing after -count matches +""", + +"unpatch": +"""unpatch class[.method]|* [> savefile] # Unpatch method (and save patch to file) + +In directory /osh/patches, "unpatch *" will unpatch all currently patched methods. +""", + +"unpickle": +"""unpickle filename [field=value] # Read pickled trace contexts from file """, + +"untag": +"""untag [object|.] # Untag object""", + +"untrace": +"""untrace ([class.][method]|*|all) # Disable tracing for class/method""", + +"up": +"""up # Command alias to move one level up in stack frames in a trace context (to an older frame)""", + +"view": +"""view [-d] [-i] [class/method/file] # Display source/doc using editor for objects/traces/files + +-d Display docstring only +-i Display output inline, i.e., without using an editor. +""", + +} + + instance = None + + def __new__(cls, *args, **kwargs): + cls.instance = super(OShell, cls).__new__(cls) + return cls.instance + + def __init__(self, globals_dict={}, locals_dict={}, init_func=None, + init_file=None, allow_unsafe=False, work_dir="", + add_env={}, banner=DEFAULT_BANNER, no_input=False, + new_thread=False, echo_callback=None, + db_interface=None, web_interface=None, hold_wrapper=None, + eventloop_callback=None, _stdin=sys.stdin, _stdout=sys.stdout, _stderr=sys.stderr): + """Create OShell instance, but do not start the run loop (use OShell.loop for that). + + Args: + globals_dict: dictionary of global variables (usually globals()) + locals_dict: dictionary of local variables (usually locals()) + init_func: function to be invoked after initialazation but before + the run loop starts; init_func is provided a single + argument, the initialized OShell instance + init_file: name of file with initial commands to be executed + allow_unsafe: allow "unsafe" operations such as assignments, + function calls, and code patching + work_dir: absolute path of file directory to switch to + for special directory ~~w (default: program work directory) + add_env: dict of environment variables to be added for shell + command execution + banner: string to display in terminal at startup + db_interface: database interface class + web_interface: websocket interface class + hold_wrapper: wrapper class for yielding holds [e.g., yield hold_wrapper(callback_func)] + eventloop_callback: schedules callback; has signature eventloop_callback(callback_function) + _stdin, _stdout, _stder: input, output, error streams + """ + + assert not work_dir or is_absolute_path(work_dir), "work_dir must be an absolute path" + + super(OShell, self).__init__(globals_dict=globals_dict, locals_dict=locals_dict, + banner=banner, echo_callback=echo_callback, + db_interface=db_interface, web_interface=web_interface, + no_input=no_input, new_thread=new_thread, + _stdin=_stdin, _stdout=_stdout, _stderr=_stderr) + self.init_func = init_func + self.init_file = init_file + self.allow_unsafe = allow_unsafe + self.work_dir = work_dir + self.add_env = add_env + + if hold_wrapper: + OTrace.hold_wrapper = hold_wrapper + + if eventloop_callback: + OTrace.eventloop_callback = eventloop_callback + + OTrace.base_context[GLOBALS_DIR] = globals_dict + OTrace.base_context[LOCALS_DIR] = locals_dict + OTrace.base_context[PICKLED_DIR] = {} + + self.break_queue = Queue.Queue() + self.break_events = OrderedDict() + + self.aliases = { + "cde": ["cd", ENTITY_CHAR], + "cdt": ["cd", TRACE_INFO], + "dn": ["cd", DOWN_STACK], + "dv": ["cd", DOWN_STACK, ";", "view", "-i"], + "doc": ["view", "-d", "-i"], + "ls": ["ls", "-C"], + "show": ["view", "-i"], + "up": ["cd", UP_STACK], + "uv": ["cd", UP_STACK, ";", "view", "-i"], + } + + # Currrent full path contains a list of tuples (path_component_name, locals) + # Start with /osh/globals as current working directory + self.cur_fullpath = [(BASE_DIR, OTrace.base_context), + (GLOBALS_DIR, globals_dict)] + + self.dir_stack = [] + self._completion_list = [] + + self.update_terminal_size() + if readline: + readline.set_completer(self.completer) + readline.parse_and_bind('tab: complete') + + def loop(self): + """Start run loop for OShell.""" + super(OShell, self).loop() + + def run(self): + if self.init_func: + self.init_func(self) + + db_interface = self.lazy_dirs.get(DATABASE_DIR) + if db_interface: + OTrace.set_database_root(db_interface.get_root_tree()) + db_interface.set_access_hook(OTrace.access_hook) + + if self.web_interface: + OTrace.set_web_root( self.web_interface.get_root_tree() ) + self.web_interface.set_web_hook(OTrace.web_hook) + + if self.init_file: + filename = expandpath(self.init_file) + if os.path.exists(filename): + self.stuff_lines( ["source '%s'\n" % filename] ) + + super(OShell,self).run() + + def get_prompt(self): + """Return (prompt, cur_dir_path).""" + return (self.prompt1, self.make_path_str()) + + def get_main_module(self): + return sys.modules[ self.globals_dict["__name__"] ] + + def in_base_dir(self): + return bool(self.cur_fullpath and self.cur_fullpath[0][INAME] == BASE_DIR) + + def get_base_subdir(self): + return self.cur_fullpath[BASE_OFFSET][INAME] if self.in_base_dir() and len(self.cur_fullpath) > BASE_OFFSET else "" + + def get_leaf_dir(self): + return "" if not self.cur_fullpath else self.cur_fullpath[-1][INAME] + + def get_leaf_dict(self): + return None if not self.cur_fullpath else self.cur_fullpath[-1][ISUBDIR] + + def get_cur_value(self): + """ Return value corresponding to current directory + """ + if len(self.cur_fullpath) > BASE2_OFFSET-1: + return self.get_subdir(self.cur_fullpath[-2][ISUBDIR], [self.cur_fullpath[-1][INAME]], value=True) + elif len(self.cur_fullpath) == BASE2_OFFSET-1 and self.get_base_subdir() == GLOBALS_DIR: + return self.get_main_module() + else: + return None + + def get_parent_value(self): + """ Return value corresponding to parent of current directory + """ + if len(self.cur_fullpath) > BASE2_OFFSET: + return self.get_subdir(self.cur_fullpath[-3][ISUBDIR], [self.cur_fullpath[-2][INAME]], value=True) + elif len(self.cur_fullpath) == BASE2_OFFSET and self.get_base_subdir() == GLOBALS_DIR: + return self.get_main_module() + else: + return None + + def get_context_path(self): + if len(self.cur_fullpath) >= TRACE_OFFSET and self.get_base_subdir() in (RECENT_DIR, SAVED_DIR): + return self.cur_fullpath[:TRACE_OFFSET] + elif len(self.cur_fullpath) >= BASE2_OFFSET and self.get_base_subdir() == ALL_DIR: + return self.cur_fullpath[:BASE2_OFFSET] + return [] + + def get_db_root_path(self): + base_subdir = self.get_base_subdir() + if base_subdir in self.lazy_dirs and len(self.cur_fullpath) >= self.lazy_dirs[base_subdir].root_depth+BASE1_OFFSET: + return self.cur_fullpath[:self.lazy_dirs[base_subdir].root_depth+BASE1_OFFSET] + return [] + + def get_web_path(self): + if self.web_interface and len(self.cur_fullpath) > BASE1_OFFSET and self.get_base_subdir() == WEB_DIR: + return [x[0] for x in self.cur_fullpath[BASE1_OFFSET:]] + return [] + + def make_path_str(self, path=None, relative=False): + """Creates path string from single or tuple components""" + if path is None: + path = self.cur_fullpath + if not path: + return PATH_SEP + if isinstance(path[0], (list,tuple)): + path_str = PATH_SEP.join(str(x[INAME]) for x in path) + else: + path_str = PATH_SEP.join(str(x) for x in path) + return path_str if relative else PATH_SEP+path_str + + def read_file(self, filename): + """Reads and executes commands from file. + Returns null string on success, or error + """ + try: + with open(os_path(filename), "r") as f: + self.stuff_lines(f.readlines()) + return "" + except Exception: + return "Error in reading from '%s'" % filename + + def update_terminal_size(self): + tty_size = getTerminalSize() + if not tty_size: + tty_size = (24, 80) + self.tty_width = tty_size[1] + + def completer(self, text, state, line=None, all=False): + """Handle TAB completion: text is the partial "filename"; return completion list. + """ + if not readline: + return [] + if state > 0: + if state < len(self._completion_list): + return self._completion_list[state] + else: + return None + + if line is None: + line = readline.get_line_buffer() + + if line.startswith(EXEC_PREFIX): + line = "exec " + line[len(EXEC_PREFIX):] + + comps = shlex.split(line) + + if comps: + if comps[0] in self.commands or comps[0] in self.aliases: + cmd = comps[0] + else: + # Default command is "pr" + cmd = "pr" + line = cmd + " " + line + comps = [cmd] + comps + else: + cmd = "" + + prefix = expanduser(comps[-1]) if comps else "" + preline = line[:len(line)-len(text)].strip() + + if cmd in self.aliases: + actual_cmd = self.aliases[cmd][0] + else: + actual_cmd = cmd + + if not preline: + # Complete command name + cmd_keys = self.commands.keys() + cmd_keys.sort() + self._completion_list = [s for s in cmd_keys if s and s.startswith(text)] + + elif cmd == "set" and len(comps) == 2: + # Complete parameter name + tem_list = [key for key in Set_params if key.startswith(prefix)] + tem_list.sort() + self._completion_list = [x[len(prefix)-len(text):] for x in tem_list] + + elif PATH_SEP not in prefix and prefix.find("..") == -1 and self.get_base_subdir() in (GLOBALS_DIR, LOCALS_DIR): + # Complete object name for relative path in globals/locals + path_list = (prefix+"*").split(".") + tem_list = self.path_matches(self.cur_fullpath, path_list, delimiter=".", + completion=True) + self._completion_list = [x[len(prefix)-len(text):] for x in tem_list] + + else: + # Complete "directory" name + path_list = (prefix+"*").split(PATH_SEP) + if is_absolute_path(prefix): + offset = 1 + path_list = path_list[1:] + cur_fullpath = [] + else: + offset = 0 + cur_fullpath = self.cur_fullpath + tem_list = self.path_matches(cur_fullpath, path_list, completion=True) + self._completion_list = [x[1:] if is_absolute_path(x) else x[len(prefix)-len(text)-offset:] for x in tem_list] + self._completion_list = [x.replace(" ", "\\ ") for x in self._completion_list] + + ##print "ABC completer: line='%s', text='%s', preline='%s', state=%d, list: %s" % (line, text, preline, state, self._completion_list) + if all: + return self._completion_list + elif state < len(self._completion_list): + return self._completion_list[state] + else: + return None + + def full_path_comps(self, new_dir): + """ Given new path (absolute or relative), returns list of full path components + """ + if not new_dir: + return new_dir + new_path = new_dir.split(PATH_SEP) # Does not handle quoted strings and escaped characters + if not new_path[-1]: + # Ignore trailing slash + new_path = new_path[-1:] + if not new_path[0]: + # Absolute path + new_path = new_path[1:] + else: + # Not absolute path + cur_path = [comp[INAME] for comp in self.cur_fullpath] + while cur_path and new_path and new_path[0] in (".", ".."): + if new_path[0] == "..": + cur_path = cur_path[:-1] + new_path = new_path[1:] + new_path = cur_path + new_path + return new_path + + def get_rel_dir(self, new_dir): + """ Given new absolute path, returns relative path + """ + cur_path = [comp[INAME] for comp in self.cur_fullpath] + if not new_dir: + return PATH_SEP + PATH_SEP.join(cur_path) + + if not is_absolute_path(new_dir): + # Relative path + return new_dir + + full_path = self.full_path_comps(new_dir) + nmatch = 0 + while nmatch < min(len(cur_path), len(full_path)): + if cur_path[nmatch] != full_path[nmatch]: + break + nmatch += 1 + + new_path = [".."] * max(0, len(cur_path)-nmatch) + if nmatch < len(full_path): + new_path += full_path[nmatch:] + + return PATH_SEP.join(new_path) + + def entity_path_comps(self, full_path): + # Split full path at ENTITY_CHAR + indx = full_path.index(ENTITY_CHAR) + key_comps = full_path[:indx] + sub_comps = full_path[indx+1:] + base_subdir = full_path[BASE_OFFSET] + if base_subdir in self.lazy_dirs: + entity_key = self.lazy_dirs[base_subdir].key_from_path(key_comps[BASE1_OFFSET:]) + else: + entity_key = None + return key_comps, sub_comps, entity_key + + def get_default_dir(self): + if self.in_base_dir(): + # Default otrace directory + context_path = self.get_context_path() + db_root_path = self.get_db_root_path() + if context_path: + # Change to top directory of current context + default_dir = context_path + elif db_root_path: + # Change to current leaf directory in db root tree + default_dir = db_root_path + else: + # Globals directory + default_dir = [(BASE_DIR, OTrace.base_context), + (GLOBALS_DIR, OTrace.base_context[GLOBALS_DIR])] + else: + # Default OS directory (~) + default_dir = [] + dir_path = os.path.expanduser("~") + pathnames = dir_path.split(PATH_SEP)[1:] + for j, name in enumerate(pathnames): + # Determine context for each sub path + sub_path = pathnames[:j+1] + subdir = self.get_subdir("root", path_list=sub_path) + default_dir.append((name, subdir)) + + return default_dir[:] + + def change_workdir(self, workdir=None): + """Changes working directory, returning null string on success, + or error message string + """ + if workdir: + workdir = workdir.strip() + + tail_path = "" + if not workdir: + # Default dir + self.cur_fullpath = self.get_default_dir() + + elif workdir == PATH_SEP: + # Root dir + self.cur_fullpath = [] + + elif workdir == PATH_SEP+BASE_DIR: + # Root dir + self.cur_fullpath = [(BASE_DIR, OTrace.base_context)] + + else: + path_list = workdir.split(PATH_SEP) # Does not handle quoted strings and escaped characters + + if not path_list[0]: + # Absolute path + if len(path_list) > 1 and path_list[1] == BASE_DIR: + path_list.pop(0) + self.cur_fullpath = [(BASE_DIR, OTrace.base_context)] + else: + self.cur_fullpath = [] + elif path_list[0] == BASE_DIR and not self.cur_fullpath: + # Base directory + self.cur_fullpath = [(BASE_DIR, OTrace.base_context)] + elif path_list[0] == ".": + # Do nothing + pass + elif path_list[0] == "..": + # Change to parent dir + if self.cur_fullpath: + self.cur_fullpath.pop() + else: + # Relative path + matches = self.path_matches(self.cur_fullpath, path_list=path_list[0:1]) + if not matches: + return "Failed to change to directory '%s'" % path_list[0] + if len(matches) > 1: + return "Ambiguous directory change '%s'" % path_list[0] + + subdir = self.get_subdir(self.locals_dict, [ matches[0] ]) + if subdir is None: + return "Stale directory '%s'" % matches[0] + + # Change to one level inner directory + self.cur_fullpath.append((matches[0], subdir)) + + # Remaining portion of path + tail_path = PATH_SEP.join(path_list[1:]) + + if self.in_base_dir(): + if len(self.cur_fullpath) > BASE_OFFSET: + self.locals_dict = self.get_leaf_dict() + else: + self.locals_dict = OTrace.base_context + else: + if self.cur_fullpath: + self.locals_dict = self.get_leaf_dict() + else: + self.locals_dict = self.get_subdir("root") + + if tail_path: + return self.change_workdir(tail_path) + else: + return "" + + def update_prompt(self): + if not self.cur_fullpath: + self.prompt1 = "/> " + + elif len(self.cur_fullpath) == TRACE_OFFSET and self.get_base_subdir() in (RECENT_DIR, SAVED_DIR): + # Special case; double path component in prompt + class_prefix = self.cur_fullpath[BASE2_OFFSET][INAME] + time_suffix = self.cur_fullpath[BASE2_OFFSET+2][INAME][7:] + if "." in class_prefix: + class_prefix = class_prefix[:class_prefix.find(".")] + self.prompt1 = "%s..%s> " % (class_prefix, time_suffix) + else: + # Final path component as prompt + prompt = self.get_leaf_dir() + if prompt in (UP_STACK, DOWN_STACK) and self.has_trc("funcname"): + prompt = self.get_trc("funcname") + if self.get_web_path() and len(self.cur_fullpath) > BASE1_OFFSET: + self.prompt1 = "web..%s> " % (prompt,) + elif self.in_base_dir() and len(self.cur_fullpath) > BASE1_OFFSET: + self.prompt1 = "%s..%s> " % (BASE_DIR, prompt,) + else: + self.prompt1 = "%s> " % (prompt,) + + if self.break_events: + self.prompt1 = "BRK:" + self.prompt1 + + + def get_subdir(self, cur_dir="base", path_list=[], value=False): + """Returns dictionary corresponding to subdirectory, or None, if unable to do so + If value==True, return value corresponding to path, instead. + """ + if value and not path_list: + # Return value corresponding to path + return cur_dir + + # Wrap object to appear as a dict + if cur_dir is None: + cur_dir = {} + elif cur_dir == "base": + cur_dir = OTrace.base_context + elif cur_dir == "root" or isinstance(cur_dir, OSDirectory): + dir_path = PATH_SEP if cur_dir == "root" else cur_dir.path + cur_dir = dict( (subdir_name, OSDirectory(dir_path+PATH_SEP+subdir_name)) for subdir_name in os.listdir(dir_path) ) + if dir_path == PATH_SEP: + cur_dir[BASE_DIR] = OTrace.base_context + elif isinstance(cur_dir, (list, tuple)): + cur_dir = ListDict(cur_dir) + elif isinstance(cur_dir, dict): + # Temporary workaround for dict keys containing slashes (PATH_SEP) or spaces: replace slash with % and space with _ ("COMMENTED OUT") + if 0 and any( (isinstance(key, basestring) and (PATH_SEP in key or " " in key)) for key in cur_dir): + cur_dir = dict((key.replace(PATH_SEP, "%").replace(" ", "_") if isinstance(key, basestring) else key, value) for key, value in cur_dir.items()) + + elif not isinstance(cur_dir, (MappingDict, weakref.WeakValueDictionary)): + cur_dir = ObjectDict(cur_dir) + + if not path_list: + # Return "directory" corresponding to path + return cur_dir + + sub_dir = None + if path_list[0] in cur_dir: + try: + sub_dir = cur_dir[path_list[0]] + except Exception: + return None + elif path_list[0].isdigit() and int(path_list[0]) in cur_dir: + try: + sub_dir = cur_dir[int(path_list[0])] + except Exception: + return None + + if sub_dir is None: + return None + return self.get_subdir(sub_dir, path_list[1:], value=value) + + def path_matches(self, cur_fullpath, path_list, delimiter=PATH_SEP, sort=True, absolute=False, + completion=False): + """Returns list of matching paths (including simple wildcards) + for specified context + """ + if cur_fullpath: + locals_dict = cur_fullpath[-1][ISUBDIR] + else: + locals_dict = self.get_subdir("root") + + if not path_list: + matches = locals_dict.keys() + matches.sort() + if absolute: + matches = [delimiter + x for x in matches] + return matches + + if path_list[0] == ".": + return self.path_matches(cur_fullpath, path_list[1:], delimiter=delimiter, sort=sort, + absolute=absolute, completion=completion) + + if path_list[0] == GLOBALS_PREFIX: + matches = self.path_matches([(BASE_DIR, OTrace.base_context), + (GLOBALS_DIR, OTrace.base_context[GLOBALS_DIR])], + path_list[1:], delimiter=delimiter, sort=sort, + absolute=absolute, completion=completion) + return [GLOBALS_PREFIX+delimiter+x for x in matches] + + if path_list[0] == "..": + if cur_fullpath: + new_path_list = [x[INAME] for x in cur_fullpath[:-1]] + path_list[1:] + parent_prefix = delimiter+delimiter.join(x[INAME] for x in cur_fullpath[:-1]) if len(cur_fullpath) > 1 else "" + else: + new_path_list = path_list[1:] + parent_prefix = "" + matches = self.path_matches([], new_path_list, delimiter=delimiter, sort=sort, + absolute=True, completion=completion) + return [".."+x[len(parent_prefix):] for x in matches] if completion else matches + + matches = [] + for key in locals_dict.keys(): + first_dir = path_list[0] + matched = (first_dir == key) or (first_dir.isdigit() and int(first_dir) == key) + if not matched and ("?" in first_dir or "*" in first_dir or "[" in first_dir): + try: + # Try regexp match + pattern = first_dir.replace("+", "\\+").replace("?", ".?").replace("*", ".*") # Convert shell wildcard pattern to python regexp + matchobj = re.match(pattern, key) + if matchobj and matchobj.group() == key: + matched = True + except Exception: + pass + + if matched: + if len(path_list) == 1: + matches.append(key) + else: + subdir = self.get_subdir(locals_dict, [key]) + if not subdir and not completion: + matches.append(delimiter.join([key]+path_list[1:])) + elif subdir: + new_path = cur_fullpath[:] + new_path.append((key, subdir)) + inner_matches = self.path_matches(new_path, path_list[1:], delimiter=delimiter, + sort=sort, completion=completion) + + for inner_match in inner_matches: + if inner_match.startswith(delimiter): + matches.append(inner_match) + else: + matches.append(key+delimiter+inner_match) + matches.sort() + if absolute: + matches = [x if x.startswith(delimiter) else delimiter+x for x in matches] + return matches + + def shutdown(self): + # Resume break point events on shutdown + for event in self.break_events.values(): + event.set() + super(OShell, self).shutdown() + + def line_wrap(self, str_list, html_attrs=None, pre_count=0): + """Format a list of strings so that they are laid out in tabular form. + If pre_count, the first pre_count entries always appear in a separate line + """ + if not str_list: + return "" + + str_list = [str(s) for s in str_list] + nstr = len(str_list) + + max_width = min(self.tty_width//2 - 1, max([len(s) for s in str_list])) + ncols = (self.tty_width // (max_width+1)) + text_fmt = "%-"+str(max_width)+"s" + + formatted = [] + for j, value in enumerate(str_list): + if html_attrs: + uri, mime, command = html_attrs[value] + s = str(value) + fmt_value = self.html_fmt % (uri, mime, command, s) + if len(s) < max_width: + fmt_value += " "*(max_width-len(s)) + else: + fmt_value = text_fmt % value + + jdel = j - pre_count + if (pre_count and j == pre_count) or (jdel > 0 and not (jdel % ncols)): + formatted.append("\n") + elif j: + formatted.append(" ") + formatted.append(fmt_value) + + return "".join(formatted) + + def execute(self, line, here_doc=None): + """ Execute single oshell command and return (out_str, err_str) + here_doc contains optional input string. + """ + try: + return self.parse(line, batch=True, here_doc=here_doc) + except Exception: + return ("", format_traceback()) + + def parse(self, line, batch=False, here_doc=None): + """Parse command line and execute command, returning (out_str, err_str) like self.push + here_doc contains optional input. + """ + cmd_opts = type('Bunch', (object,), + dict(batch=batch, + here_doc=here_doc, + redirect_in=None, + redirect_out=None, + edit_force=False, + view_inline=False, + view_docstr=False) ) + + self.update_terminal_size() + + while not self.break_queue.empty(): + trace_id, event = self.break_queue.get() + if trace_id in self.break_events: + self.break_events[trace_id].set() + self.break_events[trace_id] = event + + if not line.strip(): + return "", "" + + if line.strip() == "lock": + if not Set_params["password"]: + while True: + password = read_password("Set password: ").strip() + if not password: + return "", "Lock cancelled" + if password == read_password("Confirm password: ").strip(): + Set_params["password"] = encrypt_password(password) + break + + while True: + try: + password = read_password("Enter password to unlock: ").strip() + if verify_password(password, Set_params["password"]): + break + except: + self.shutdown() + return ("", "Shutting down...") + return "", "" + + if line.lstrip().startswith("repeat "): + line = line.lstrip()[len("repeat "):] + if line: + self.set_repeat(line) + if not self.repeat_alt_screen: + self.repeat_alt_screen = 1 + + # Variables: + # line: full command line + # rem_line: command line minus the command + # cmd: command + # comps: command arguments + + if line.startswith(EXEC_PREFIX): + cmd = "exec" + rem_line = line[len(EXEC_PREFIX):].lstrip() + comps = shlex.split(rem_line.strip()) + else: + comps = shlex.split(line.strip()) + + if comps and (comps[0] in self.commands or comps[0] in self.aliases): + cmd = comps.pop(0) + rem_line = line.lstrip()[len(cmd):].lstrip() + else: + # Default command is "pr" + cmd = "pr" + rem_line = line + + if cmd in self.aliases: + # Substitute for alias in command line + new_comps = [] + clear_comps = False + for arg in self.aliases[cmd]: + if "\\**" in arg: + # Pythonize shell arguments by quoting them (comma-separated) + arg = arg.replace("\\**", pythonize(comps)) + clear_comps = True + elif "\\*" in arg: + # Substitute all shell arguments (space-separated) + arg = arg.replace("\\*", " ".join(comps)) + clear_comps = True + for j, comp in enumerate(comps): + s = "\\"+str(j+1) + if s in arg: + # Substitute individual arguments + arg = arg.replace(s, comp) + clear_comps = True + new_comps.append(arg) + if not new_comps: + return "", "Error in alias: "+cmd + if clear_comps: + comps = [] + rem_line = "" + cmd = new_comps.pop(0) + if new_comps: + rem_line = " ".join(new_comps) + " " + rem_line + line = cmd + " " + rem_line + comps = new_comps + comps + + # Sequences of "if" blocks, each of which ends with "return (out_str, err_str)" + out_str = "" + err_str = "" + + # Handle shell, javascript, and selected oshell commands + if cmd == "quit": + err_str = "Shutting down..." + self.shutdown() + return (out_str, err_str) + + elif cmd == "help": + cmd_keys = self.commands.keys() + cmd_keys.sort() + if not comps: + out_str = 'Commands:\n%s\n\nIf you omit the command, "pr" is assumed.\nUse TAB key for command completion.\nType "help " or "help *" for more info\n\nSee %s for documentation\n\n' % (self.line_wrap(cmd_keys), DOC_URL) + elif comps[0] in self.commands: + out_str = self.commands[comps[0]] + if comps[0] == "set": + for name, value in Help_params.iteritems(): + out_str += "\n%-17s %s" % (name+":", value) + else: + out_str = "\n".join([self.commands[k].partition("\n")[0] for k in cmd_keys]) + ("\n\nUse otrace.traceassert(,label=..,action=..) to trace assertions\n\nSee %s for documentation\n\n" % DOC_URL) + return (out_str, err_str) + + elif cmd == "alias": + if not comps: + aliases = self.aliases.items() + aliases.sort() + out_str = "\n".join([name+" "+" ".join(value) for name, value in aliases]) + else: + if len(comps) < 2: + if comps[0] in self.aliases: + out_str = " ".join(self.aliases[comps[0]]) + else: + err_str = "No such alias: %s" % comps[0] + elif comps[1]: + self.aliases[comps[0]] = comps[1:] + elif comps[0] in self.aliases: + del self.aliases[comps[0]] + return (out_str, err_str) + + elif cmd in ("cd", "cdls", "pushd", "popd", "swapd"): + opts = [] + while comps and comps[0].startswith("-"): + opts.append(comps.pop(0)) + + follow_up_cmd = "" + if cmd == "cdls": + follow_up_cmd = "ls" + line = follow_up_cmd + " " + rem_line + + if cmd == "popd": + if not self.dir_stack: + return ("", "Nothing to pop") + + if opts == ["-a"]: + new_dir = self.dir_stack[0] + self.dir_stack = [] + else: + new_dir = self.dir_stack.pop() + elif cmd == "swapd": + if not self.dir_stack: + return ("", "Nothing to swap") + new_dir = self.dir_stack[-1] + self.dir_stack[-1] = self.make_path_str() + elif cmd in ("cd", "cdls", "pushd"): + if cmd == "pushd": + self.dir_stack.append(self.make_path_str()) + if not comps: + # Cd to default "home" directory + new_dir = None + else: + new_dir = comps.pop(0) + + if new_dir is not None: + if ";" in new_dir: + # Expect follow up command + new_dir, sep, tail = new_dir.partition(";") + if tail: + follow_up_cmd = tail + elif comps: + follow_up_cmd = comps.pop(0) + elif comps and comps[0].startswith(";"): + comp = comps.pop(0) + if comp[1:]: + follow_up_cmd = comp[1:] + elif comps: + follow_up_cmd = comps.pop(0) + + new_dir = expanduser(new_dir) + if new_dir.startswith(TRACE_ID_PREFIX): + trace_id = new_dir[len(TRACE_ID_PREFIX):] + if not trace_id: + # Cd to default trace_id entry + new_dir = None + else: + # Cd to recent trace_id entry + new_dir = PATH_SEP + PATH_SEP.join([BASE_DIR, RECENT_DIR] + ContextDict.split_trace_id(trace_id)) + + if new_dir and PATH_SEP not in new_dir and new_dir.find("..") == -1 and self.get_base_subdir() in (GLOBALS_DIR, LOCALS_DIR): + # Replace . with / + new_dir = new_dir.replace(".", PATH_SEP) + out_str, err_str = self.cmd_cd(new_dir) + # Set prompt after changing directory + self.update_prompt() + + if follow_up_cmd: + # Follow successful cd with command + cmd = follow_up_cmd + comps = opts + comps + rem_line = " ".join(comps) + else: + # Just cd; completed + return (out_str, err_str) + + if cmd in ("edit", "unpatch", "view"): + while comps: + # View options + if comps[0] == "-d": + cmd_opts.view_docstr = True + elif comps[0] == "-f": + cmd_opts.edit_force = True + elif comps[0] == "-i": + cmd_opts.view_inline = True + else: + break + comps.pop(0) + + if comps: + comp0 = comps.pop(0) + while comps: + # Combine file redirection into single token + if comp0.endswith("<") or comp0.endswith(">") or comps[0].startswith("<") or comps[0].startswith(">"): + comp0 += comps.pop(0) + else: + break + + while True: + # Strip out file redirection from token + if comp0.find("<") > comp0.find(">"): + comp0, sep, cmd_opts.redirect_in = comp0.rpartition("<") + continue + if comp0.find(">") > comp0.find("<"): + comp0, sep, cmd_opts.redirect_out = comp0.rpartition(">") + continue + break + + # Expand filename, substituting for ".." and "~" + comp0 = expanduser(comp0) + if not is_absolute_path(comp0): + # Relative path + path_list = comp0.split(PATH_SEP) + matches = self.path_matches(self.cur_fullpath, path_list=path_list) + if not matches: + # No match; use specified name + if cmd != "edit" and not self.in_base_dir(): + return ("", "File '%s' not found" % comp0) + elif len(matches) == 1: + comp0 = matches[0] + else: + return ("", "Cannot edit/view multiples files: %s" % comp0) + + if cmd == "edit" and cmd_opts.redirect_in == "" and cmd_opts.here_doc is None: + return ("", "Expecting here_doc input for editing %s" % comp0) + + comps = [comp0] + comps + + if cmd != "unpatch": + try: + return self.edit_view(cmd, comps, inline=cmd_opts.view_inline, here_doc=cmd_opts.here_doc) + except AltHandler: + pass # Handle edit/unpatch/view command for /osh/* + + elif not self.in_base_dir(): + # Non-otrace command; handle using shell + return self.cmd_shell(line) + + elif cmd == "pwd": + cwd = self.make_path_str() + if comps and comps[0] == "-a": + return ("\n".join([cwd]+[x for x in reversed(self.dir_stack)]), err_str) + else: + return (cwd, err_str) + + elif self.get_web_path() and len(self.get_web_path()) >= self.web_interface.root_depth: + # Non-otrace command; handle using web interface + if Set_params["safe_mode"]: + return ("", "Javascript console disabled in safe mode; set safe_mode False") + try: + self.web_interface.send_command(self.get_web_path(), line) + return ("_NoPrompt_", None) + except Exception, excp: + return ("", "Error in command execution '%s': %s" % (line, excp)) + + # Handle remaining oshell commands + if cmd == "trace": + # Initiate tracing + return self.cmd_trace(cmd, comps, line, rem_line) + + elif cmd == "untrace": + return self.cmd_untrace(cmd, comps, line, rem_line) + + elif cmd in ("edit", "unpatch"): + return self.cmd_edit_unpatch(cmd, comps, line, rem_line, cmd_opts) + + elif cmd == "source": + if not comps: + err_str = "No filename to read!" + else: + filename = expanduser(comps[0]) + if not is_absolute_path(filename): + return (out_str, "Must specify absolute pathname for input file %s" % filename) + if not os.path.exists(filename): + return (out_str, "Input file %s not found" % filename) + err_str = self.read_file(filename) + return (out_str, err_str) + + elif cmd in ("resume", "save", "del"): + return self.cmd_del_resume_save(cmd, comps, line, rem_line) + + elif cmd in ("ls", "rm"): + # List "directory" or 'remove' entries + return self.cmd_lsrm(cmd, comps, line, rem_line) + + elif cmd == "set": + # Set (or display) parameters + return self.cmd_set(cmd, comps, line, rem_line) + + elif cmd == "view": + return self.cmd_view(cmd, comps, line, rem_line, cmd_opts) + + elif cmd in ("tag", "untag"): + # Tag/untag object for tracing + return self.cmd_tag_untag(cmd, comps, line, rem_line) + + elif cmd == "unpickle": + # Read and unpickle trace contexts + return self.cmd_unpickle(cmd, comps, line, rem_line) + + elif cmd == "pr": + # Evaluate expression and print it + return self.cmd_pr(cmd, comps, line, rem_line, cmd_opts) + + elif cmd == "exec": + # Execute code + return self.cmd_exec(cmd, comps, line, rem_line, cmd_opts) + + else: + return (out_str, "Unrecognized command '%s'" % cmd) + + return out_str, err_str + + def cmd_shell(self, line): + """Execute shell command, returning (out_str, err_str)""" + out_str, err_str = "", "" + try: + env = os.environ.copy() + env.update(self.add_env) + osh_bin = Set_params["osh_bin"] + if osh_bin: + # Prepend osh_bin to PATH (prepending with cwd, if needed) + osh_bin = expanduser(osh_bin) + if not is_absolute_path(osh_bin): + osh_bin = os.path.join(os.getcwd(), os_path(osh_bin)) + prev_path = env.get("PATH") + if prev_path: + env["PATH"] = osh_bin+":"+prev_path + else: + env["PATH"] = osh_bin + + exec_context = {"queue": Queue.Queue()} + def execute_in_thread(): + try: + exec_context["proc"] = subprocess.Popen(["/bin/bash", "-l", "-c", line], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=self.make_path_str(), env=env) + exec_context["queue"].put(exec_context["proc"].communicate()) + except Exception, excp: + exec_context["queue"].put(("", "ERROR in executing '%s': %s" % (line, excp))) + + if Term_attr: + save_attr = termios.tcgetattr(sys.stdin.fileno()) + thrd = threading.Thread(target=execute_in_thread) + thrd.start() + try: + return exec_context["queue"].get(True, EXEC_TIMEOUT) + except Queue.Empty: + exec_context["proc"].kill() + # Restore terminal attributes + if Term_attr: + termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, save_attr) + return ("", "Timed out command execution '%s'" % (line,)) + + except Exception, excp: + return ("", "Error in command execution '%s': %s" % (line, excp)) + + def cmd_view(self, cmd, comps, line, rem_line, cmd_opts): + """View, returning (out_str, err_str)""" + out_str, err_str = "", "" + obj_path = self.make_path_str() + obj_value = None + if not comps or comps[0] == ".": + if not self.in_base_dir(): + return ("", "No object to display") + + if self.has_trc("frame"): + # Display source context for exceptions, trace asserts etc. + self.locals_dict["_stdout"] = self._stdout + out_str, err_str = self.push('print >>_stdout, "%%s:%%s %%s\\n" %% %s["frame"][:3], "".join(%s["frame"][3])' % (TRACE_INFO, TRACE_INFO)) + del self.locals_dict["_stdout"] + return (out_str, err_str) + + if self.has_trc("func"): + obj_value = self.get_trc("func") + else: + obj_value = self.get_cur_value() + + else: + comp0 = comps.pop(0) + if is_absolute_path(comp0): + # Absolute path + if comp0 == PATH_SEP+BASE_DIR: + return ("", "Cannot edit/view %s" % comp0) + elif not comp0.startswith(PATH_SEP+BASE_DIR+PATH_SEP): + raise Exception("Internal error; cannot view %s" % comp0) + + obj_path = comp0 + tem_path = comp0[len(PATH_SEP+BASE_DIR+PATH_SEP):].replace(PATH_SEP, ".").split(".") + obj_value = self.get_subdir("base", tem_path, value=True) + else: + # Relative path + if not self.in_base_dir(): + raise Exception("Internal error; cannot view %s" % comp0) + obj_path = obj_path + PATH_SEP + comp0 + tem_path = comp0.replace(PATH_SEP, ".").split(".") + obj_value = self.get_subdir(self.locals_dict, tem_path, value=True) + + if not obj_value: + return (out_str, "No object to display source for") + + if not (inspect.ismodule(obj_value) or inspect.isclass(obj_value) or + inspect.ismethod(obj_value) or inspect.isfunction(obj_value)): + # Display class for non-module/class/method/function objects + if hasattr(obj_value, "__class__"): + obj_value = getattr(obj_value, "__class__") + try: + # Display object source + if cmd_opts.view_docstr: + doc = inspect.getdoc(obj_value) + lines = doc.split("\n") if doc else [] + else: + lines, start_line = OTrace.getsourcelines(obj_value) + content = "".join(de_indent(lines)) + except Exception, excp: + return (out_str, "Unable to display source for %s:\n %s" % (obj_path, excp)) + + if cmd_opts.view_inline: + return (content, err_str) + + if cmd_opts.redirect_out: + filename = expanduser(cmd_opts.redirect_out) + if not is_absolute_path(filename): + return (out_str, "Must specify absolute pathname for output file %s" % filename) + try: + with open(os_path(filename), "w") as f: + f.write(content) + return ("", "") + except Exception, excp: + return (out_str, "Error in saving view output: "+str(excp)) + + OTrace.callback_handler.editback(content, filepath=obj_path, filetype="python", editor=Set_params["editor"], modify=False) + return (out_str, err_str) + + def cmd_edit_unpatch(self, cmd, comps, line, rem_line, cmd_opts): + """Edit/unpatch, returning (out_str, err_str)""" + out_str, err_str = "", "" + if Set_params["safe_mode"]: + return (out_str, "Patching/unpatching not permitted in safe mode; set safe_mode False") + patch_name = None + parent_obj = None + if comps and comps[0].startswith(DIR_PREFIX[GLOBALS_DIR]): + # Absolute path + comp = comps.pop(0) + tem_name = comp[len(DIR_PREFIX[GLOBALS_DIR]):] + path_list = tem_name.replace(PATH_SEP, ".").split(".") + patch_name = ".".join(path_list) + old_obj = self.get_subdir(self.globals_dict, path_list, value=True) + if len(path_list) > 1: + parent_obj = self.get_subdir(self.globals_dict, path_list[:-1], value=True) + else: + parent_obj = self.get_main_module() + else: + if self.get_base_subdir() not in (GLOBALS_DIR, PATCHES_DIR): + return (out_str, "Patching only works in subdirectory of globals/patches") + if not comps: + old_obj = self.get_cur_value() + if self.get_base_subdir() == PATCHES_DIR: + patch_name = self.get_leaf_dir() + parent_obj = None + else: + patch_name = ".".join(x[INAME] for x in self.cur_fullpath[BASE1_OFFSET:]) + parent_obj = self.get_parent_value() + else: + objname = comps.pop(0) + obj_path_comps = objname.replace(PATH_SEP, ".").split(".") + patch_name = ".".join([x[INAME] for x in self.cur_fullpath[BASE1_OFFSET:]]+obj_path_comps) + if self.get_base_subdir() == PATCHES_DIR: + old_obj = self.get_subdir(self.locals_dict, [objname], value=True) + parent_obj = None + else: + old_obj = self.get_subdir(self.locals_dict, obj_path_comps, value=True) + if len(obj_path_comps) == 1: + parent_obj = self.get_cur_value() + else: + parent_obj = self.get_subdir(self.locals_dict, obj_path_comps[:-1], value=True) + + basename = patch_name.split(".")[-1] + if not old_obj and not parent_obj and not (cmd == "unpatch" and patch_name == "*"): + return (out_str, "Invalid patch class/method: " + str(patch_name)) + + if cmd == "edit": + if inspect.isclass(old_obj): + return ("", "Class patching not yet implemented; patch methods individually") + if cmd_opts.redirect_in: + # Read patch from file + filename = expanduser(cmd_opts.redirect_in) + if not is_absolute_path(filename): + return (out_str, "Must specify absolute pathname for input file %s" % filename) + try: + with open(os_path(filename), "r") as patchf: + mod_content = patchf.read() + except Exception, excp: + return (out_str, "Error in reading from '%s': %s" % (filename, excp)) + elif cmd_opts.here_doc is not None: + mod_content = cmd_opts.here_doc + else: + # Create patch using editor + try: + lines, start_line = OTrace.getsourcelines(old_obj) + except IOError: + return (out_str, "Error: source not found; specify source filename") + except Exception, excp: + return (out_str, "Error in accessing source: %s" % excp) + + if not start_line and not cmd_opts.edit_force: + return (out_str, "Error: specify '-f' option to force editing of patched file") + + content = "".join(de_indent(lines)) + mod_content, err_str = OTrace.callback_handler.editback(content, filepath=DIR_PREFIX[GLOBALS_DIR]+patch_name, filetype="python", editor=Set_params["editor"], modify=True) + if mod_content is None and err_str is None: + # Delayed modification + return ("_NoPrompt_", None) + if err_str: + return (out_str, err_str) + if mod_content == content: + # No changes + return (out_str, "No changes") + + tem_lines = mod_content.split("\n") + mod_lines = [x+"\n" for x in tem_lines[:-1]] + if tem_lines[-1]: + mod_lines.append(tem_lines[-1]) + + # Find module containing function or class + module_name = old_obj.__module__ if old_obj else parent_obj.__module__ + module_obj = sys.modules[module_name] + + patch_locals = {} + out_str, err_str = self.interpreter.evaluate(mod_content, locals_dict=patch_locals, + globals_dict=module_obj.__dict__, + print_out=False) + if err_str: + return (out_str, err_str) + + if not old_obj or ismethod_or_function(old_obj): + # Patch single function/method + if (len(patch_locals) != 1 or patch_locals.keys()[0] != basename): + return (out_str, "Error: patch file must contain only 'def %s', but found %s" % (basename, patch_locals)) + + func = patch_locals.values()[0] + out_str += "Patched " + patch_name + ":" + patched_obj = OTrace.monkey_patch(func, old_obj, parent_obj, repatch=cmd_opts.edit_force, + source=mod_lines) + if patched_obj: + OTrace.base_context[PATCHES_DIR][patch_name] = patched_obj + else: + out_str += "-FAILED" + + else: + # Patch class + OTrace.base_context[PATCHES_DIR][patch_name] = old_obj + out_str += "Patched " + patch_name + ":" + for method_name, func in patch_locals.iteritems(): + method_obj = getattr(old_obj, method_name, None) + + out_str += " " + method_name + if not OTrace.monkey_patch(func, method_obj, old_obj, repatch=cmd_opts.edit_force): + out_str += "-FAILED" + else: + # Unpatch + if patch_name == "*" and self.get_base_subdir() == PATCHES_DIR: + unpatch_items = OTrace.base_context[PATCHES_DIR].items() + else: + unpatch_items = [(patch_name, old_obj)] + + if len(unpatch_items) == 1 and cmd_opts.redirect_out: + filename = expanduser(cmd_opts.redirect_out) + if not is_absolute_path(filename): + return (out_str, "Must specify absolute pathname for output file %s" % filename) + try: + lines, start_line = OTrace.getsourcelines(unpatch_items[0][1]) + except IOError: + return (out_str, "Error: patch source not found for saving") + except Exception, excp: + return (out_str, "Error in saving patch: %s" % excp) + + try: + with open(os_path(filename), "w") as f: + f.write("".join(lines)) + except Exception, excp: + return (out_str, "Error in saving patch source: "+str(excp)) + + out_str = "Unpatching" + for patch_name, unpatch_obj in unpatch_items: + if ismethod_or_function(unpatch_obj): + # Unpatch single function/method + if OTrace.monkey_unpatch(unpatch_obj): + out_str += " " + patch_name + else: + return (out_str, "Unpatching failed for %s" % patch_name) + else: + # Unpatch class + out_str += " " + patch_name + ":" + for name, method_obj in inspect.getmembers(unpatch_obj, ismethod_or_function): + if OTrace.monkey_unpatch(method_obj): + out_str += " " + method_obj.__name__ + try: + del OTrace.base_context[PATCHES_DIR][patch_name] + except Exception: + pass + return (out_str, err_str) + + def cmd_untrace(self, cmd, comps, line, rem_line): + """Terminate tracing, returning (out_str, err_str)""" + out_str, err_str = "", "" + trace_name = "" + if comps: + trace_name = comps[0] + parent_obj = None + if trace_name in ("all", "*") or trace_name[0] == TRACE_LABEL_PREFIX or trace_name.startswith(TRACE_LOG_PREFIX): + trace_value = trace_name + else: + path_list = trace_name.split(".") + trace_value = self.get_subdir(self.locals_dict, path_list, value=True) + if len(path_list) > 1: + parent_obj = self.get_subdir(self.locals_dict, path_list[:-1], value=True) + else: + parent_obj = self.get_cur_value() + + if trace_value is None: + return (out_str, "Invalid class/method for untracing: " + str(trace_name)) + + with Trace_rlock: + # Remove tracing + if inspect.isclass(trace_value): + OTrace.trace_entity(trace_value, unwrap=True) + elif inspect.isclass(parent_obj): + OTrace.trace_method(parent_obj, trace_value, unwrap=True) + elif inspect.ismodule(parent_obj): + OTrace.trace_modulefunction(parent_obj, trace_value, unwrap=True) + elif not isinstance(trace_value, basestring): + return (out_str, "Cannot untrace %s" % trace_value) + + fullname = OTrace.remove_trace(trace_name, parent=parent_obj) + out_str = "untraced %s" % fullname + + return (out_str, err_str) + + def cmd_trace(self, cmd, comps, line, rem_line): + """Initiate tracing, returning (out_str, err_str)""" + out_str, err_str = "", "" + trace_name = "" + trace_call = False + trace_return = False + match_tag = "" + argmatch = {} + access_type = "" + fullname = "" + + trace_condition = None + break_action = None + break_count = 0 + while comps and comps[0].startswith("-"): + comp = comps.pop(0) + if comp in ("-a", "-c", "-n") and not comps: + return (out_str, "Missing argument for option %s" % comp) + if comp == "-a": + if comps[0] in TRACE_ACTIONS: + break_action = comps.pop(0) + else: + return (out_str, "Expected one of %s for option %s" % (comp, TRACE_ACTIONS)) + elif comp == "-c": + trace_condition = comps.pop(0) + elif comp == "-n": + if comps[0].isdigit() or (comps[0][1:].isdigit() and comps[0][0] in "+-"): + break_count = int(comps.pop(0)) + else: + return (out_str, "Expected integer argument for option %s" % comp) + else: + return (out_str, "Invalid option %s" % comp) + + if not comps: + keys = OTrace.trace_names.keys() + keys.sort() + out_str = "\n".join(str(OTrace.trace_names[k]) for k in keys) + else: + trace_name = comps.pop(0) + trace_value = None + tracing_key = False + parent_obj = None + if trace_name.startswith(DIR_PREFIX[GLOBALS_DIR]): + tem_name = trace_name[len(DIR_PREFIX[GLOBALS_DIR]):] + path_list = tem_name.replace(PATH_SEP, ".").split(".") + trace_value = self.get_subdir(self.globals_dict, path_list, value=True) + if len(path_list) > 1: + parent_obj = self.get_subdir(self.globals_dict, path_list[:-1], value=True) + else: + parent_obj = self.get_main_module() + + elif trace_name.startswith(DIR_PREFIX[DATABASE_DIR]): + tracing_key = True + trace_value = trace_name[len(DIR_PREFIX[DATABASE_DIR]):] + PATH_SEP + + elif trace_name == "*" or trace_name[0] == TRACE_LABEL_PREFIX or trace_name.startswith(TRACE_LOG_PREFIX): + trace_value = trace_name + + elif self.get_base_subdir() == DATABASE_DIR: + # Relative database path + tracing_key = True + full_path = self.full_path_comps(trace_name) + trace_value = PATH_SEP + PATH_SEP.join(full_path[1:]) + + else: + # Other relative path + path_list = trace_name.replace(PATH_SEP, ".").split(".") + trace_value = self.get_subdir(self.locals_dict, path_list, value=True) + parent_obj = None + if inspect.ismethod(trace_value): + method_self = getattr(trace_value, "__self__", None) + if method_self: + if inspect.isclass(method_self): + parent_obj = method_self + else: + parent_obj = getattr(method_self, "__class__", None) + + if not parent_obj: + if self.get_base_subdir() != GLOBALS_DIR: + return (out_str, "Must be in subdirectory of globals to trace") + if len(path_list) > 1: + parent_obj = self.get_subdir(self.locals_dict, path_list[:-1], value=True) + else: + parent_obj = self.get_cur_value() + + if comps: + return (out_str, "Multiple objects in trace command not yet implemented: %s" % comps) + + if trace_value is None: + return (out_str, "Invalid class/method/key for tracing: " + str(trace_name)) + + if trace_condition: + try: + argmatch = match_parse(trace_condition) + except Exception: + return (out_str, "Invalid trace arg/attr match: " + trace_condition) + + if argmatch: + pass + elif tracing_key: + if trace_condition in ("get", "put", "delete", "modify", "all"): + access_type = trace_condition + else: + return (out_str, "Invalid trace access type: " + trace_condition) + elif trace_condition.startswith("tagged"): + match_tag = trace_condition[6:] if len(trace_condition) > 6 else "*" + elif trace_condition == "call": + trace_call = True + elif trace_condition == "return": + trace_return = True + elif trace_condition == "all": + trace_call = True + trace_return = True + else: + return (out_str, "Invalid trace condition " + trace_condition) + + if break_action == "tag": + trace_call = False + trace_return = True + + if tracing_key: + if not access_type: + access_type = "modify" + key_path = trace_value.split(PATH_SEP) + if ENTITY_CHAR in key_path or DATABASE_DIR not in self.lazy_dirs: + return (out_str, "Invalid trace key") + else: + key = self.lazy_dirs[DATABASE_DIR].key_from_path(key_path) + if not key: + return (out_str, "Invalid trace key: " + trace_value) + else: + trace_value = str(key) + + with Trace_rlock: + # Setup tracing + if inspect.isclass(trace_value): + OTrace.trace_entity(trace_value) + elif inspect.isclass(parent_obj) and (inspect.ismethod(trace_value) or inspect.isfunction(trace_value)): + OTrace.trace_method(parent_obj, trace_value) + elif inspect.ismodule(parent_obj) and inspect.isfunction(trace_value): + OTrace.trace_modulefunction(parent_obj, trace_value) + elif not isinstance(trace_value, basestring): + return (out_str, "Cannot trace %s" % type(trace_value)) + + fullname = OTrace.add_trace(trace_value, parent=parent_obj, argmatch=argmatch, + trace_call=trace_call, trace_return=trace_return, + break_count=break_count, break_action=break_action, + match_tag=match_tag, access_type=access_type) + out_str = "Tracing " + str(OTrace.trace_names[fullname]) + + return (out_str, err_str) + + + def cmd_del_resume_save(self, cmd, comps, line, rem_line): + """Delete/resume/save, returning (out_str, err_str)""" + out_str, err_str = "", "" + trace_ids = [] + if not comps: + if self.has_trc("id"): + trace_ids = [ self.get_trc("id") ] + elif len(self.cur_fullpath) == BASE1_OFFSET and self.get_base_subdir() == ALL_DIR: + for comp in comps: + trace_ids += self.path_matches(self.cur_fullpath, [comp]) + + if not trace_ids: + err_str = "No match for trace_id" + + for trace_id in trace_ids: + if cmd == "resume": + # Resume from breakpoint or hold + with Trace_rlock: + event = self.break_events.get(trace_id) + if event: + event.set() + del self.break_events[trace_id] + out_str = "Resuming " + trace_id + OTrace.remove_break_point(trace_id) + else: + context = OTrace.base_context[ALL_DIR].get(trace_id) + if context and context.get_trc("context") == "holds": + self_arg = context.get("self") + if hasattr(self_arg, OTrace.resume_attr): + resume_from_hold(self_arg) + else: + err_str = "Unable to resume from hold for " + trace_id + OTrace.remove_break_point(trace_id) + else: + err_str = "Unable to resume " + trace_id + + self.change_workdir(PATH_SEP+BASE_DIR+PATH_SEP+GLOBALS_DIR) + self.update_prompt() + + elif cmd == "save": + # Save context(s) + with Trace_rlock: + context = OTrace.base_context[ALL_DIR].get(trace_id) + if context is not None: + OTrace.base_context[SAVED_DIR].add_context(context, trace_id) + else: + err_str = "Unable to save context '%s'" % trace_id + + elif cmd == "del": + # Delete context(s) + with Trace_rlock: + try: + del OTrace.base_context[ALL_DIR][trace_id] + except Exception: + pass + OTrace.base_context[SAVED_DIR].remove_context(trace_id) + return (out_str, err_str) + + def cmd_lsrm(self, cmd, comps, line, rem_line): + """'List' directory or 'remove' entries, returning (out_str, err_str)""" + out_str, err_str = "", "" + recursive = False + base_class = None + time_sort_key = None + ls_show = set() + show_attr = False + show_long = False + + if cmd == "rm": + if Set_params["safe_mode"]: + return (out_str, "rm: deletion not permitted in safe mode; set safe_mode False") + elif not self.lazy_dirs: + return (out_str, "rm: no database connected") + elif comps and comps[0] == "-r": + recursive = True + comps.pop(0) + elif cmd == "ls": + while comps and comps[0].startswith("-"): + comp = comps.pop(0) + if comp.startswith("-."): + # Exclude attributes of current/super class + base_name = comp[2:] + if base_name[:1].isalpha(): + # Exclude attributes of "-.classname" + path_list = base_name.split(".") + value = self.get_subdir(self.locals_dict, path_list, value=True) + if not value: + value = self.get_subdir(self.globals_dict, path_list, value=True) + if inspect.isclass(value): + base_class = value + else: + cur_value = self.get_cur_value() + if cur_value is None: + cur_class = self.locals_dict.get("__class__", None) + elif inspect.isclass(cur_value): + cur_class = cur_value + else: + cur_class = getattr(cur_value, "__class__", None) + if not base_name: + # Exclude attributes of current class + base_class = cur_class + elif cur_class: + # Exclude attributes of super class (if defined) + mro_classes = inspect.getmro(cur_class) + base_class = mro_classes[min(len(base_name),len(mro_classes)-1)] + else: + # Single character options + for opt in comp[1:]: + if opt == "a": + show_attr = True + elif opt == "c": + ls_show.add("class") + elif opt == "f": + ls_show.add("function") + elif opt == "l": + show_long = True + elif opt == "m": + ls_show.add("module") + elif opt == "t": + # Time ordering + if self.get_base_subdir() in (ALL_DIR, RECENT_DIR, SAVED_DIR): + time_sort_key = lambda x:ContextDict.split_trace_id(os.path.split(x)[1])[3] + elif opt == "v": + ls_show.add("variable") + else: + pass # ignore other options + + globals_or_locals = self.get_base_subdir() in (GLOBALS_DIR, LOCALS_DIR) + if not comps: + if cmd == "rm": + return (out_str, "rm: specify entities to delete") + # Display list of "subdirectories" + matches = self.locals_dict.keys() + else: + # Display/delete specified directories + matches = [] + for comp in comps: + if comp == PATH_SEP: + # Root directory + matches += [PATH_SEP] + elif comp == ".": + # Current directory + matches += self.make_path_str() + else: + comp_dir = expanduser(comp) + fullpath = self.cur_fullpath + absolute = is_absolute_path(comp_dir) + if absolute: + comp_dir = comp_dir[1:] + fullpath = [] + elif PATH_SEP not in comp_dir and comp_dir.find("..") == -1 and globals_or_locals: + # Relative path + comp_dir = comp_dir.replace(".", PATH_SEP) + assert comp_dir + path_list = comp_dir.split(PATH_SEP) + comp_matches = self.path_matches(fullpath, path_list, absolute=absolute) + if not comp_matches and ENTITY_CHAR in path_list: + # Accept entity property names as matches + comp_matches = [comp_dir] + matches += comp_matches + + if base_class: + matches = [attr for attr in matches if not hasattr(base_class, attr)] + if not show_attr: + matches = [attr for attr in matches if attr in SHOW_HIDDEN or not (attr.startswith("__") and attr.endswith("__"))] + + if not matches and comps: + return (out_str, "no matches '%s'" % (line.strip(),)) + + if time_sort_key: + try: + matches.sort(key=time_sort_key) + except Exception: + matches.sort() + else: + matches.sort() + + if cmd == "ls": + # List + _, cur_dir_path = self.get_prompt() + max_width = min(self.tty_width//3 - 1, max([len(key) for key in matches]) if matches else 2) + fmt = "%-"+str(max_width)+"s" + + path_attrs = OrderedDict() + pre_count = 0 + if not show_long and len(self.cur_fullpath) > 1 and Set_params["allow_xml"] and OTrace.html_wrapper: + parent_path = self.make_path_str(self.cur_fullpath[:-1]) + default_path = self.make_path_str(self.get_default_dir()) + path_attrs[".."] = ["file://"+urllib.quote(parent_path), "x-python/object", "cdls"] + path_attrs["."] = ["file://"+urllib.quote(cur_dir_path), "x-python/object", "cdls"] + path_attrs["~~"] = ["file://"+urllib.quote(default_path), "x-python/object", "cdls"] + pre_count = len(path_attrs) + + out_list = [] + for j, dir_path in enumerate(matches): + if is_absolute_path(dir_path): + path_list = dir_path[1:].split(PATH_SEP)[BASE_OFFSET:] + locals_dict = OTrace.base_context + else: + if dir_path and PATH_SEP not in dir_path and dir_path.find("..") == -1 and globals_or_locals: + # Replace . with / + dir_path = dir_path.replace(".", PATH_SEP) + path_list = dir_path.split(PATH_SEP) + locals_dict = self.locals_dict + + full_path = self.full_path_comps(dir_path) + + if ENTITY_CHAR in path_list: + key_comps, sub_comps, entity_key = self.entity_path_comps(full_path) + if entity_key and full_path[BASE_OFFSET] in self.lazy_dirs: + entity = self.lazy_dirs[full_path[BASE_OFFSET]].get_entity(entity_key) + path_list = sub_comps + if isinstance(entity, dict): + locals_dict = entity + elif entity: + locals_dict = ObjectDict(entity) + else: + locals_dict = {} + if not base_class or not hasattr(base_class, path_list[-1]): + value = self.get_subdir(locals_dict, path_list, value=True) + if inspect.isclass(value): + value_type = "class" + elif inspect.isfunction(value) or inspect.ismethod(value): + value_type = "function" + elif inspect.ismodule(value): + value_type = "module" + else: + value_type = "variable" + + if ls_show and value_type not in ls_show: + # Not displaying this type of value + continue + + if isinstance(value, (dict, weakref.WeakValueDictionary)): + value_str = "{%s}" % (", ".join(map(str, value.keys())), ) + elif isinstance(value, LineList): + value_str = "[%s]" % (value,) + elif isinstance(value, (list, tuple)): + value_str = "[%s]" % (", ".join(map(str, value)), ) + else: + value_str = repr(value) + + mime, command = get_obj_properties(value, full_path=full_path) + markup = ["file://"+urllib.quote(cur_dir_path+PATH_SEP+dir_path), "x-python/"+mime, command] + path_attrs[dir_path] = markup + + if show_long: + if Set_params["allow_xml"] and OTrace.html_wrapper: + s = str(dir_path) + fmt_value = self.html_fmt % tuple(markup + [s]) + if len(s) < max_width: + fmt_value += " "*(max_width-len(s)) + out_list.append( fmt_value + " = " + cgi.escape(value_str) + "\n") + else: + fmt_value = fmt % dir_path + out_list.append( fmt_value + " = " + value_str + "\n") + + if not((j+1) % 5): + out_list.append("\n") + + if show_long: + out_str = "".join(out_list) + else: + # Display names only + if Set_params["allow_xml"] and OTrace.html_wrapper: + out_str = self.line_wrap(path_attrs.keys(), html_attrs=path_attrs, pre_count=pre_count) + else: + out_str = self.line_wrap(path_attrs.keys()) + + if Set_params["allow_xml"] and OTrace.html_wrapper: + out_str = OTrace.html_wrapper.wrap('
'+out_str+'
') + return (out_str, err_str) + + # rm (for database) + out_list = [] + delete_lists = collections.defaultdict(list) + for j, dir_path in enumerate(matches): + full_path = self.full_path_comps(dir_path) + base_subdir = full_path[BASE_OFFSET] + if len(full_path) < BASE2_OFFSET or base_subdir not in self.lazy_dirs or ENTITY_CHAR in full_path: + err_str += "Invalid path: " +dir_path+" " + else: + entity_key = self.lazy_dirs[base_subdir].key_from_path(full_path[BASE1_OFFSET:]) + if not entity_key: + err_str += "Invalid path: " +dir_path+" " + else: + delete_lists[base_subdir].append(entity_key) + + if not err_str and delete_lists: + for base_subdir, key_list in delete_lists.items(): + deleted_keys = self.lazy_dirs[base_subdir].delete_entities(key_list, recursive=recursive) + out_list += "Deleted %s keys" % len(deleted_keys) + + out_str = "".join(out_list) + return (out_str, err_str) + + def cmd_set(self, cmd, comps, line, rem_line): + """set or display parameters, returning (out_str, err_str)""" + out_str, err_str = "", "" + if len(comps) > 1: + # Set parameter + try: + name, value = comps[0:2] + if value == "None": + value = None + if name not in Help_params: + return ("", "Invalid parameter: "+name) + if name == "log_format": + OTrace.callback_handler.logformat(value.replace("+", " ")) + elif name == "log_level": + OTrace.callback_handler.loglevel(int(value)) + elif name == "log_remote": + OTrace.callback_handler.remote_log(value, remove=(not value)) + elif name == "log_truncate": + OTrace.callback_handler.tracelen(int(value)) + elif name == "pickle_file": + if PickleInterface.write_connection: + PickleInterface.write_connection.close() + PickleInterface.write_connection = None + if value: + PickleInterface.create_pickle_db(expandpath(value)) + elif name == "unpickle_file": + pass + elif name == "trace_active": + OTrace.trace_active = bool(value) + else: + param_type = type(Set_params[name]) + if param_type is bool: + value = (value.lower() == "true") + else: + value = param_type(value) + if name == "safe_mode" and not self.allow_unsafe and not value: + return ("", "Switching to unsafe mode not permitted") + if name == "password" and ":" not in value: + return ("", "No salt in encrypted password; SET PASSWORD FAILED") + Set_params[name] = value + except Exception, excp: + return (out_str, "Error in setting parameter %s: %s" % (name, excp)) + return ("%s = %s" % (name, value), "") + else: + # Display parameters + names = [] + if not comps: + names = Help_params.keys() + elif len(comps) == 1 and comps[0] in Help_params: + names = [comps[0]] + for name in names: + if name == "log_format": + value = OTrace.callback_handler.logformat().replace(" ", "+") + elif name == "log_level": + value = OTrace.callback_handler.loglevel() + elif name == "log_remote": + value = OTrace.callback_handler.remote_log() + elif name == "log_truncate": + value = OTrace.callback_handler.tracelen() + elif name == "pickle_file": + value = PickleInterface.write_file + elif name == "unpickle_file": + value = PickleInterface.read_file + elif name == "trace_active": + value = OTrace.trace_active + else: + value = Set_params[name] + out_str += "%s = %s\n" % (name, value) + return (out_str, err_str) + + def cmd_cd(self, new_dir): + """cd to new_dir, returning (out_str, err_str)""" + out_str, err_str = "", "" + cur_path = [comp[INAME] for comp in self.cur_fullpath] + if new_dir == ENTITY_CHAR and ENTITY_CHAR not in self.locals_dict and ENTITY_CHAR in cur_path: + # cd to entity "root" directory + key_comps, sub_comps, entity_key = self.entity_path_comps(self.full_path_comps(new_dir)) + entity_dir = PATH_SEP.join([".."]*(len(cur_path)-len(key_comps)-1)) + if entity_dir: + err_str = self.change_workdir(entity_dir) + if err_str: + return (out_str, err_str) + elif new_dir and len(cur_path) > BASE_OFFSET and cur_path[BASE_OFFSET] in self.lazy_dirs: + # Currently in a lazy directory + base_subdir = cur_path[BASE_OFFSET] + lazy_interface = self.lazy_dirs[base_subdir] + root_depth = lazy_interface.root_depth + sub_dir = new_dir + new_path = self.full_path_comps(new_dir) + if len(new_path) > BASE_OFFSET and new_path[BASE_OFFSET] == base_subdir: + # Staying in same lazy directory + if len(new_path) > root_depth+BASE_OFFSET and (is_absolute_path(new_dir) or + len(cur_path) <= root_depth+BASE_OFFSET or + new_path[root_depth+BASE_OFFSET] != cur_path[root_depth+BASE_OFFSET]): + # Need to query database for root key tree + db_dir = PATH_SEP.join([".."]*(len(new_path)-(root_depth+BASE1_OFFSET))) + sub_dir = PATH_SEP.join(new_path[root_depth+BASE1_OFFSET:]) + if db_dir: + err_str = self.change_workdir(db_dir) + if err_str: + return (out_str, err_str) + # Retrieve key tree for root key + root_key = lazy_interface.key_from_path(new_path[BASE1_OFFSET:root_depth+BASE1_OFFSET]) + locals_dict = lazy_interface.get_child_tree(root_key) + for j in xrange(BASE1_OFFSET, root_depth+BASE1_OFFSET): + locals_dict = locals_dict[new_path[j]] + self.locals_dict = locals_dict + self.cur_fullpath.append( (new_path[root_depth+BASE_OFFSET], self.locals_dict) ) + + if ENTITY_CHAR in new_path: + key_comps, sub_comps, entity_key = self.entity_path_comps(new_path) + if not entity_key: + return (out_str, "Invalid key") + + if len(self.cur_fullpath) < len(key_comps)+1 or self.cur_fullpath[:len(key_comps)+1] != (key_comps+[ENTITY_CHAR]): + # Need to query database for entity + sub_dir = PATH_SEP.join(sub_comps) + key_dir = self.get_rel_dir(PATH_SEP+PATH_SEP.join(key_comps)) + if key_dir: + err_str = self.change_workdir(key_dir) + if err_str: + return (out_str, err_str) + entity = lazy_interface.get_entity(entity_key) + if isinstance(entity, dict): + self.locals_dict = entity + elif entity: + self.locals_dict = ObjectDict(entity) + else: + self.locals_dict = {} + self.cur_fullpath.append( (ENTITY_CHAR, self.locals_dict) ) + + if sub_dir: + err_str = self.change_workdir(sub_dir) + if err_str: + return (out_str, err_str) + + else: + err_str = self.change_workdir(new_dir) + if err_str: + return (out_str, err_str) + + return (out_str, err_str) + + def cmd_tag_untag(self, cmd, comps, line, rem_line): + """Tag/untag object, returning (out_str, err_str)""" + out_str, err_str = "", "" + if not self.cur_fullpath: + return (out_str, err_str) + value = None + if not comps or comps[0] == ".": + if self.has_trc("frame"): + if "self" in self.locals_dict: + value = self.locals_dict["self"] + else: + return (out_str, "No self object to %s in trace context" % cmd) + else: + value = self.get_cur_value() + else: + full_path_list = comps[0].split(".") + value = self.get_subdir(self.locals_dict, full_path_list, value=True) + + try: + if cmd == "tag": + if len(comps) > 1: + tag = comps[1] + else: + tag = "id" + + OTrace.tag(value, tag) + else: + OTrace.untag(value) + except Exception, excp: + return (out_str, "Error in %s: %s" % (cmd, excp)) + return (out_str, err_str) + + def cmd_unpickle(self, cmd, comps, line, rem_line): + """Unpickle file, returning (out_str, err_str)""" + out_str, err_str = "", "" + if not comps: + return (out_str, "Please specify filename") + filename = expandpath(comps.pop(0)) + filters = {} + while comps: + comp = comps.pop(0) + field, sep, value = comp.partition("=") + if sep: + filters[str(field)] = value + try: + PickleInterface.open_pickle_db(filename) + keys = PickleInterface.read_keys_pickle_db(**filters) + for key in keys: + dirs = ContextDict.split_trace_id(key) + context = OTrace.base_context[PICKLED_DIR] + for cdir in dirs: + if cdir not in context: + context[cdir] = {} + context = context[cdir] + context[ENTITY_CHAR] = None + except Exception, excp: + err_str = "Error in unpickling from %s: %s" % (filename, excp) + raise # ABC + return (out_str, err_str) + + def cmd_pr(self, cmd, comps, line, rem_line, cmd_opts): + """Print expression, returning (out_str, err_str)""" + out_str, err_str = "", "" + if len(comps) == 1 and comps[0].startswith(PATH_SEP+BASE_DIR+PATH_SEP): + tem_path = comps[0][len(PATH_SEP+BASE_DIR+PATH_SEP):].replace(PATH_SEP, ".").split(".") + obj_value = self.get_subdir("base", tem_path, value=True) + try: + out_str = str(obj_value) + except Exception, excp: + err_str = str(excp) + return (out_str, err_str) + + if "\n" in rem_line or "\r" in rem_line: + err_str = "Line breaks are not permitted in expressions" + + elif Set_params["safe_mode"] and ("(" in rem_line or re.search(r"[^=]=[^=]", rem_line)): + err_str = "Open parenthesis and assignment operator are not permitted in expressions in safe mode; set safe_mode False" + elif rem_line.lstrip().startswith("import ") or ASSIGNMENT_RE.match(rem_line): + return out_str, "Use 'exec' or '!' to execute import or assignment statements" + else: + if Set_params["pretty_print"]: + self.locals_dict["_otrace_pformat"] = otrace_pformat + pr_command = "_otrace_pformat("+rem_line+", width=%d)" % self.tty_width + out_str, err_str = self.push(pr_command, batch=True) + del self.locals_dict["_otrace_pformat"] + elif cmd_opts.batch: + out_str, err_str = self.push(rem_line, batch=True) + else: + # Use 'print' command (pformat does not remove quotes from strings) + self.locals_dict["_otrace_stdout"] = self._stdout + self.locals_dict["_otrace_value"] = self.get_cur_value() + if sys.version_info[0] < 3: + pr_command = "print >>_otrace_stdout, "+rem_line + else: + pr_command = "print("+rem_line+", file=_otrace_stdout)" + out_str, err_str = self.push(pr_command) + del self.locals_dict["_otrace_value"] + del self.locals_dict["_otrace_stdout"] + if err_str and re.search(r"[^=]=[^=]", rem_line): + err_str += "\n Looks like a python assignment statement; try prefixing with 'exec' or '!'" + return (out_str, err_str) + + def cmd_exec(self, cmd, comps, line, rem_line, cmd_opts): + """Execute statement, returning (out_str, err_str)""" + out_str, err_str = "", "" + if Set_params["safe_mode"]: + out_str, err_str = "", "Code execution not permitted in safe mode; set safe_mode False" + else: + out_str, err_str = self.push(rem_line, batch=False) + out_str = out_str or "" + return (out_str, err_str) + + def edit_view(self, cmd, comps, inline=False, here_doc=None): + """Handle edit/view of files, returning (out_str, err_str) or raising AltHandler""" + out_str, err_str = "", "" + if not comps: + if self.in_base_dir(): + raise AltHandler() + else: + return ("", "Must specify filename to edit/view") + + comps = comps[:] # Copy, just in case + + comp0 = comps[0] + if is_absolute_path(comp0): + # Absolute path + if comp0 == PATH_SEP+BASE_DIR or comp0.startswith(PATH_SEP+BASE_DIR+PATH_SEP): + raise AltHandler() + filepath = comp0 + + else: + # Relative path + if self.in_base_dir(): + raise AltHandler() + filepath = self.make_path_str() + PATH_SEP + comp0 + + content = None + if os.path.exists(filepath): + try: + with open(os_path(filepath), "r") as f: + content = f.read() + except Exception, excp: + return (out_str, "Error in reading from '%s': %s" % (filepath, excp)) + + basename, extension = os.path.splitext(filepath) + if extension: + filetype = FILE_EXTENSIONS.get(extension[1:].lower(), "") + else: + filetype = "" + if cmd == "view": + if content is None: + return ("", "File %s not found" % filepath) + if inline: + # Inline display + return (content, err_str) + else: + # Editor display + OTrace.callback_handler.editback(content, filepath=filepath, filetype=filetype, editor=Set_params["editor"]) + return (out_str, err_str) + + # Edit file + if here_doc is None: + mod_content, err_str = OTrace.callback_handler.editback(content, filepath=filepath, filetype=filetype, editor=Set_params["editor"], modify=True) + if mod_content is None and err_str is None: + # Delayed modification + return ("_NoPrompt_", None) + if err_str: + return (out_str, err_str) + else: + mod_content = here_doc + + if mod_content == content: + # No changes + return (out_str, "No changes") + + try: + # Write modified content + with open(os_path(filepath), "w") as f: + f.write(mod_content) + except Exception, excp: + return (out_str, "Error in saving file '%s': %s" % (filepath, excp)) + return ("", "") + +class TraceOpts(object): + """Trace match options + """ + def __init__(self, trace_name, argmatch={}, break_count=-1, trace_call=False, trace_return=False, + break_action=None, match_tag="", access_type=""): + self.trace_name = trace_name + self.argmatch = argmatch + self.break_count = break_count + self.trace_call = trace_call + self.trace_return = trace_return + self.break_action = break_action + self.match_tag = match_tag + self.access_type = access_type + + def __str__(self): + if self.match_tag: + taglabel = "tagged" if self.match_tag == "*" else "tagged"+self.match_tag + else: + taglabel = "" + condition = self.argmatch or self.access_type or ( taglabel if taglabel + else ("all" if self.trace_call and self.trace_return + else ("call" if self.trace_call + else ("return" if self.trace_return else "") ) ) ) + options = "" + if self.break_action: + options = options + "-a %s " % self.break_action + + if condition: + options = options + "-c %s " % condition + + if self.break_count: + options = options + "-n %d " % self.break_count + + return "%s%s" % (options, self.trace_name) + +class ContextDict(dict): + """Dictionary for saving trace contexts + """ + context_types = {"as": "asserts", "br": "breaks", "db": "dbaccess", "ex": "exceptions", "hd": "holds", "tg": "tags", "tr": "traces"} + + def __init__(self, dirname=""): + super(ContextDict, self).__init__() + self.dirname = dirname + self.trace_ids = collections.defaultdict(list) + + @classmethod + def make_trace_id(cls, context_type, fullmethodname, id_label, trace_timestamp): + """Creates trace_id, and return (trace_id, context_id) + """ + ctype = "un" + for cshort, clong in cls.context_types.items(): + # Locate short context type + if context_type == clong: + ctype = cshort + + if id_label: + context_id = ctype + "-" + id_label + else: + context_id = ctype + + # Replace any separators in trace_id components with .. + fullmethodname = fullmethodname.replace(TRACE_ID_SEP, "..") + context_id = context_id.replace(TRACE_ID_SEP, "..") + + return (TRACE_ID_SEP.join((fullmethodname, context_id, trace_timestamp)), context_id) + + @classmethod + def split_trace_id(cls, trace_id): + fullmethodname, context_id, trace_timestamp = trace_id.split(TRACE_ID_SEP) + + ctype, sep, id_label = context_id.partition("-") + if ctype not in cls.context_types: + context_type = "unknown" + else: + context_type = cls.context_types[ctype] + + return [context_type, fullmethodname, context_id, trace_timestamp] + + def remove_context(self, trace_id, keep_holds=False): + """ Remove context (called from add_context, with RLock; should not block otrace thread) + """ + with Trace_rlock: + context_type, fullmethodname, context_id, trace_timestamp = self.split_trace_id(trace_id) + if context_type == "holds": + try: + context = self[context_type][fullmethodname][context_id][trace_timestamp] + self_arg = context["self"] + if self_arg and getattr(self_arg, OTrace.resume_attr, None): + if keep_holds: + # Keep on hold + return + else: + # Cancel hold (within RLock; this call should not block otrace thread) + resume_from_hold(self_arg) + except Exception: + pass + + context_trace_ids = self.trace_ids[context_type] + + # Remove trace id from list + try: + indx = context_trace_ids.index(trace_id) + if indx >= 0: + del context_trace_ids[indx] + except Exception: + pass + + # Remove context from dictionary + try: + del self[context_type][fullmethodname][context_id][trace_timestamp] + if not self[context_type][fullmethodname][context_id]: + del self[context_type][fullmethodname][context_id] + if not self[context_type][fullmethodname]: + del self[context_type][fullmethodname] + except Exception: + pass + + def add_context(self, new_context, trace_id): + """Adds context + """ + with Trace_rlock: + context_type, fullmethodname, context_id, trace_timestamp = self.split_trace_id(trace_id) + + context_trace_ids = self.trace_ids[context_type] + context_trace_ids.append(trace_id) + + maxdir = "max_" + self.dirname + if Set_params.get(maxdir) and len(context_trace_ids) > Set_params[maxdir]: + # Remove oldest entry of this type + if context_type != "breaks": # Breakpoints are removed when resuming + self.remove_context(context_trace_ids[0]) + + # Add new entry + if context_type not in self: + self[context_type] = {} + + if fullmethodname not in self[context_type]: + self[context_type][fullmethodname] = {} + + if context_id not in self[context_type][fullmethodname]: + self[context_type][fullmethodname][context_id] = {} + + self[context_type][fullmethodname][context_id][trace_timestamp] = new_context # Strong reference + + return trace_id + + +class TraceCallback(object): + """ Override this class to control logging of callbacks etc + """ + def __init__(self, log_level=logging.ERROR, trace_len=0, logger=None, log_handler=None): + self.log_level = log_level + self.trace_len = trace_len + self.remote_host = None + self.logger = logger + if not self.logger: + self.logger = logging.getLogger("") + self.logger.propagate = 0 + + if not self.logger.handlers and not log_handler: + log_handler = CallbackLogHandler() + + if log_handler: + log_handler.setLevel(level=logging.WARNING) + log_handler.setFormatter(logging.Formatter(fmt="%(name).4s%(levelname).1s %(message)s")) + self.logger.addHandler(log_handler) + + def loglevel(self, level=None): + """ Set/return logging level + Override this method, if need be. + """ + if level is None: + return self.logger.getEffectiveLevel() + self.logger.setLevel(level) + return level + + def tracelen(self, trace_len=None): + """ Set trace message length, if not None. + Return current trace message length + """ + if trace_len is not None: + self.trace_len = trace_len + return self.trace_len + + def logformat(self, fmt=None): + return "" + + def logmessage(self, log_level, msg, exc_info=None, logtype="", plaintext=""): + # If log_level is None, always display message + if logtype or log_level is None or log_level >= self.log_level: + if OShell.instance: + prefix, suffix = OShell.instance.switch_screen(logtype) + else: + prefix, suffix = "", "" + sys.stderr.write(prefix+(plaintext or msg)+"\n"+suffix) + + def remote_log(self, host=None, remove=False): + """ Set host (:port) for remote logging. If host is omitted, return current remote host. + If remove, remove current remote host. + """ + if remove: + self.remote_host = None + for handler in self.logger.handlers: + # Remove any socket handlers + if isinstance(handler, logging.handlers.SocketHandler): + self.logger.removeHandler(handler) + handler.close() + self.logger.addHandler(logging.StreamHandler()) + return + + if not host: + return self.remote_host + + self.remote_host = host + + addr, sep, port = host.partition(":") + if port: + port = int(port) + else: + port = logging.handlers.DEFAULT_TCP_LOGGING_PORT + + for handler in self.logger.handlers: + # Remove any stream handlers + if isinstance(handler, logging.StreamHandler): + self.logger.removeHandler(handler) + handler.close() + self.logger.addHandler(logging.handlers.SocketHandler(addr, port)) + + + def display_log(self, msg, trace_id="", prefix="", logtype="trace"): + """Display log message, with message prefixed by trace_id or prefix""" + log_level = self.log_level + plaintext = "" + if trace_id: + if Set_params["allow_xml"] and OTrace.html_wrapper: + subpath_names = ContextDict.split_trace_id(trace_id) + markup = ["file:///"+PATH_SEP.join([BASE_DIR, RECENT_DIR]+subpath_names), "x-python/object", "cdls"] + plaintext = trace_id + " " + msg + prefix = OShell.html_fmt % tuple(markup + [cgi.escape(trace_id)]) + msg = cgi.escape(msg) + msg = prefix + " " + msg + log_level += 10 + if trace_id.startswith("breaks") or trace_id.startswith("holds"): + log_level += 10 + + elif prefix: + msg = prefix + ": " + msg + if Set_params["allow_xml"] and OTrace.html_wrapper: + plaintext = msg + msg = cgi.escape(msg) + + if Set_params["allow_xml"] and OTrace.html_wrapper: + pass + elif self.trace_len: + msg = msg[:self.trace_len] + + self.logmessage(log_level, msg, logtype=logtype, plaintext=plaintext) + + + def callback(self, trace_id, methodtype, modulename, classname, func_name, arg_val_pairs=[], + nameless_args_list=[], retro=False): + """ Handle function call trace. + Override this method, if need be. + """ + arg_vals_str = [] + for arg, val in arg_val_pairs: + if isinstance(val, type): + valstr = val.__name__ + else: + valstr = str(val) + arg_vals_str.append(arg+"="+valstr) + + arg_vals_str += map(repr, nameless_args_list) + call_str = ", ".join(arg_vals_str) + if retro: + func_name += "*" + + self.display_log(call_str, trace_id=trace_id, prefix=("%s.%s" % (classname, func_name)) ) + + def returnback(self, trace_id, methodtype, modulename, classname, func_name, result): + """ Handle function return trace. + Override this method, if need be. + """ + return_str = "return %s" % (str(result),) + self.display_log(return_str, trace_id=trace_id, prefix=("%s.%s" % (classname, func_name)) ) + + def accessback(self, trace_id, op_type, key_str, entity): + """ Handle database access callback + Override this method, if need be. + """ + msg = "access %s %s" % (op_type, key_str) + self.display_log(msg, trace_id=trace_id) + + def editback(self, content, filepath="", filetype="", editor="", modify=False): + """ Create temp file with content, and display using editor. + If not modify, return (content, None) after displaying content. + On successful modification, returns (modified_content, ""). + On error, returns (None, err_str). + File is deleted before returning. + If implementing delayed modification, returns (None, None); + subsequent command can provide modified data through stdin "here" document. + """ + if not editor: editor = "vi" + tempname = None + try: + try: + # Create temp file for editing content + funit, tempname = tempfile.mkstemp(suffix=".py", text=True) + try: + os.write(funit, content) + except Exception, excp: + return (None, "Error in writing to temp file %s: %s" % (tempname, excp)) + finally: + os.close(funit) + + # Edit temp file + ret_code = subprocess.call([editor, tempname]) + if ret_code: + return (None, "Error in editing %s (%s): %s" % (filepath, tempname, ret_code)) + except Exception, excp: + return (None, "Error in editing %s (%s): %s" % (filepath, tempname, ret_code)) + + if not modify: + return (content, None) + else: + try: + # Read edited temp file + with open(os_path(tempname), "r") as tempf: + return (tempf.read(), "") + except Exception, excp: + return (None, "Error in reading temp file '%s': %s" % (tempname, excp)) + finally: + if tempname: + # Delete temp file + try: + os.remove(tempname) + except Exception: + pass + + +class DefaultCallback(TraceCallback): + """ Simple default callback implementation + """ + def logmessage(self, log_level, msg, exc_info=None, logtype="", plaintext=""): + # If log_level is None, always display message + if log_level is None or log_level >= self.log_level: + super(DefaultCallback, self).logmessage(None, msg, exc_info=exc_info, logtype=logtype, plaintext=plaintext) + +class CallbackLogHandler(logging.Handler): + def __init__(self): + self.recursion_lock = set() + logging.Handler.__init__(self) + + def emit(self, record): + if OTrace.callback_handler: + try: + thread_name = threading.currentThread().getName() + except Exception: + thread_name = "" + + if thread_name in self.recursion_lock: + # Recursive call to emit; ignore + return + self.recursion_lock.add(thread_name) + + try: + msg = self.format(record) + OTrace.callback_handler.logmessage(None, msg) + except Exception: + self.handleError(record) + finally: + self.recursion_lock.discard(thread_name) + + def flush(self): + pass + +class HtmlWrapper(object): + """ Wrapper for HTML output + """ + def wrap(self, html, msg_type=""): + return html + +class FunctionInfo(object): + """ Return object containing information about a function""" + def __init__(self, function, classname="", modulename="", methodtype=""): + self.fn = function + self.classname = classname + self.modulename = modulename + self.methodtype = methodtype + + func_code = function.func_code + func_defaults = function.func_defaults + argcount = func_code.co_argcount + self.argnames = func_code.co_varnames[:argcount] + if func_defaults: + self.argdefs = dict(zip(self.argnames[-len(func_defaults):], func_defaults)) + else: + self.argdefs = {} + +class OTrace(object): + """Object tracing class. + All methods are class methods + Class cannot be instantiated. + """ + class_trace_attr = "_otrace_class_trace" + orig_function_attr = "_otrace_orig_function" + unpatched_function_attr = "_otrace_unpatched_function" + patch_parent_attr = "_otrace_patch_parent" + patch_source_attr = "_otrace_patch_source" + orig_generator_name_attr = "_otrace_orig_generator_name" + hold_attr = "_otrace_hold" + resume_attr = "_otrace_resume" + trace_tag_attr = "_otrace_tag" + + interpreter = TraceInterpreter() + callback_handler = DefaultCallback() + hold_wrapper = None + eventloop_callback = None + html_wrapper = None + + default_context = {"__name__": "__otrace__", "__doc__": None, + "_trace_id": None, "_trace_related": {}} + + # Use lists or collections to avoid assigning value to class attributes, + # which could be hidden during subclassing + + base_context = {ALL_DIR: weakref.WeakValueDictionary(), + GLOBALS_DIR: {}, + LOCALS_DIR: {}, + PATCHES_DIR: {}, + # Strong references to contexts only in recent and saved + RECENT_DIR: ContextDict(dirname=RECENT_DIR), + SAVED_DIR: ContextDict(dirname=SAVED_DIR), + } + + prev_timestamp = [None] + + recent_pathnames = [BASE_DIR, GLOBALS_DIR] + recent_trace_id = [None] + recent_trace_context = [None] + + trace_id_set = set() + trace_log_set = set() + trace_names = {} + trace_keys = {} + + trace_all = False + trace_active = False + + def __new__(cls, *args, **kwargs): + raise OTraceException("Class cannot be instantiated") + + @classmethod + def setup(cls, start_path=[BASE_DIR, GLOBALS_DIR], callback_handler=None, + hold_wrapper=None, eventloop_callback=None): + cls.recent_pathnames = start_path + if callback_handler: + cls.callback_handler = callback_handler + if hold_wrapper: + cls.hold_wrapper = hold_wrapper + if eventloop_callback: + cls.eventloop_callback = eventloop_callback + + @classmethod + def class_method_names(cls, method, parent=None): + """Analyzes method and returns tuple (classname, methodname) + Raises OTraceException on failure + """ + classname = "" + methodname = "" + + if isinstance(method, str): + # String + if "." in method: + classname, methodname = method.split(".") + else: + methodname = method + + elif inspect.isclass(method): + # Class + classname = method.__name__ + + elif inspect.isclass(parent): + # Bound instance/class method + methodname = method.__name__ + classname = parent.__name__ + + elif inspect.isfunction(method): + # Function + methodname = method.__name__ + + elif hasattr(method, "__name__"): + # Object with attribute name + methodname = method.__name__ + else: + raise OTraceException("add_trace: Cannot trace object " + str(method)) + + return (classname, methodname) + + @classmethod + def set_database_root(cls, root_tree): + cls.base_context[DATABASE_DIR] = root_tree + + @classmethod + def set_web_root(cls, root_tree): + cls.base_context[WEB_DIR] = root_tree + + @classmethod + def add_trace(cls, method=None, parent=None, argmatch={}, break_count=-1, trace_call=False, + trace_return=False, break_action=None, match_tag="", access_type=""): + """To trace all, method = "*" + To list all methods traced, method = None + To trace class, method="classname." or classname + To trace method of any class, method=".methodname" + To trace bound method method ="classname.methodname" or self.methodname + To trace function, method = "functionname" or functionname + argmatch = {"arg1": "value1", "self.arg2": "value2", "arg3":{"entry31": "value31", "entry32","value32"}, + "arg4!=": "value4", "return": "retvalue"} + Returns full name of method traced, or null string + If neither trace_call nor trace_return are specified, only exceptions are traced. + To trace entity keys, trace /kind:name/... get/put/delete/modify/all + """ + with Trace_rlock: + if not method: + trace_names_list = cls.trace_names.keys() + cls.trace_keys.keys() + trace_names_list.sort() + if cls.trace_all: + trace_names_list.insert(0, "*") + return trace_names_list + + cls.trace_active = True + + if argmatch: + ret_match = [key == "return" or key.startswith("return.") for key in argmatch] + if any(ret_match): + trace_return = True + if break_action != "tag" and not all(ret_match): + trace_call = True + + if method == "*": + cls.trace_all = True + return "*" + elif isinstance(method, basestring) and method.startswith(PATH_SEP): + # Trace entity key + cls.trace_keys[method] = TraceOpts(method, argmatch=argmatch, break_count=break_count, access_type=access_type, + break_action=break_action) + return method + elif isinstance(method, basestring) and method[0] == TRACE_LABEL_PREFIX: + # Trace label for logging + fullname = method + elif isinstance(method, basestring) and method.startswith(TRACE_LOG_PREFIX): + # Trace log prefix for break point + fullname = method + cls.trace_log_set.add(fullname) + else: + if not isinstance(argmatch, dict): + raise OTraceException("add_trace: argmatch must be a dict instance") + + classname, methodname = cls.class_method_names(method, parent) + fullname = methodname if not classname else classname+"."+methodname + + trace_opts = TraceOpts(fullname, argmatch=argmatch, break_count=break_count, trace_call=trace_call, + trace_return=trace_return, break_action=break_action, match_tag=match_tag) + + cls.trace_names[fullname] = trace_opts + + return fullname + + @classmethod + def remove_trace(cls, method=None, parent=None): + """To remove all traces, specify method = "all". + Returns full name of method untraced, or null string + """ + if not method: + raise OTraceException("Specify name to untrace or 'all'") + + if method == "all": + cls.clear_trace() + return "all tracing" + + with Trace_rlock: + untrace_name = "" + retvalue = "" + + if method == "*": + cls.trace_all = False + retvalue = "*" + elif method.startswith(PATH_SEP): + # Untrace entity key + try: + del cls.trace_keys[method] + retvalue = method + except Exception: + pass + elif isinstance(method, basestring) and method[0] == TRACE_LABEL_PREFIX: + # Untrace label + untrace_name = method + elif isinstance(method, basestring) and method.startswith(TRACE_LOG_PREFIX): + # Untrace log + untrace_name = method + cls.trace_log_set.discard(untrace_name) + else: + try: + classname, methodname = cls.class_method_names(method, parent) + untrace_name = methodname if not classname else classname+"."+methodname + except Exception: + pass + + if untrace_name: + try: + del cls.trace_names[untrace_name] + except Exception: + pass + + if (not cls.trace_id_set and not cls.trace_all and + not cls.trace_names and not cls.trace_keys): + cls.trace_active = False + + return retvalue or untrace_name + + @classmethod + def make_tag(cls, obj, tag=""): + if not tag or tag == "id": + return "0x%x" % id(obj) + elif tag == "time": + return cls.get_timestamp() + else: + return tag + + @classmethod + def set_tag(cls, obj, trace_tag): + setattr(obj, cls.trace_tag_attr, trace_tag) + + @classmethod + def get_tag(cls, obj): + return getattr(obj, cls.trace_tag_attr, None) + + @classmethod + def remove_tag(cls, obj): + with Trace_rlock: + deleted_value = getattr(obj, cls.trace_tag_attr, None) + delattr(obj, cls.trace_tag_attr) + return deleted_value + + @classmethod + def track_trace_id(cls, trace_id=""): + with Trace_rlock: + if trace_id: + cls.trace_id_set.add(trace_id) + else: + cls.trace_id_set.clear() + + if (not cls.trace_id_set and not cls.trace_all and + cls.trace_names and not cls.trace_keys): + cls.trace_active = False + + @classmethod + def clear_trace(cls): + """Clear all tracing + """ + with Trace_rlock: + cls.trace_active = False + cls.trace_all = False + + cls.trace_id_set.clear() + cls.trace_names.clear() + cls.trace_keys.clear() + + @classmethod + def break_flow(cls, trace_id, action="break"): + if action == "break": + # Send signal to OShell + break_event = threading.Event() + OShell.instance.break_queue.put( (trace_id, break_event) ) + # Wait for signal from OShell + break_event.wait() + else: + # Invoke debugger (suspends oshell input until debugger exits) + OShell.invoke_debugger(action) + + @classmethod + def tracereturn(cls, return_value): + """Handles tracing of return values + """ + if not cls.trace_active: + return + + # Get caller's frame + frameobj, filename, lineno, function, code_context, index = inspect.stack(context=0)[1] + + args, varargs, varkw, locals_dict = inspect.getargvalues(frameobj) + + try: + ##frame_info = (filename, lineno, function, code_context) + + # Copy locals_dict (just to be safe in avoiding cyclic references) + locals_dict = locals_dict.copy() + + if "self" in locals_dict: + try: + # Attach a string representation of local variables to self object of returning function + setattr(locals_dict["self"], "_trace_"+function+"_locals", str(locals_dict)) + except Exception: + pass + + return return_value + finally: + # Clean up to avoid cyclic references to objects in frame + del frameobj, args, varargs, varkw, locals_dict + + @classmethod + def traceassert(cls, condition, label="", action=None): + """Trace assertions. + If not tracing, simply acts like "assert condition, label". + If action=="break", break execution if condition is False. + If action=="ipdb" or "pdb", invokes ipdb/pdb if condition is False. + If action=="hold", returns a callable object that accepts a single argument, callback, + wrapped in a hold_wrapper. + The callable object will schedule an immediate callback in the event loop if condition is True, + but delays the callback until the "resume" command, if condition is False. + """ + if condition: + # Asserted condition is true; if "hold" action, schedule immediate callback + return cls.hold_wrapper(schedule_callback) if action == "hold" and cls.hold_wrapper else None + + if not cls.trace_active: + assert condition, label + return + + frame_records = inspect.stack(context=Set_params["assert_context"])[1:] + frameobj, filename, lineno, funcname, code_context, index = frame_records[0] + frame_records.reverse() + + argvalues = inspect.getargvalues(frameobj) + args, varargs, varkw, caller_locals_dict = argvalues + + try: + fullmethodname = funcname + methodtype = "" + classname = "" + modulename = "" + self_arg = caller_locals_dict.get("self") + if self_arg: + try: + classname = self_arg.__class__.__name__ + if classname: + fullmethodname = classname + "." + funcname + methodtype = "instancemethod" + except Exception: + pass + + # Nested locals dict + locals_dict = cls.traverse_framestack(frame_records, + copy_locals=bool(Set_params["assert_context"])) + locals_dict.set_trc("argstr", inspect.formatargvalues(*argvalues)) + + if action == "break": + context_type = "breaks" + elif action == "hold": + context_type = "holds" + else: + context_type = "asserts" + trace_context, trace_id = cls.create_context(fullmethodname, self_arg, locals_dict, + id_label=label, context_type=context_type) + cls.callback_handler.callback(trace_id, methodtype, modulename, classname, funcname) + + if action == "hold": + return check_for_hold(self_arg) + elif action in BREAK_ACTIONS: + cls.break_flow(trace_id, action=action) + finally: + # Clean up to avoid cyclic references to objects in frame + del frame_records, frameobj, argvalues, args, varargs, varkw, caller_locals_dict + + try: + del locals_dict + except Exception: + pass + + return None + + @classmethod + def untag(cls, obj): + """Removes tag from object + """ + return cls.remove_tag(obj) + + @classmethod + def tag(cls, obj, tag, **kwargs): + """ Tags object, where tag = "id" (default), "time", or any other string. + kwargs are additional variables (dict) to save in the tag context + """ + if not isinstance(obj, object): + raise Exception("Only instances of object can be tagged") + + self_arg = obj + trace_tag = cls.make_tag(self_arg, tag=tag) + + locals_dict = {} + if kwargs: + locals_dict.update(kwargs) + + locals_dict["self"] = self_arg + + methodtype = "" + classname = obj.__class__.__name__ + function = "" + modulename = "" + fullmethodname = classname + context_type = "tags" + trace_context, trace_id = cls.create_context(fullmethodname, self_arg, locals_dict, + id_label=trace_tag, context_type=context_type) + cls.set_tag(self_arg, trace_id) + + cls.callback_handler.callback(trace_id, methodtype, modulename, classname, function) + return trace_id + + + @classmethod + def get_timestamp(cls): + with Trace_rlock: + TRACE_TIMESTAMP_FORMAT = "%y%m%d-%H-%M-%S" + ##TRACE_TIMESTAMP_FORMAT = "%H-%M-%S" + utctime = datetime.datetime.utcnow() + short_timestamp = utctime.strftime(TRACE_TIMESTAMP_FORMAT) + long_timestamp = short_timestamp + ".%06d" % (utctime.microsecond//1000, ) + + if not cls.prev_timestamp[0]: + trace_timestamp = short_timestamp + elif long_timestamp.startswith(cls.prev_timestamp[0]): + # Current timestamp string overlaps with previous; add fractional seconds + prev_len = len(cls.prev_timestamp[0]) + if prev_len > len(short_timestamp): + # Add extra decimal digit of time-precision to distinguish timestamp + trace_timestamp = long_timestamp[:prev_len+1] + else: + # Add first decimal digit (past decimal point) + trace_timestamp = long_timestamp[:prev_len+2] + else: + # Select minimum length non-overlapping timestamp + for j, ch in enumerate(cls.prev_timestamp[0]): + if ch != long_timestamp[j]: + slen = max(j+1, len(short_timestamp) ) + trace_timestamp = long_timestamp[:slen] + break + + # Save generated timestamp + cls.prev_timestamp[0] = trace_timestamp + return trace_timestamp + + @classmethod + def create_context(cls, fullmethodname, self_arg, locals_dict, id_label="", + context_type="traces", excp=None, exc_info=None): + """Creates new context (locals_dict) and returns (trace_id, trace_context) + """ + trace_id, context_id = ContextDict.make_trace_id(context_type, fullmethodname, id_label, cls.get_timestamp()) + new_context_path = [BASE_DIR, RECENT_DIR] + ContextDict.split_trace_id(trace_id) + if self_arg is not None: + if context_type == "holds" and cls.hold_wrapper: + # Set hold callback attribute + # Hold handler should return a callable entity (for use by trampoline) + # When called, the callable should accept a callback function as the argument + # and should set the resume_attr of self_arg to the callback function (or a proxy). + # NOTE: callback function should not block otrace thread; it should schedule a callback in the event loop + try: + setattr(self_arg, cls.hold_attr, + cls.hold_wrapper(HoldHandler(self_arg, PATH_SEP+PATH_SEP.join(new_context_path)) ) ) + except Exception: + pass + + new_context = TraceDict(locals_dict) + new_context.set_trc("id", trace_id) + new_context.set_trc("thread", threading.currentThread().getName()) + new_context.set_trc("context", context_type) + new_context.set_trc("related", {}) + new_context["__name__"] = "__console__" + new_context["__doc__"] = None + + if Set_params["append_traceback"] and excp and not isinstance(excp, StopIteration): + # Append innermost traceback to exception string + # To prevent information leakage, partition exception string at newline, + # if using exception to return string values + excp_str = excp.args[0] if excp.args and isinstance(excp.args[0], basestring) else "" + iappend = excp_str.find("\n") + if iappend < 0: + iappend = len(excp_str) + excp_str += "\n" + + pre_str, post_str = excp_str[:iappend+1], excp_str[iappend+1:] + if post_str.startswith(TRACE_ID_PREFIX): + tb_str = "" + j = post_str.find("\n") + if j > 0: + prev_trace_id = post_str[len(TRACE_ID_PREFIX):j] + post_str = post_str[j+1:] + # Link to previous trace id in current context + new_context[TRACE_ID_PREFIX+prev_trace_id] = None + else: + # Attach first (innermost) traceback to exception message + tb_str = "".join(format_traceback(exc_info)) if exc_info else "" + + # Insert current trace id in exception string before traceback + new_excp_str = pre_str + TRACE_ID_PREFIX + trace_id + "\n" + tb_str + post_str + new_context.set_trc("exception", new_excp_str) + if excp.args: + excp.args = tuple([new_excp_str] + list(excp.args[1:])) + else: + excp.args = (new_excp_str,) + + with Trace_rlock: + # Update path for newest trace entry + cls.base_context[RECENT_DIR].add_context(new_context, trace_id) + cls.base_context[ALL_DIR][trace_id] = new_context # Save weak reference to all traces + + if context_type == "tags" and Set_params["save_tags"]: + cls.base_context[SAVED_DIR].add_context(new_context, trace_id) + + del cls.recent_pathnames[:] + cls.recent_pathnames += new_context_path + + cls.recent_trace_id[0] = trace_id + cls.recent_trace_context[0] = new_context + + if PickleInterface.write_connection: + PickleInterface.write_pickle_db(trace_id, new_context) + return (new_context, trace_id) + + @classmethod + def remove_break_point(cls, trace_id): + with Trace_rlock: + subpath_names = [RECENT_DIR] + ContextDict.split_trace_id(trace_id) + if subpath_names == cls.recent_pathnames[BASE_OFFSET:]: + del cls.recent_pathnames[:] + cls.recent_pathnames += [BASE_DIR, GLOBALS_DIR] + cls.base_context[RECENT_DIR].remove_context(trace_id) + + @classmethod + def evaluate_in_context(cls, expression, trace_id=None, globals_dict={}, print_out=False): + """ Evaluate expression; return output string, if print_out is True, + Use the argument variables associated with trace_id as the context. + If trace_id is None, use last trace context. + If trace_id is not recognized, or is set to null string, the default context is used. + Returns tuple (out_str, err_str) + """ + context = None + if trace_id is None: + recent_traces = cls.base_context[RECENT_DIR].get("traces") + if recent_traces: + trace_id = recent_traces.keys()[0] + context = cls.base_context[RECENT_DIR]["traces"][trace_id] + else: + context = cls.base_context[ALL_DIR].get(trace_id) + + if not context: + context = cls.default_context + + out_str, err_str = cls.interpreter.evaluate(expression, locals_dict=context, + globals_dict=globals_dict, print_out=print_out) + if err_str is None: + err_str = out_str + out_str = "" + return (out_str, err_str) + + @classmethod + def traverse_framestack(cls, frame_records, exc_info=None, copy_locals=False): + """Traverses frame stack and initializes locals_dict for each frame, + and returns the final locals_dict + (for an exception, this would be for the frame where exception occurred) + """ + + prev_locals_dict = None + framestack = [] + for j, frame_record in enumerate(frame_records): + frameobj, srcfile, linenum, funcname, lines, index = frame_record + if funcname in IGNORE_FUNCNAMES: + continue + if copy_locals or j+1 == len(frame_records): + # Get local variables and arguments for frame (always for last frame) + args, varargs, varkw, locals_dict = inspect.getargvalues(frameobj) + # Copy locals_dict (just to be safe in avoiding cyclic references) + locals_dict = TraceDict(cls.copy_or_not(locals_dict, split=True)) + locals_dict.set_trc("argvalues", (args, varargs, varkw) ) + else: + # Do not save copy of local variables + locals_dict = TraceDict({}) + + locals_dict.set_trc("frame", (srcfile, linenum, funcname, lines) ) + locals_dict.set_trc("funcname", funcname) + if prev_locals_dict is not None: + locals_dict[UP_STACK] = prev_locals_dict + prev_locals_dict[DOWN_STACK] = locals_dict + prev_locals_dict = locals_dict + framestack.append( locals_dict.get_trc("frame") ) + + locals_dict.set_trc("framestack", LineList(framestack)) + locals_dict.set_trc("where", "-->".join([x[2] for x in framestack])) + + if exc_info: + locals_dict.set_trc("exc_stack", "".join(traceback.format_exception(*exc_info)) ) + try: + if Set_params["allow_xml"] and cls.html_wrapper: + locals_dict.set_trc("exc_context", cls.html_wrapper.wrap(cgitb.html(exc_info, 5)) ) + else: + locals_dict.set_trc("exc_context", cgitb.text(exc_info, 5)) + except Exception, excp: + pass + + return locals_dict + + @classmethod + def check_trace_match(cls, fn, fullmethodname, self_arg, arg_dict, trace_dict=None, + break_action=None, on_return=False, return_value=None, match_tag=""): + """ Trace dict example: + {"arg1": "value1", "self.arg2": "value2", "arg3":{"entry31": "value31", "entry32","value32"}, + "arg4!=": "value4", "return": "retvalue"} + Returns triplet tuple (trace_context, trace_id, related_id) + trace_context: None, or dict if a trace condition was matched + trace_id: Trace ID for matched condition (including classname prefix) + related_id: If no trace match, relate to tag match (or null string, if none) + + If break_action == "tag", a tag "trace_id" is returned on successful match, and + no new context id is created. + """ + if isinstance(trace_dict, dict): + if not on_return and trace_dict and all(key == "return" or key.startswith("return.") for key in trace_dict): + # No match (match only on return) + return (None, "", "") + + trace_matched = True # Assume match unless it turns out otherwise + matched_list = [] + if match_tag: + if match_tag == "*": + # Check all arguments for tags + check_args = arg_dict.items() + else: + # Check single argument for tag + check_args = [(match_tag, arg_dict[match_tag])] if match_tag in arg_dict else [] + + trace_matched = False + for arg_name, arg_value in check_args: + trace_tag = getattr(arg_value, cls.trace_tag_attr, None) + if trace_tag: + # Argument is tagged; matched + trace_matched = True + matched_list = ["tagged%s;%s" % (arg_name, trace_tag.split(":")[1])] + break + elif not trace_dict: + # Default match (no matching attributes specified) + pass + else: + for trace_name, trace_value in trace_dict.items(): + # For each traced attribute or argument + trace_name, cmp_op = strip_compare_op(trace_name) + arg_name, sep, prop_name = trace_name.partition(".") + if arg_name == "return": + # Trace match on returned value + if on_return: + actual_value = return_value + else: + # Skip return value matching if not returning from function + continue + else: + # Match argument value + if arg_name not in arg_dict: + continue + actual_value = arg_dict[arg_name] + if prop_name: + # Match argument properties + if not hasattr(actual_value, prop_name): + continue + actual_value = getattr(actual_value, prop_name) + + if isinstance(actual_value, dict) and isinstance(trace_value, dict): + # Dict value; match each trace dict entry with actual dict entry + for inner_key, inner_value in trace_value: + if inner_key in actual_value: + if compare(actual_value[inner_key], cmp_op, inner_value): + matched_list.append("%s.%s%s%s" % (trace_name, inner_key, cmp_op, inner_value)) + else: + trace_matched = False + break + if not trace_matched: + break + elif compare(actual_value, cmp_op, trace_value): + # Match "scalar" actual value with trace value + matched_list.append("%s%s%s" % (trace_name, cmp_op, trace_value)) + else: + trace_matched = False + break + + if trace_matched: + # Trace match succeeded + matched_list.sort() + id_label = ",".join(matched_list).replace(":", ";") + + if break_action == "tag": + # Automatic tagging; create only tag "trace_id" (no new trace context) + id_label += ";0x%x" % id(self_arg) + return (None, id_label, "") + + if not id_label: + id_label = "return" if on_return else "call" + if break_action == "break": + context_type = "breaks" + elif break_action == "hold": + context_type = "holds" + else: + context_type = "traces" + + # Copy args dict, cloning if need be + locals_dict = TraceDict(cls.copy_or_not(arg_dict, split=True)) + locals_dict.set_trc("stack", LineList(traceback.format_stack()[:-3])) + locals_dict.set_trc("func", fn) + trace_context, trace_id = cls.create_context(fullmethodname, self_arg, locals_dict, + id_label=id_label, context_type=context_type) + return (trace_context, trace_id, "") + + # No trace match requested, or trace match failed; check for any related trace_id + related_id = "" + if Set_params["trace_related"]: + if on_return: + if hasattr(return_value, cls.trace_tag_attr): + # Result with tag attribute is related + related_id = getattr(return_value, cls.trace_tag_attr) + else: + for arg_name, arg_value in arg_dict.items(): + if hasattr(arg_value, cls.trace_tag_attr): + # Argument with tag attribute is related + with Trace_rlock: + related_id = getattr(arg_value, cls.trace_tag_attr) + related_context = cls.base_context["tags"].get(related_id) + if related_context and self_arg and "__class__" in self_arg: + # Trace related + related_context.get_trc("related")[self_arg.__class__.__name__] = self_arg + break + + return (None, "", related_id) + + @classmethod + def copy_or_not(cls, obj, split=False, keep_self=True): + """Return deepcopy of obj if deep_copy parameter is set and obj has attribute + __deepcopy__ or is a (dict,list,set,tuple) else just return obj + If split, process list or dict values individually and re-group them, + creating at atleast a shallow copy of the list/dict in the process. + If keep_self (default), keep original self, and store copy as self_copy + """ + if not Set_params["deep_copy"]: + return obj + + if not split: + if hasattr(obj, "__deepcopy__") or isinstance(obj, (dict,list,set,tuple)): + return copy.deepcopy(obj) + else: + return obj + + if isinstance(obj, list): + return map(cls.copy_or_not, obj) + + if isinstance(obj, dict): + obj_copy = dict( zip(obj.keys(), map(cls.copy_or_not, obj.values())) ) + if keep_self and "self" in obj and obj_copy["self"] is not obj["self"]: + # Keep original self and store copy as self_copy + obj_copy["self_copy"] = obj_copy["self"] + obj_copy["self"] = obj["self"] + return obj_copy + + raise Exception("Unable to split %s" % type(obj)) + + @classmethod + def otrace_function_call(cls, info, *args, **kwargs): + """Auxiliary method used by wrapper in trace_function + """ + if not cls.trace_active: + return info.fn(*args, **kwargs) + + # Collect arguments + argcount = len(info.argnames) + args_pairs = zip(info.argnames, args) + kwargs_pairs = kwargs.items() + defaulted_pairs = [(x, info.argdefs[x]) for x in info.argnames[argcount-len(info.argdefs):argcount] + if x not in kwargs] + + info.arg_val_pairs = args_pairs + kwargs_pairs + defaulted_pairs + info.nameless_args_list = args[argcount:] + + # Create ordered dict of argument, value pairs + info.arg_dict = OrderedDict(info.arg_val_pairs) + + if info.methodtype == "instancemethod" and argcount: + # Extract self argument + info.self_arg = info.arg_val_pairs[0][1] + else: + info.self_arg = None + + # Check for full-name/function-name/class-name trace match + info.fullname = info.fn.__name__ if not info.classname else info.classname+"."+info.fn.__name__ + + with Trace_rlock: + trace_opts = cls.trace_names.get(info.fullname) # Full name match (most specific) + class_match = False + if not trace_opts: + if info.classname: + trace_opts = cls.trace_names.get("."+info.fn.__name__) # Method name match + if not trace_opts: + trace_opts = cls.trace_names.get(info.classname+".") # Class name match (least specific) + if trace_opts: + class_match = True + + info.name_matched = False + info.return_match_dict = None + + info.trace_context, info.trace_id, info.related_id = None, None, None + trace_call, trace_return, break_action, match_tag = (False, False, None, "") + + if cls.trace_all: + # Match any name + info.name_matched = True + + if trace_opts: + trace_call = trace_opts.trace_call + break_action = trace_opts.break_action + match_tag = trace_opts.match_tag + ##trace_return = trace_opts.trace_return + + if not trace_opts.argmatch and not match_tag and break_action != "tag": + # Name match only; no check for trace_id match + info.name_matched = True + + if trace_opts.trace_return: + if break_action == "tag": + # Tag operation; check match only upon return + info.return_match_dict = trace_opts.argmatch + elif class_match: + # Class name match; check for trace id match only upon return + info.return_match_dict = trace_opts.argmatch + elif trace_opts.argmatch: + # Check for return value match, if requested + ret_argmatch = [(key, value) for key, value in trace_opts.argmatch.items() if key == "return" or key.startswith("return.")] + if ret_argmatch: + info.return_match_dict = dict(ret_argmatch) + + if match_tag or (not class_match and trace_opts.trace_call): + # Check for trace_id or related_id match on argument values + info.trace_context, info.trace_id, info.related_id = cls.check_trace_match(info.fn, info.fullname, info.self_arg, info.arg_dict, trace_dict=trace_opts.argmatch, break_action=break_action, match_tag=match_tag) + + if info.trace_context: + with Trace_rlock: + # Trace matched; increment/decrement trace count + if trace_opts.break_count < 0: + # Trace match; increment trace match count (tracing yet to begin) + trace_opts.break_count += 1 + # Skip current match + info.trace_context, info.trace_id, info.related_id = None, None, None + + elif trace_opts.break_count > 0: + # Trace match; decrement trace match count + trace_opts.break_count -= 1 + if trace_opts.break_count == 0: + # Delete trace entry (tracing completed) + cls.remove_trace(trace_opts.trace_name) + + info.call_display = (info.name_matched or info.trace_context or info.related_id) and (trace_call or match_tag) + if info.call_display: + # Display call name trace + cls.callback_handler.callback(info.trace_id, info.methodtype, info.modulename, info.classname, info.fn.__name__, info.arg_val_pairs, info.nameless_args_list) + if break_action in BREAK_ACTIONS: + cls.break_flow(info.trace_id, action=break_action) + + if not trace_opts: + # Execute actual function call (not tracing) + return_value = info.fn(*args, **kwargs) + if isinstance(return_value, types.GeneratorType): + return return_value + else: + # Execute actual function call, but with tracing + try: + return_value = info.fn(*args, **kwargs) + + if isinstance(return_value, types.GeneratorType): + # Wrap generator in a trace generator + wrapped_gen = cls.trace_generator(info, trace_opts, return_value) + try: + # Save name of original generator for tracing + code_obj = getattr(return_value, "gi_code", None) + frame_obj = getattr(return_value, "gi_frame", None) + if not code_obj and frame_obj and hasattr(frame_obj, "f_code"): + code_obj = frame_obj.f_code + if code_obj: + gen_name = getattr(code_obj, "co_name","")+":"+getattr(code_obj, "co_filename","")+":*" + # NOTE: This fails, because generator attributes cannot be modified? + setattr(wrapped_gen, cls.orig_generator_name_attr, gen_name) + except Exception: + pass + + if info.call_display and break_action == "hold" and cls.hold_wrapper and info.self_arg is not None: + # Hold before generator executes + try: + context_path = [BASE_DIR, RECENT_DIR] + list(ContextDict.split_trace_id(info.trace_id)) + async_handler = cls.hold_wrapper(HoldHandler(info.self_arg, PATH_SEP+PATH_SEP.join(context_path), + resume_value=wrapped_gen) ) + setattr(info.self_arg, cls.hold_attr, async_handler) + return async_handler + except Exception: + pass + else: + return wrapped_gen + except StopIteration: + # Pass through StopIteration (which is used by trampoline for returns) + raise + except Exception, excp: + try: + # Traceback exception; re-raise within try..finally block to preserve trace + raise + finally: + id_label = getattr(excp, "__name__", None) + if not id_label and hasattr(excp, "__class__"): + id_label = excp.__class__.__name__ + + exc_info = sys.exc_info() + + # Inspect frame where exception occurred + innerframe_records = inspect.trace() + outerframe_records = inspect.getouterframes(innerframe_records[0][0]) + outerframe_records.reverse() + frame_records = outerframe_records + innerframe_records[1:] + + try: + # Traverse stack and retrieve locals for frame where exception occurred + locals_dict = cls.traverse_framestack(frame_records, exc_info, copy_locals=True) + dummy_context, trace_id = cls.create_context(info.fullname, info.self_arg, locals_dict, + id_label=id_label, context_type="exceptions", + excp=excp, exc_info=exc_info) + + cls.callback_handler.callback(trace_id, info.methodtype, info.modulename, info.classname, info.fn.__name__) + except Exception: + pass + finally: + del innerframe_records, outerframe_records, frame_records + + try: + del locals_dict + del dummy_context + except Exception: + pass + + return cls.trace_return_value(info, trace_opts, False, return_value) + + @classmethod + def trace_return_value(cls, info, trace_opts, trampoline_return, return_value): + trace_call, trace_return, break_action, match_tag = (False, False, None, False) + + if trace_opts: + ##trace_call = trace_opts.trace_call + trace_return = trace_opts.trace_return + break_action = trace_opts.break_action + match_tag = trace_opts.match_tag + + if match_tag or (not info.trace_context and info.return_match_dict and info.self_arg): + # Check for trace_id/related_id match on instance variables or return value + info.trace_context, info.trace_id, info.related_id = cls.check_trace_match(info.fn, info.fullname, info.self_arg, info.arg_dict, trace_dict=info.return_match_dict, break_action=break_action, on_return=True, return_value=return_value, match_tag=match_tag) + + if info.trace_context: + # Add return value to trace context + info.trace_context.set_trc("return_value", return_value) + + return_display = (info.name_matched or info.trace_context or info.related_id) and (trace_return or (match_tag and not info.call_display)) + if break_action == "tag": + # Tag self object; requires only new "trace_id", not trace_context + if info.trace_id and info.self_arg: + cls.tag(info.self_arg, info.trace_id, **info.arg_dict) + elif return_display: + if not info.call_display: + # Display call name trace retroactively + cls.callback_handler.callback(info.trace_id, info.methodtype, info.modulename, info.classname, info.fn.__name__, info.arg_val_pairs, info.nameless_args_list, retro=True) + + if info.trace_context: + # Update return trace count (only if not already updated during call) + with Trace_rlock: + # Trace matched; increment/decrement trace count + if trace_opts and trace_opts.break_count < 0: + # Trace match; increment trace match count (tracing yet to begin) + trace_opts.break_count += 1 + # Skip current match + info.trace_context, info.trace_id, info.related_id = None, None, None + + elif trace_opts and trace_opts.break_count > 0: + # Trace match; decrement trace match count + trace_opts.break_count -= 1 + if trace_opts.break_count == 0: + # Delete trace entry (tracing completed) + cls.remove_trace(trace_opts.trace_name) + + # Display return name trace + cls.callback_handler.returnback(info.trace_id, info.methodtype, info.modulename, info.classname, info.fn.__name__, return_value) + if break_action in BREAK_ACTIONS: + cls.break_flow(info.trace_id, action=break_action) + + elif break_action == "hold" and trampoline_return and cls.hold_wrapper and info.self_arg is not None: + try: + context_path = [BASE_DIR, RECENT_DIR] + ContextDict.split_trace_id(info.trace_id) + async_handler = cls.hold_wrapper(HoldHandler(info.self_arg, PATH_SEP+PATH_SEP.join(context_path), + resume_value=return_value) ) + setattr(info.self_arg, cls.hold_attr, async_handler) + return async_handler + except Exception: + pass + + return return_value + + @classmethod + def trace_generator(cls, info, trace_opts, gen): + send_value = None + exception_flag = False + while True: + # Invoke generator + try: + if exception_flag: + yield_value = gen.throw(type(send_value), send_value.args) + else: + yield_value = gen.send(send_value) + except StopIteration, excp: + # Last iteration completed ("return value") + ret_value = excp.args[0] if excp.args else None + try: + # Traceback trampoline return value + ret_value = cls.trace_return_value(info, trace_opts, True, ret_value) + except Exception: + pass + raise StopIteration(ret_value) + + except Exception, excp: + try: + # Traceback exception and re-raise it + raise + finally: + id_label = getattr(excp, "__name__", None) + if not id_label and hasattr(excp, "__class__"): + id_label = excp.__class__.__name__ + + exc_info = sys.exc_info() + + # Inspect frame where exception occurred + frame_records = inspect.trace() + + try: + # Traverse stack and retrieve locals for frame where exception occurred + locals_dict = cls.traverse_framestack(frame_records, exc_info, copy_locals=True) + dummy_context, trace_id = cls.create_context(info.fullname, info.self_arg, locals_dict, + id_label=id_label, context_type="exceptions", + excp=excp, exc_info=exc_info) + + cls.callback_handler.callback(trace_id, info.methodtype, info.modulename, info.classname, info.fn.__name__) + except Exception, excp: + pass + finally: + del frame_records + + try: + del locals_dict + del dummy_context + except Exception: + pass + + # Yield value to trampoline + try: + send_value = (yield yield_value) + exception_flag = False + except GeneratorExit, excp: + # Close generator and return + try: + gen.close() + except Exception: + pass + raise StopIteration() + + except Exception, excp: + # Propagate exception to generator in next iteration + send_value = excp + exception_flag = True + + @classmethod + def trace_function(cls, function, classname="", modulename="", methodtype="", unwrap=False): + """Wrap function for tracing. (If unwrap, unwrap function.) + classname is the name of the class for instance/class/static methods. + methodtype = "" (function) or "instancemethod" or "classmethod" or "staticmethod" + """ + if hasattr(function, cls.orig_function_attr): + if unwrap: + # Unwrap function (return original function) + orig_function = getattr(function, cls.orig_function_attr) + delattr(function, cls.orig_function_attr) + return orig_function + else: + # Function already wrapped; do nothing + return function + elif unwrap: + # Function not wrapped; do nothing + return function + + func_info = FunctionInfo(function, classname=classname, modulename=modulename, methodtype=methodtype) + @functools.wraps(function) + def otrace_wrapped(*args, **kwargs): + if not cls.trace_active: + return function(*args, **kwargs) + return cls.otrace_function_call(func_info, *args, **kwargs) + + # Save original function + setattr(otrace_wrapped, cls.orig_function_attr, function) + return otrace_wrapped + + @classmethod + def trace_method(cls, parent_cls, method, modulename="", unwrap=False): + """Trace a method in a class""" + methodtype = get_method_type(parent_cls, method) + + methodname = method.__name__ + if methodname.startswith("__") and not methodname.endswith("__"): + # Unmangle private class name + methodname = "_" + parent_cls.__name__ + methodname + + if methodname not in ("__str__", "__repr__"): + # Non-stringifying method; trace it + new_function = cls.trace_function(get_naked_function(method), classname=parent_cls.__name__, + modulename=modulename, methodtype=methodtype, unwrap=unwrap) + if methodtype == "classmethod": + new_function = classmethod(new_function) + elif methodtype == "staticmethod": + new_function = staticmethod(new_function) + + setattr(parent_cls, methodname, new_function) + + return getattr(parent_cls, methodname) + + @classmethod + def trace_modulefunction(cls, mod, function, unwrap=False): + """Trace function in module""" + setattr(mod, function.__name__, cls.trace_function(function, modulename=mod.__name__, unwrap=unwrap)) + + @classmethod + def trace_entity(cls, entity, exclude=[], include=[], modulename="", unwrap=False): + """ Trace an entity (which may be a module or a class) + All members are traced recursively, except for members in the the exclude list, + unless include list is specified, in which case only included members are traced. + """ + if inspect.isclass(entity): + # Trace only methods in class + if unwrap: + if hasattr(entity, cls.class_trace_attr): + delattr(entity, cls.class_trace_attr) + else: + setattr(entity, cls.class_trace_attr, True) + + members = inspect.getmembers(entity, ismethod_or_function) + + # Only trace immediate class methods (not parent class methods) + members = [(name, member) for name, member in members if name in entity.__dict__] + + elif inspect.ismodule(entity): + # Trace functions and classes in module + modulename = entity.__name__ + members = inspect.getmembers(entity, inspect.isfunction) + members += inspect.getmembers(entity, inspect.isclass) + + if include: + trace_set = set(include) + else: + trace_set = set(x[0] for x in members) + for name in exclude: + trace_set.discard(name) + + trace_list = [] + for name, member in members: + if name not in trace_set: + continue + trace_list.append(name) + if inspect.isclass(entity): + # Trace method in class + cls.trace_method(entity, member, modulename=modulename, unwrap=unwrap) + elif inspect.isfunction(member): + # Trace function in module + cls.trace_modulefunction(entity, member, unwrap=unwrap) + else: + # Trace class in module + cls.trace_entity(member, modulename=modulename, unwrap=unwrap) + + return trace_list + + @classmethod + def web_hook(cls, op_type, path, data): + # Must be thread-safe (OK if output only) + if not OShell.instance: + return + oshell = OShell.instance + try: + if op_type == "stderr": + if oshell.repeat_interval: + oshell.set_repeat(None) + oshell.std_output(data+"\n") + + if op_type == "stdout": + if oshell.repeat_interval: + data = CLEAR_SCREEN_SEQUENCE + data + if oshell.repeat_alt_screen == 1: + oshell.repeat_alt_screen = 2 + data = ALT_SCREEN_ONSEQ + data + else: + if oshell.repeat_alt_screen == 2: + oshell.repeat_alt_screen = 0 + data = ALT_SCREEN_OFFSEQ + data + data = data+"\n"+oshell.prompt1 + oshell.std_output(data, flush=True) + except Exception: + pass + + @classmethod + def access_hook(cls, op_type, entity_key, entity_cache): + if not cls.trace_keys: + return + key_str = str(entity_key) + trace_opts = cls.trace_keys.get(key_str) + if not trace_opts: + return + + name_str = urllib.unquote(key_str).replace(PATH_SEP, "_") # ABC Temporary fix for unquoting keys + trace_matched = False + entity = None + + if trace_opts.access_type: + if op_type == "get": + if trace_opts.access_type not in ("get", "all"): + return + elif op_type == "put": + if trace_opts.access_type not in ("put", "modify", "all"): + return + elif op_type == "delete": + if trace_opts.access_type not in ("delete", "modify", "all"): + return + trace_matched = True + + elif trace_opts.argmatch: + if not entity_cache: + return + entity = entity_cache.unpack() + for attr, value in trace_opts.argmatch.iteritems(): + attr, cmp_op = strip_compare_op(attr) + if not compare(getattr(entity, attr, None), cmp_op, value): + # Match failed; no trace + return + trace_matched = True + + if not trace_matched: + return + + if trace_opts.break_count < 0: + # Trace match; increment trace match count (tracing yet to begin) + trace_opts.break_count += 1 + # Skip current match + return + + elif trace_opts.break_count > 0: + # Trace match; decrement trace match count + trace_opts.break_count -= 1 + if trace_opts.break_count == 0: + # Delete trace entry (tracing completed) + cls.remove_trace(trace_opts.trace_name) + return + + if not entity and entity_cache: + entity = entity_cache.unpack() + + locals_dict = TraceDict({ENTITY_CHAR: entity}) + locals_dict.set_trc("stack", "".join(traceback.format_stack()[:-3]) ) + trace_context, trace_id = cls.create_context(name_str, None, locals_dict, + context_type="dbaccess", id_label=op_type) + + cls.callback_handler.accessback(trace_id, op_type, key_str, entity) + if trace_opts.break_action in BREAK_ACTIONS: + cls.break_flow(trace_id, action=trace_opts.break_action) + + + @classmethod + def getsourcelines(cls, method): + """ Returns (list_of_source_lines, start_line) + start_line is 0 if source is obtained from patch attribute, rather than the source file. + If no source is found, IOError is raised. + """ + orig_fn = getattr(method, cls.orig_function_attr, None) or method + patch_source_list = getattr(orig_fn, cls.patch_source_attr, None) + if patch_source_list: + return patch_source_list, 0 + else: + return inspect.getsourcelines(orig_fn) + + @classmethod + def monkey_patch(cls, new_func, method, parent, methodtype="", repatch=False, source=None): + """ Override a function or a class/instance method with a new function, + returning new_func on success and None on failure. + parent must be a class or a module that contains the method/function. + The original unpatched method is saved as an attribute of the patched method, + and can be used by the patched method to invoke the unpatched method, e.g. + def method(self, *args, **kwargs): + unpatched = getattr(self.method, "_otrace_unpatched_function") + # Modify args/kwargs + ret_value = unpatched(self, *args, **kwargs) + # Modify ret_value + return ret_value + For new methods, specify + methodtype = "" (function) or "instancemethod" or "classmethod" or "staticmethod" + If repatch, force overwriting of previous patch, if present. + If source (list of strings) is specified, it is also saved as an attribute. + """ + assert not parent or inspect.isclass(parent) or inspect.ismodule(parent) + with Trace_rlock: + if not parent: + parent = getattr(method, cls.patch_parent_attr, None) + if not method: + # Add new method + wrapped = False + mname = new_func.__name__ + orig_method = None + else: + # Patch current method + methodtype = "" + mname = method.__name__ + if inspect.isclass(parent): + methodtype = get_method_type(parent, method) + if mname.startswith("__") and not mname.endswith("__"): + # Private class name; unmangle + mname = "_%s%s" % (parent.__name__, mname) + + wrapped = hasattr(method, cls.orig_function_attr) + if wrapped: + if inspect.isclass(parent): + method = cls.trace_method(parent, method, unwrap=True) + else: + setattr(parent, mname, cls.trace_function(method, methodtype=methodtype, unwrap=True)) + + orig_method = getattr(method, OTrace.unpatched_function_attr, None) + + if not orig_method or repatch: + # Save original (or current) method (which may be None) as attribute of new method + setattr(new_func, cls.unpatched_function_attr, orig_method or method) + + # Save patch source as attribute of new method + setattr(new_func, cls.patch_source_attr, source) + + # Save parent as attribute of new method + setattr(new_func, cls.patch_parent_attr, parent) + + # Attributes of new_func can be accessed after decoration, + # but cannot be deleted + if methodtype == "classmethod": + new_func = classmethod(new_func) + elif methodtype == "staticmethod": + new_func = staticmethod(new_func) + + setattr(parent, mname, new_func) + + if wrapped: + if inspect.isclass(parent): + cls.trace_method(parent, getattr(parent, mname), unwrap=False) + else: + setattr(parent, mname, cls.trace_function(getattr(parent, mname), methodtype=methodtype, unwrap=False)) + + return new_func if not orig_method or repatch else None + + @classmethod + def monkey_unpatch(cls, method): + """ Revert to original value for overridden function or method, returning True on success + """ + with Trace_rlock: + parent = getattr(method, cls.patch_parent_attr, None) + if not parent: + return None + methodtype = "" + mname = method.__name__ + if inspect.isclass(parent): + methodtype = get_method_type(parent, method) + if mname.startswith("__") and not mname.endswith("__"): + # Private class name; unmangle + mname = "_%s%s" % (parent.__name__, mname) + + wrapped = hasattr(method, cls.orig_function_attr) + if wrapped: + if inspect.isclass(parent): + method = cls.trace_method(parent, method, unwrap=True) + else: + setattr(parent, mname, cls.trace_function(method, methodtype=methodtype, unwrap=True)) + + unpatch = hasattr(method, cls.unpatched_function_attr) + if unpatch: + orig_method = getattr(method, cls.unpatched_function_attr) + if orig_method: + setattr(parent, mname, orig_method) + else: + delattr(parent, mname) + + # Remove original method and patch source attributes (no need for this?) + func = get_naked_function(method) + delattr(func, cls.unpatched_function_attr) + delattr(func, cls.patch_source_attr) + delattr(func, cls.patch_parent_attr) + + if wrapped: + if inspect.isclass(parent): + cls.trace_method(parent, getattr(parent, mname), unwrap=False) + else: + setattr(parent, mname, cls.trace_function(getattr(parent, mname), methodtype=methodtype, unwrap=False)) + + return unpatch + +def schedule_callback(callback=None): + if not callback: + return + if OTrace.eventloop_callback: + # Schedule callback in event loop to ensure thread safety + OTrace.eventloop_callback(callback) + else: + # Direct callback; may be unsafe! + callback() + +class HoldHandler(object): + """ Hold handler for use with otrace (callable instance) + """ + def __init__(self, self_arg, path, resume_value=None): + self.self_arg = self_arg + self.path = path + self.resume_value = resume_value + self.callback = None + + def __call__(self, callback=None): + # Save callback for use on request completion + self.callback = callback + if hasattr(self.self_arg, OTrace.hold_attr): + delattr(self.self_arg, OTrace.hold_attr) + setattr(self.self_arg, OTrace.resume_attr, self.resume) + + def resume(self): + # Executed in otrace thread; should insert resume callback in event loop and return immediately + if hasattr(self.self_arg, OTrace.resume_attr): + delattr(self.self_arg, OTrace.resume_attr) + callback = self.callback + self.callback = None + if not callback: + return + schedule_callback(functools.partial(callback, self.resume_value)) + +# otrace pickle_interface +class PickleInterface(object): + root_depth = 4 # Depth of key path tree dict that is automatically updated + # Below this depth, the key path tree is updated on demand, + # one branch at a time + + write_connection = None + read_connection = None + write_file = "" + read_file = "" + + @classmethod + def set_monitor(cls, monitor): + pass + + @classmethod + def set_access_hook(cls, hook): + pass + + @classmethod + def key_from_path(cls, path_list): + """ Construct key from list of path components + """ + if not path_list: + return None + + if len(path_list) < 2: + path_list = path_list + ["*"] + + if len(path_list) < 3: + for ctype, context_type in ContextDict.context_types.items(): + if path_list[0] == context_type: + return TRACE_ID_SEP.join([path_list[1], ctype+"-*", "*"]) + return None + + if len(path_list) < 4: + path_list = path_list + ["*"] + + # Primary key (trace_id) = class.method:context_id:timestamp + return TRACE_ID_SEP.join(path_list[1:4]) + + @classmethod + def path_from_key(cls, key): + """ Convert key to path component list + """ + return ContextDict.split_trace_id(key) + + @classmethod + def get_entity(cls, entity_key): + """Read and return trace context from db""" + key = cls.key_from_path([""] + entity_key.replace(PATH_SEP, TRACE_ID_SEP).split(TRACE_ID_SEP)) + contexts = cls.read_records_pickle_db(key=key) + return contexts[0] if contexts else None + + @classmethod + def delete_entities(cls, key_list, recursive=False): + """ Delete directory tree entries for key_list (not the actual pickled entities)""" + deleted = [] + for key in key_list: + path = cls.path_from_key(key) + context = OTrace.base_context[PICKLED_DIR] + parents = [] + while path: + cdir = path.pop(0) + if cdir == "*": + break + elif cdir in context: + parents.append([context, cdir]) + context = context[cdir] + else: + context = None + break + + if not context: + continue + context.clear() + deleted.append(key) + while parents: + context, cdir = parents.pop() + if not context.get(cdir): + context.pop(cdir, None) + return deleted + + @classmethod + def get_root_tree(cls): + """ Returns dict tree (with root_depth) that is updated automatically + """ + return OTrace.base_context[PICKLED_DIR] + + @classmethod + def get_child_tree(cls, ancestor_key, entity_char=":"): + """ Returns dict tree branch below the root tree + """ + path = cls.path_from_key(ancestor_key) + context = {entity_char: cls.get_entity(ancestor_key)} + while path: + key = path.pop() + context = {key: context} + return context + + @classmethod + def create_pickle_db(cls, filename): + """Create pickle database file for writing + (accessed only from application, not otrace) + """ + try: + cls.write_connection = sqlite3.connect(filename, check_same_thread=False) + cls.write_file = filename + cls.pickle_names = ["otrace_context", "recnum", "key", "methodname", "context_id", "timestamp", "pickled_object"] + + cls.pickle_create_sql = "CREATE TABLE IF NOT EXISTS %s (%s INTEGER PRIMARY KEY AUTOINCREMENT, %s TEXT UNIQUE, %s TEXT, %s TEXT, %s TEXT, %s TEXT)" % tuple(cls.pickle_names[:]) + cls.pickle_insert_sql = "INSERT INTO %s VALUES (null, :%s, :%s, :%s, :%s, :%s)" % tuple(cls.pickle_names[:1] + cls.pickle_names[2:]) + + cls.pickle_select_key_sql = "SELECT %s FROM %s" % (cls.pickle_names[2], cls.pickle_names[0]) + cls.pickle_select_record_sql = "SELECT * FROM %s" % (cls.pickle_names[0],) + cls.write_connection.execute(cls.pickle_create_sql) + cls.write_connection.commit() + except sqlite3.OperationalError, msg: + logging.error("Error in creating pickle database %s: %s" % (cls.write_file, msg)) + raise + + @classmethod + def pickle_check(cls, obj, depth=0): + """Return copy of object to be pickled, replacing non-pickleable components with string + representations + """ + # PRELIMINARY IMPLEMENTATION + # (Until we develop "failsafe" pickling, this will only pickle pickleable objects + # upto certain depth and stringify the rest, e.g., files, StringIO etc.) + if depth < MAX_PICKLE_CHECK_DEPTH: + if isinstance(obj, (list, tuple)): + new_obj = [cls.pickle_check(x, depth=depth+1) for x in obj] + return new_obj if isinstance(obj, list) else tuple(new_obj) + + if isinstance(obj, dict): + return dict((key, cls.pickle_check(value, depth=depth+1)) for key, value in obj.iteritems()) + + try: + # Check if object is pickleable (and not too large when pickled) + pickled = cPickle.dumps(obj) + if len(pickled) <= MAX_PICKLE_DATA_LENGTH: + return obj + except Exception: + pass + + # Object not pickleable; stringify it (truncating, if need be) + s = str(obj) + return s if len(s) <= MAX_PICKLE_DATA_LENGTH else s[:MAX_PICKLE_DATA_LENGTH]+"..." + + @classmethod + def write_pickle_db(cls, trace_id, obj): + """Write pickled context object corresponding to trace_id + """ + context_type, methodname, context_id, timestamp = ContextDict.split_trace_id(trace_id) + try: + pickled = cPickle.dumps(cls.pickle_check(obj), cPickle.HIGHEST_PROTOCOL) + with Pickle_rlock: + cursor = cls.write_connection.execute(cls.pickle_insert_sql, + {"key": trace_id, "timestamp": timestamp, "methodname": methodname, + "context_id": context_id, "pickled_object": sqlite3.Binary(pickled)}) + cls.write_connection.commit() + except Exception, excp: + logging.error("Error in adding entry to pickle_db %s: %s", cls.write_file, excp) + + @classmethod + def open_pickle_db(cls, filename): + """Open pickle database file for reading + (accessed only from otrace thread) + """ + try: + cls.read_file = filename + if filename == cls.write_file: + cls.read_connection = cls.write_connection + else: + if cls.read_connection and cls.read_connection is not cls.write_connection: + cls.read_connection.close() + cls.read_connection = None + cls.read_connection = sqlite3.connect(filename) + except sqlite3.OperationalError, msg: + logging.error("Error in reading pickle database %s: %s" % (cls.read_file, msg)) + raise + + @classmethod + def read_keys_pickle_db(cls, **kwargs): + """Read keys as filtered by kwargs + (key=..., methodname=..., context_id=...) + and return list of matching keys + (If no filters, all keys are returned) + """ + try: + with Pickle_rlock: + select_sql = cls.pickle_select_key_sql + select_vals = [] + for arg_name, arg_value in kwargs.items(): + select_sql += " WHERE %s=?" % arg_name + select_vals.append(arg_value) + cursor = cls.read_connection.execute(select_sql, select_vals) + return [str(x[0]) for x in cursor.fetchall()] + except Exception, excp: + logging.error("Error in retrieving key(s) from pickle_db: %s", kwargs, excp) + + @classmethod + def read_records_pickle_db(cls, **kwargs): + """Read contexts as filtered by kwargs + (key=..., methodname=..., context_id=...) + and return list of unpickled context objects + (If no filters, all records are returned) + """ + try: + with Pickle_rlock: + select_sql = cls.pickle_select_record_sql + select_vals = [] + for arg_name, arg_value in kwargs.items(): + select_sql += " WHERE %s=?" % arg_name + select_vals.append(arg_value) + cursor = cls.read_connection.execute(select_sql, select_vals) + rows = cursor.fetchall() + return [cPickle.loads(str(row[-1])) for row in rows] + except Exception, excp: + logging.error("Error in retrieving record(s) from pickle_db: %s", kwargs, excp) + + +# Convenient aliases +traceassert = OTrace.traceassert +tag = OTrace.tag +untag = OTrace.untag +get_tag = OTrace.get_tag +set_tag = OTrace.set_tag + +if __name__ == "__main__": + # Test OTrace + class TestClass(object): + def method(self, arg1, kwarg1=None): + print "Invoked method with arg1="+str(arg1)+", kwarg1="+str(kwarg1) + + def method2(self, arg1, kwarg1=None): + print "Invoked method2 with arg1="+str(arg1)+", kwarg1="+str(kwarg1) + return arg1 + + @classmethod + def cmethod(cls, arg1): + print "Invoked cmethod with arg1="+str(arg1) + return (1, 2) + + @staticmethod + def smethod(self, arg1): + print "Invoked smethod with arg1="+str(arg1) + return False + + # Method for patching + def method2(self, *args, **kwargs): + unpatched = getattr(self.method2, "_otrace_unpatched_function") + args = tuple([args[0] + "-MODIFIED"] + list(args[1:])) + ret_value = unpatched(self, *args, **kwargs) + return ret_value + "-RETMODIFIED" + + glob_dict = {"gvar": "gvalue"} + loc_dict = {"arg1": "value1"} + + trace_int = TraceInterpreter() + print trace_int.evaluate("arg1", print_out=True, locals_dict=loc_dict) + print trace_int.evaluate("arg1 = 'new_value'", locals_dict=loc_dict, globals_dict=glob_dict) + print trace_int.evaluate("arg1", print_out=True, locals_dict=loc_dict) + + print trace_int.evaluate("arg1.", print_out=True, locals_dict=loc_dict) + print trace_int.evaluate("arg1 = undefined_value", locals_dict=loc_dict) + + print trace_int.evaluate("gvar", print_out=True, globals_dict=glob_dict) + print trace_int.evaluate("global gvar; gvar = 'new_glob_value ' + arg1", locals_dict=loc_dict, globals_dict=glob_dict) + print trace_int.evaluate("gvar", print_out=True, locals_dict=loc_dict, globals_dict=glob_dict) + print glob_dict["gvar"] + + print trace_int.evaluate("gvar = 'new_local_value2'", locals_dict=loc_dict, globals_dict=glob_dict) + print trace_int.evaluate("gvar", print_out=True, locals_dict=loc_dict, globals_dict=glob_dict) + print glob_dict["gvar"] + print loc_dict["gvar"] + + OTrace.trace_entity(TestClass) + + OTrace.add_trace() + + testobj = TestClass() + + testobj.method(11, kwarg1="KWRD") + testobj.cmethod(True) + testobj.smethod("ss", 33) + + OTrace.disable_trace() + + OTrace.add_trace(".method", argmatch={"arg1":33}) + OTrace.add_trace(".method2", argmatch={"return":44}) + + testobj.method(11, kwarg1="KWRD") + testobj.method(33, kwarg1="KWRD") + + testobj.method2(22, kwarg1="KWRD") + testobj.method2(44, kwarg1="KWRD") + + testobj.cmethod(True) + testobj.smethod("ss", 33) + + print testobj.method2("55orig", kwarg1="KWRD") + OTrace.monkey_patch(method2, TestClass.method2, TestClass) + print testobj.method2("55patch", kwarg1="KWRD") + OTrace.monkey_unpatch(TestClass.method2) + print testobj.method2("55unpatch", kwarg1="KWRD") + diff --git a/graphterm/packetserver.py b/graphterm/packetserver.py new file mode 100755 index 0000000..8bbfb90 --- /dev/null +++ b/graphterm/packetserver.py @@ -0,0 +1,923 @@ +#!/usr/bin/env python +# + +""" +Packet server: for flash etc. +""" + +import hashlib +import time + +import errno +import functools +import hmac +import hashlib +import logging +import socket +import ssl +import struct +import thread +import traceback +import uuid + +try: + import json +except ImportError: + import simplejson as json + +from tornado import ioloop +from tornado import iostream +from tornado import escape + +SIGN_SEP = "|" +SIGN_HEXDIGITS = 24 +SIGN_HASH = hashlib.sha256 + +FLASH_POLICY_PORT = 843 +FLASH_SOCKET_PORT = 8843 + +FRAMELEN_FORMAT = "!L" # Format for frame length prefix +FLASH_DELIMITER = "\0" + +POLICY_FILE_REQUEST = "" +MASTER_POLICY_XML_FORMAT = """ + + + +""" +SOCKET_POLICY_XML_FORMAT = """ + + +""" + +def dict2kwargs(dct, unicode2str=False): + """Converts unicode keys in a dict to ascii, to allow it to be used for keyword args. + If unicode2str, all unicode values to converted to str as well. + (This is needed when the dict is created from JSON) + """ + return dict([(str(k), str(v) if unicode2str and isinstance(v, unicode) else v) for k, v in dct.iteritems()]) + +class UserMessage(Exception): + """ Exception for sending error messages to client + """ + pass + +class SystemMessage(Exception): + """ Exception for sending error messages to system administrator + """ + pass + +class PacketConnector(object): + """ Serves Flash policy and other requests, delimited by "\0" or framed + by a frame length prefix (typically using "!L" unsigned 32-bit format) + Override process request method. + If max_packet_buf > 0, derived class must explicitly call resend_buffered_packets + and clear_sent_packets as needed. Can also use self.packet_id to identify packets. + If max_packet_buf < 0, connection is shutdown if buffer is full. + To ensure unique connection for each connection_id, call new_connection as soon as + a new connection is created. (Any previous connection with the same id is closed). + """ + _all_connections = None # Must be re-defined to {} in final derived class + def __init__(self, client=False, server_type="", delimiter=None, + framelen_format=None, single_request=False, ssl_options={}, max_packet_buf=0): + if not delimiter and not framelen_format: + if server_type == "flash": + delimiter = FLASH_DELIMITER + elif server_type == "frame": + framelen_format = FRAMELEN_FORMAT + else: + raise Exception("Must specify delimiter or framelen_format") + elif delimiter and framelen_format: + raise Exception("Must specify only one of delimiter or framelen_format") + + self.client = client + self.server_type = server_type + self.delimiter = delimiter + self.framelen_format = framelen_format + self.fmt_size = struct.calcsize(framelen_format) if framelen_format else 0 + self.single_request = single_request + self.ssl_options = ssl_options + + self.expect_len = 0 + self.closed = False + self.stream = None + + self.max_packet_buf = max_packet_buf + self.packet_id = 1 # ID of next packet to be sent (wraps around) + self.packet_buf = [] + self.connection_id = "" # Usually set first on client and then sent to server + self.last_active_time = 0 # Time when data was last received or sent + + def new_connection(self, connection_id): + if self._all_connections is None: + raise Exception("Must define class attribute %s._all_connections = {}" % self.__class__.__name__) + if self.connection_id: + if self.connection_id == connection_id: + return + raise Exception("Connection id mismatch: old %s != new %s" % (self.connection_id, connection_id)) + + self.connection_id = connection_id + if connection_id: + old_connection = self._all_connections.get(connection_id) + self._all_connections[connection_id] = self + if old_connection: + # Shutdown any previous connection for this ID + old_connection.shutdown() + + def shutdown(self): + # Does not self.close (done by derived class) + if self.connection_id: + if self._all_connections.get(self.connection_id) is self: + del self._all_connections[self.connection_id] + self.connection_id = "" + + def on_close(self): + raise Exception("NOT IMPLEMENTED") + + @classmethod + def shutdown_all(cls): + while cls._all_connections: + conn_id, conn = cls._all_connections.popitem() + conn.shutdown() + + @classmethod + def get_connection(cls, connection_id): + return cls._all_connections.get(connection_id) + + @classmethod + def get_connection_ids(cls): + return cls._all_connections.keys() + + def process_packet(self, message): + """ Processes packet; raises exception on error, leading to connection being closed. + If single_request, will be called with None value if server closes connection. + Otherwise, message is always non-None. + OVERRIDE + """ + raise SystemMessage("NOT IMPLEMENTED") + + def receive_header(self, data=None): + """ Receives frame length header + """ + if not self.stream or not data or len(data) != self.fmt_size: + self.on_close() + return + self.expect_len = struct.unpack(self.framelen_format, data)[0] + self.stream.read_bytes(self.expect_len, self.receive_packet) + + def receive_packet(self, data=None, start=False): + """ Receives stream request, processes it, and waits for next one + """ + if not data and not start: + self.on_close() + return + + self.last_active_time = time.time() + if data: + if self.delimiter: + # Strip out delimiter + data = data[:-len(self.delimiter)] + elif len(data) != self.expect_len: + # Incomplete frame; close + self.on_close() + return + + try: + self.process_packet(data) + except Exception, excp: + logging.warning("PacketConnector: Error in processing packet: %s", excp, exc_info=True) + self.on_close() + return + + if not self.stream: + self.on_close() + return + + if not start and self.single_request: + if self.client: + # Processed response to single request + self.shutdown() + # If server, it will be shutdown after response is sent + else: + # Process first (or next packet) + if self.fmt_size: + self.stream.read_bytes(self.fmt_size, self.receive_header) + else: + self.stream.read_until(self.delimiter, self.receive_packet) + + def send_json(self, obj, finish=False, buffer=False, nobuffer=False): + if not self.closed: + self.send_packet(json.dumps(obj), finish=finish, utf8=True, buffer=buffer, nobuffer=nobuffer) + + def make_packet(self, data, utf8=False): + if utf8: + data = escape.utf8(data) + + if self.framelen_format: + packet = struct.pack(self.framelen_format, len(data)) + data + else: + if self.delimiter in data: + raise Exception("Delimiter not allowed in message") + packet = data + self.delimiter + return packet + + def clear_sent_packets(self, packet_id): + while self.packet_buf and self.packet_buf[0][0] <= packet_id: + self.packet_buf.pop(0) + + def resend_buffered_packets(self): + for packet_id, data, finish, utf8 in self.packet_buf: + self.send_packet(data, finish=finish, utf8=utf8, nobuffer=True) + + def is_connected(self): + return self.connected + + def is_writable(self): + return self.connected or len(self.packet_buf) < abs(self.max_packet_buf) + + def send_packet(self, data, finish=False, utf8=False, buffer=False, nobuffer=False): + """ + If buffer, packet is not actually sent, just buffered. + If nobuffer, self.packet_id is not incremented. + """ + if self.closed: + return + + if thread.get_ident() != ioloop.IOLoop.instance()._thread_ident: + raise Exception("PacketConnector.send_packet invoked from non-ioloop thread") + + self.last_active_time = time.time() + if not nobuffer: + if self.max_packet_buf: + while len(self.packet_buf) >= abs(self.max_packet_buf): + if self.max_packet_buf < 0: + # Buffer overflow; close + self.on_close() + return + self.packet_buf.pop(0) + self.packet_buf.append( (self.packet_id, data, finish, utf8) ) + self.packet_id = (self.packet_id + 1) % 0x40000000 + + if finish or self.single_request: + callback = self.shutdown + else: + callback = None + + if self.stream and not buffer: + try: + self.stream.write(self.make_packet(data, utf8=utf8), callback) + except Exception, excp: + logging.warning("PacketConnector.send_packet: ERROR: %s", excp) + self.on_close() + +class PacketClient(PacketConnector): + """ Sends one or more packets to server and handles response packets. + If single_request, send a single request and wait for a single response, + otherwise, call connect to initiate connection. + If reconnect_sec, keep reconnecting (after waiting reconnect_sec) until explicit shutdown. + If reconnect_timeout, stop trying to reconnect after reconnect_timeout secs. + """ + def __init__(self, host, port, io_loop=None, noresponse=False, server_type="", + delimiter=None, framelen_format=None, single_request=False, + ssl_options={}, max_packet_buf=0, reconnect_sec=0, reconnect_timeout=0): + super(PacketClient, self).__init__(client=True, server_type=server_type, + delimiter=delimiter, + framelen_format=framelen_format, + ssl_options=ssl_options, + max_packet_buf=max_packet_buf, + single_request=single_request) + self.host = host + self.port = port + if not io_loop: + io_loop = ioloop.IOLoop.instance() + self.io_loop = io_loop + self.noresponse = noresponse + self.reconnect_sec = reconnect_sec + self.reconnect_timeout = reconnect_timeout + + self.request_packet = None + self.timeout_cb = None + self.reconnect_cb = None + self.reconnect_time = 0 # Time when reconnection was initiated + self.connected = False # True if connection is currently active + self.client_opened = False # True after first succesful connect + + def reset(self): + if self.timeout_cb: + try: + self.io_loop.remove_timeout(self.timeout_cb) + except Exception, excp: + pass + self.timeout_cb = None + self.connected = False + self.request_packet = None + + @classmethod + def get_client(cls, connection_id, connect=(), connect_kw={}): + """ Return client connection for connection id. + If connect = (host, port), a connection is initiated, if not found. + """ + conn = super(PacketClient, cls).get_connection(connection_id) + if not conn and connect: + # Create new connection to server and connect + conn = cls(*connect, **connect_kw) + conn.new_connection(connection_id) + conn.connect() + return conn + + def connect(self, timeout=30): + self.clear_reconnect() + if self.closed: + return + self.reset() + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) + if self.ssl_options: + self.stream = iostream.SSLIOStream(sock, io_loop=self.io_loop, ssl_options=self.ssl_options) + else: + self.stream = iostream.IOStream(sock, io_loop=self.io_loop) + self.stream.set_close_callback(self.on_close) + if timeout > 0: + self.timeout_cb = self.io_loop.add_timeout(time.time()+timeout, self.connect_timeout) + self.stream.connect((self.host, self.port), self.on_connect) + + def on_connect(self): + if self.closed: + return + self.reset() + self.connected = True + self.client_opened = True + self.reconnect_time = 0 # Reconnect succeeded + self.handle_connect() + + if self.single_request: + assert self.request_packet + self.stream.write(self.request_packet, callback=self.single_request_sent) + self.request_packet = None + else: + self.receive_packet(start=True) + + def connect_timeout(self): + self.timeout_cb = None + if self.connected or self.closed: + return + if self.reconnect_sec > 0: + self.on_close() + else: + self.shutdown() + + def reconnect_callback(self): + self.reconnect_cb = None + if self.connected or self.closed: + return + self.connect() + + def handle_connect(self): + """ Called after connecting (or reconnecting) to server + Override + """ + pass + + def clear_reconnect(self): + if not self.reconnect_cb: + return + try: + self.io_loop.remove_timeout(self.reconnect_cb) + except Exception, excp: + pass + self.reconnect_cb = None + + def shutdown(self): + if self.closed: + return + self.closed = True + + self.clear_reconnect() + self.shutdown_aux() + + if not self.noresponse and self.single_request and self.request_packet: + # Simulate None response if single request has been sent + self.request_packet = None + try: + self.process_packet(None) + except Exception: + pass + + super(PacketClient, self).shutdown() + self.handle_shutdown() + + def shutdown_aux(self): + self.reset() + if self.stream: + try: + self.stream.close() + except Exception: + pass + self.stream = None + + def handle_shutdown(self): + """ Called when connection is shutdown (useful when reconnecting is enabled) + Override + """ + pass + + def on_close(self): + if self.closed: + return + self.handle_close() + cur_time = time.time() + if self.reconnect_sec <= 0 or ( + self.reconnect_timeout and self.reconnect_time and (cur_time - self.reconnect_time) > self.reconnect_timeout) or (self._all_connections.get(self.connection_id) is not self): + # No reconnect or reconnect timed out or old connection + self.shutdown() + else: + # Reconnect + self.shutdown_aux() + if not self.reconnect_cb: + if not self.reconnect_time: + self.reconnect_time = cur_time + self.reconnect_cb = self.io_loop.add_timeout(cur_time+self.reconnect_sec, self.reconnect_callback) + + def handle_close(self): + """ Called when server closes connection (multiple calls, if reconnecting is enabled) + Override + """ + pass + + def send_single_request(self, message, utf8=False): + assert self.single_request + self.request_packet = self.make_packet(message, utf8=utf8) + self.connect() + + def single_request_sent(self): + if self.noresponse: + self.shutdown() + return + self.receive_packet(start=True) + + +class PacketConnection(PacketConnector): + """ Serves Flash policy and other requests, delimited by "\0" or framed + by a frame length prefix (typically using "!L" unsigned 32-bit format) + Override process request method. + """ + def __init__(self, stream, address, server_address, server_type="", + delimiter=None, framelen_format=None, ssl_options={}, + max_packet_buf=0, single_request=False): + super(PacketConnection, self).__init__(client=False, server_type=server_type, + delimiter=delimiter, + framelen_format=framelen_format, + ssl_options=ssl_options, + max_packet_buf=max_packet_buf, + single_request=single_request) + + self.server_address = server_address + self.address = address + self.source ="%s:%s" % address + if server_type: + self.source = server_type + ":" + self.source + + self.connected = True # Server-side socket is connected from start + self.stream = stream + self.stream.set_close_callback(self.on_close) + + def shutdown(self): + self.on_close() + try: + self.stream.close() + except Exception: + pass + self.stream = None + super(PacketConnection, self).shutdown() + + def on_close(self): + if self.closed: + return + self.closed = True + self.handle_close() + self.shutdown() + + def handle_close(self): + """ Called when connection is closed + Override + """ + pass + + def map_name_to_host(self, message): + """ Maps group name to host:port, returning id@host:port/path, where path may be a null string + """ + raise SystemMessage("NOT IMPLEMENTED") + + @classmethod + def connection_ready(cls, server_address, io_loop, sock, fd, events, **kwargs): + ssl_options = kwargs.get("ssl_options", {}) + while True: + try: + connection, address = sock.accept() + except socket.error, e: + if e.args[0] not in (errno.EWOULDBLOCK, errno.EAGAIN): + raise + return + connection.setblocking(0) + logging.debug("Policy connection: %s", address) + if ssl_options: + stream_class = iostream.SSLIOStream + else: + stream_class = iostream.IOStream + stream = stream_class(connection, io_loop=io_loop) + handler = cls(stream, address, server_address, **kwargs) + handler.receive_packet(start=True) + + @classmethod + def start_tcp_server(cls, host, port, io_loop=None, **kwargs): + """ Listens for TCP requests. + If io_loop is not specified, a new io_loop is created and started (blocking) + Returns socket + """ + ssl_options = kwargs.get("ssl_options", {}) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.setblocking(0) + server_address = (host, port) + sock.bind(server_address) + if ssl_options: + sock = ssl.wrap_socket(sock, server_side=True, **ssl_options) + sock.listen(128) + + start_loop = False + if not io_loop: + start_loop = True + io_loop = ioloop.IOLoop.instance() + + callback = functools.partial(cls.connection_ready, server_address, io_loop, sock, **kwargs) + io_loop.add_handler(sock.fileno(), callback, io_loop.READ) + + logging.warning("%s: listening on %s:%d", cls.__name__, host, port) + if start_loop: + io_loop.start() + + return sock + + @classmethod + def stop_tcp_server(cls, sock, io_loop=None): + if io_loop: + io_loop.remove_handler(sock.fileno()) + sock.close() + +class RPCLink(object): + """ Implements Remote Procedure Calls, using messages of the form + [0, ["setup", ["connection_id", key_version, nonce, server_token or None] ] for setup + [0, ["validate", [server/client_rpc_token, last_received_id], {state}] ] for validation + [0, ["shutdown", [err_message] ] + [n, ["method", [args], {kwargs}] + [-n, retval], where -n acknowledges packet n, and non-null retval implies error message. + Mixin *before* PacketConnection/PacketClient, and implement derived methods of the form + remote_(self, args, kwargs) + """ + rpc_key_secret = None # Set for server/client + rpc_key_version = None # Set for server/client + def __init__(self, *args, **kwargs): + """ Arguments: + connection_id (str) required for clients. + key_secret (str) optional + key_version (int) optional + + """ + self.connection_id = kwargs.pop("connection_id", "") + if "key_secret" in kwargs: + self.rpc_key_secret = kwargs.pop("key_secret") + if "key_version" in kwargs: + self.rpc_key_version = kwargs.pop("key_version") + self.rpc_expect = "connect" + + self.rpc_ready = False + self.rpc_nonce = None + self.rpc_unvalidated_id = None + self.rpc_client_token = None + self.rpc_state = {} # (Optional) Initial state variable for connection + self.rpc_peer_state = {} # Initial state variable for peer + self.received_id = 0 # Last received packet ID + super(RPCLink, self).__init__(*args, **kwargs) + + def set_rpc_state(self, value={}): + self.rpc_state = value + + def on_connect(self): + super(RPCLink, self).on_connect() + if not self.connection_id: + self.shutdown() + raise Exception("Must specify connection_id for RPC connection setup") + self.rpc_nonce = uuid.uuid4().hex + self.rpc_expect = "connect" + self.send_json([0, ["connect", [self.connection_id, self.rpc_key_version, self.rpc_nonce, None]] ], + nobuffer=True) + + def rpc_connect(self, connection_id, key_version, nonce, token): + self.rpc_expect = "validate" + if self.client: + client_token, server_token = self.sign_token(self.connection_id, self.rpc_nonce, nonce) + if token != server_token: + self.send_json([0, ["shutdown", ["Invalid server token"]] ], nobuffer=True) + self.shutdown() + return + self.rpc_client_token = client_token + self.send_json([0, ["validate", [self.rpc_client_token, self.received_id], self.rpc_state]], nobuffer=True) + else: + # Server + self.rpc_nonce = uuid.uuid4().hex + if key_version != self.rpc_key_version: + self.send_json([0, ["shutdown", ["Invalid key version: %s" % key_version]] ], + nobuffer=True) + self.shutdown() + return + + self.rpc_unvalidated_id = connection_id + client_token, server_token = self.sign_token(connection_id, nonce, self.rpc_nonce) + self.rpc_client_token = client_token + self.send_json([0, ["connect", [None, None, self.rpc_nonce, server_token]] ], + nobuffer=True) + + def rpc_server_validate(self, token): + if token != self.rpc_client_token: + self.send_json([0, ["shutdown", ["Invalid client token"]] ], nobuffer=True) + self.shutdown() + return False + self.new_connection(self.rpc_unvalidated_id) + self.send_json([0, ["validate", [None, self.received_id], self.rpc_state]], nobuffer=True) + return True + + def sign_token(self, connection_id, client_nonce, server_nonce): + """ Returns client_token, server_token + For servers with non-None key_version, key_secret is treated as a master key, + and the string key_version+SIGN_SEP+connection_id is "signed" with the master + to generate the actual signing key. + """ + key_secret = self.rpc_key_secret + if self.client or self.rpc_key_version is None: + key_secret = self.rpc_key_secret + else: + key_secret = hmac.new(str(self.rpc_key_secret), str(self.rpc_key_version)+SIGN_SEP+connection_id, digestmod=SIGN_HASH).hexdigest()[:SIGN_HEXDIGITS] + + prefix = SIGN_SEP.join([connection_id, client_nonce, server_nonce]) + SIGN_SEP + return [hmac.new(str(key_secret), prefix+conn_type, digestmod=SIGN_HASH).hexdigest()[:SIGN_HEXDIGITS] for conn_type in ("client", "server")] + + def send_request_threadsafe(self, method, *args, **kwargs): + ioloop.IOLoop.instance().add_callback(functools.partial(self.send_request, method, *args, **kwargs)) + + def send_request(self, method, *args, **kwargs): + self.send_json([self.packet_id, [method, args, kwargs]], buffer=(not self.rpc_ready)) + + @classmethod + def send_to_connection(cls, connection_id, method, *args, **kwargs): + """ Sends RPC to connection_id, or raises exception if not connected. + """ + conn = cls.get_connection(connection_id) + if not conn or not conn.is_writable(): + raise Exception("Unable to write to %s" % connection_id) + conn.send_request(method, *args, **kwargs) + + @classmethod + def shutdown_connection(cls, connection_id): + conn = cls.get_connection(connection_id) + if conn: + conn.shutdown() + + def process_packet(self, data): + try: + packet_id, msg_obj = json.loads(data) + if (packet_id and self.rpc_expect) or (not packet_id and msg_obj[0] != self.rpc_expect and msg_obj[0] != "shutdown"): + # Drop packet + logging.info("RPCLink.process_packet: Dropped packet %s", msg_obj[0]) + return + + if not packet_id: + if msg_obj[0] == "shutdown": + self.shutdown() + logging.info("RPCLink.process_packet: shutdown: %s", msg_obj[1][0]) + elif msg_obj[0] == "connect": + connection_id, key_version, nonce, token = msg_obj[1] + self.rpc_connect(connection_id, key_version, nonce, token) + elif msg_obj[0] == "validate": + token, last_received_id = msg_obj[1] + if not self.client: + # Note: client-side validation already completed + if not self.rpc_server_validate(token): + return + self.rpc_peer_state = msg_obj[2] + self.clear_sent_packets(last_received_id) + self.resend_buffered_packets() + self.rpc_expect = "" + self.rpc_ready = True + return + except Exception, excp: + logging.warning("RPCLink.process_packet: Error in RPC message processing: %s", excp) + if not self.connection_id: + self.shutdown() + return + + if packet_id < 0: + # Ack for outbound message; pop out ack'ed and older packets in buffer + if msg_obj: + logging.debug("Return value: %s: %s", self.connection_id, msg_obj) + self.clear_sent_packets(-packet_id) + elif not self.connection_id: + self.shutdown() + raise packetserver.SystemMessage("Expected setup packet") + else: + # New inbound message of the form [method, args_array, kwargs_dict] + retval = None + try: + args = msg_obj[1] if len(msg_obj) > 1 else [] + kwargs = dict2kwargs(msg_obj[2]) if len(msg_obj) > 2 else {} + bound_method = getattr(self, "remote_"+msg_obj[0], None) + if bound_method: + retval = bound_method(*args, **kwargs) + else: + retval = self.invoke_method(msg_obj[0], *args, **kwargs) + except Exception, excp: + retval = "Error: %s: %s\n%s" % (msg_obj[0], excp, "".join(traceback.format_exc())) + logging.warning("RPCLink.process_packet: %s: %s", self.connection_id, retval) + + if packet_id > 0: + # Acknowledge message + self.received_id = packet_id + try: + self.send_json([-packet_id, retval]) + except Exception, excp: + pass + + def invoke_method(self, method, *args, **kwargs): + retval = "Error: Invalid remote method %s" % method + logging.warning("RPCLink.invoke_method: %s: %s", self.connection_id, retval) + return retval + +class PolicyServer(PacketConnection): + def __init__(self, stream, address, server_address, allow_domain="*", allow_ports="843"): + super(PolicyServer, self).__init__(stream, address, server_address, + single_request=True, server_type="flash") + self.allow_domain = allow_domain + self.allow_ports = allow_ports + + def process_packet(self, message): + """ Processes request packet ; raises exception on error + """ + request = message.strip() + if not request: + self.shutdown() + return + + logging.debug("Policy request: %s", request) + if request.startswith(POLICY_FILE_REQUEST): + policy_file = MASTER_POLICY_XML_FORMAT % (self.allow_domain, self.allow_ports) + self.send_packet(policy_file, utf8=True) + else: + host_port = self.map_id_to_host(message) + self.send_packet(host_port, utf8=True) + + def map_id_to_host(self, group_zid): + """ Maps group zid to host:port, returning host:port/path, where path may be a null string + """ + raise SystemMessage("NOT IMPLEMENTED") + + +class FlashSocket(PacketConnection): + COOKIE_SEP = "|" + + def __init__(self, stream, address, server_address, allow_domain=""): + """Specify allow_domain to allow socket to also serve policy files + """ + self.allow_domain = allow_domain + self.message_count = 0 + self.group_zid = None + self.user_code = None + self.group_code = None + self.open_access = None + self.controller_type = None + self.authenticated = False + + self.client_nonce = None + self.client_type = None + self.client_params = None + self.server_nonce = "snonce" + self.client_signature = None + self.server_signature = None + super(FlashSocket, self).__init__(stream, address, server_address, server_type="flash") + + def map_id_to_host(self, group_zid): + """ Maps group zid to host:port, returning host:port/path, where path may be a null string + """ + raise SystemMessage("NOT IMPLEMENTED") + + def process_packet(self, message): + """ Processes flash request; raises exception on error + """ + if self.allow_domain and message.startswith(POLICY_FILE_REQUEST): + policy_file = SOCKET_POLICY_XML_FORMAT % (self.allow_domain, self.server_address[1]) + self.send_packet(policy_file, utf8=True) + return + + try: + self.process_flash_request(message) + except Exception, excp: + logging.warning("FlashSocket: Error in processing request: %s", excp, exc_info=True) + if isinstance(excp, UserMessage) and excp.args: + errmsg, sep, tail = excp.args[0].partition("\n") + else: + errmsg = "Server error in processing request" + self.send_json(["ERROR", errmsg], finish=True) + return + + def process_flash_request(self, message): + if not self.message_count and message.startswith("FLASH"): + # Strip any HTTP-like headers prefix (used for proxying) + headers, sep, message = message.partition("\r\n\r\n") + + if message.startswith("Map-Name:"): + header, sep, value = message.partition(":") + self.send_packet(self.map_name_to_host(value.strip()), finish=True, utf8=true) + return + elif not message.startswith("["): + host_port = self.map_id_to_host(message) + self.send_packet(host_port, utf8=True) + return + + self.message_count += 1 + message_obj = json.loads(message) + if self.message_count == 1: + if message_obj[0] != "C_HANDSHAKE1": + raise SystemMessage("Expecting C_HANDSHAKE1 but received %s" % message_obj[0]) + + command, self.group_zid, self.user_code, key_version, self.client_nonce, self.client_type, self.client_params = message_obj + logging.info("Connection request from %s for group %s and user %s with key %s", self.client_type, self.group_zid, self.user_code, key_version) + + self.open_access, access_secret = self.get_access_info(key_version) + if not self.open_access and not access_secret: + raise UserMessage("Invalid access key for group/user %s/%s" % (self.group_zid, self.user_code)) + + server_prefix = FlashSocket.COOKIE_SEP.join([self.group_zid, key_version, self.client_nonce, + self.server_nonce])+FlashSocket.COOKIE_SEP + self.client_signature = hashlib.sha1(server_prefix+"client"+access_secret).hexdigest() + self.server_signature = hashlib.sha1(server_prefix+"server"+access_secret).hexdigest() + self.send_json(["S_HANDSHAKE1", self.server_nonce, self.server_signature]) + + elif self.message_count == 2: + if message_obj[0] != "C_HANDSHAKE2": + raise SystemMessage("Expecting C_HANDSHAKE2 but received %s" % message_obj[0]) + if message_obj[1] == self.client_signature: + self.authenticated = True + elif self.user_code or not self.open_access: + raise UserMessage("Client authentication failed") + self.register_flash_socket() + else: + try: + self.handle_message_obj(message_obj) + except UserMessage: + raise + except Exception, excp: + logging.error("FlashSocket.ProcessRequest: Error - %s", excp, exc_info=True) + + def get_access_info(self, key_version): + raise SystemMessage("NOT IMPLEMENTED") + + def register_flash_socket(self): + """ Register socket with controlling com_channel for session + Returns session/task params dict + Raises exception on failure + """ + raise SystemMessage("NOT IMPLEMENTED") + + def handle_close(self): + raise SystemMessage("NOT IMPLEMENTED") + + def handle_message_obj(self, message_obj): + raise SystemMessage("NOT IMPLEMENTED") + + +if __name__ == "__main__": + Flash_socket = None + class TestPolicyServer(PolicyServer): + def map_id_to_host(self, group_zid): + return "localhost:%s" % FLASH_SOCKET_PORT + + class TestFlashSocket(FlashSocket): + def get_access_info(self, key_version): + return False, self.group_zid+","+key_version + + def register_flash_socket(self): + global Flash_socket + if Flash_socket: + Flash_socket.shutdown() + Flash_socket = self + return {"task": 1, "choice_count": 3} + + def handle_message_obj(self, message_obj): + data = ["CHAT", "testuser", "black", "Test message!"] + self.send_json(data) + + def handle_close(self): + pass + + HOST = "localhost" + IO_loop = ioloop.IOLoop.instance() + TestPolicyServer.start_tcp_server(HOST, FLASH_POLICY_PORT, io_loop=IO_loop) + TestFlashSocket.start_tcp_server(HOST, FLASH_SOCKET_PORT, io_loop=IO_loop) + IO_loop.start() diff --git a/graphterm/testsslclient.py b/graphterm/testsslclient.py new file mode 100755 index 0000000..4fcf734 --- /dev/null +++ b/graphterm/testsslclient.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python + +import os, socket, ssl, sys, pprint, time + +host_port = ('localhost', 8899) + +ssl_options = {"cert_reqs": ssl.CERT_REQUIRED, "ca_certs": os.getenv("HOME")+"/.ssh/localhost.crt"} + +if 0: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + # require a certificate from the server + ssl_sock = ssl.wrap_socket(s, **ssl_options) + + ssl_sock.connect(host_port) + + pprint.pprint(ssl_sock.getpeercert()) + # note that closing the SSLSocket will also close the underlying socket + ssl_sock.close() + +def on_connect(): + print "Connected" + time.sleep(5) + +import tornado.iostream, tornado.ioloop + +if 0: + stream = tornado.iostream.SSLIOStream(socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0), + ssl_options=ssl_options) + + stream.connect(host_port, on_connect) + + sys.exit(0) + +import packetserver + +class MyClient(packetserver.PacketClient): + _all_connections = {} + def __init__(self, host, port, lterm_cookie="", io_loop=None, ssl_options={}): + super(MyClient, self).__init__(host, port, io_loop=io_loop, + ssl_options=ssl_options, max_packet_buf=3, + reconnect_sec=300, server_type="frame") + +conn2 = MyClient.get_client("conn2", connect=host_port, connect_kw={"ssl_options": ssl_options}) + +tornado.ioloop.IOLoop.instance().start() + + +print "conn2", conn2 diff --git a/graphterm/www/ace.js b/graphterm/www/ace.js new file mode 100644 index 0000000..50d7e86 --- /dev/null +++ b/graphterm/www/ace.js @@ -0,0 +1,19976 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +/** + * Define a module along with a payload + * @param module a name for the payload + * @param payload a function to call with (require, exports, module) params + */ + +(function() { + +var ACE_NAMESPACE = "ace"; + +var global = (function() { + return this; +})(); + +var _define = function(module, deps, payload) { + if (typeof module !== 'string') { + if (_define.original) + _define.original.apply(window, arguments); + else { + console.error('dropping module because define wasn\'t a string.'); + console.trace(); + } + return; + } + + if (arguments.length == 2) + payload = deps; + + if (!_define.modules) + _define.modules = {}; + + _define.modules[module] = payload; +}; + +/** + * Get at functionality ace.define()ed using the function above + */ +var _require = function(parentId, module, callback) { + if (Object.prototype.toString.call(module) === "[object Array]") { + var params = []; + for (var i = 0, l = module.length; i < l; ++i) { + var dep = lookup(parentId, module[i]); + if (!dep && _require.original) + return _require.original.apply(window, arguments); + params.push(dep); + } + if (callback) { + callback.apply(null, params); + } + } + else if (typeof module === 'string') { + var payload = lookup(parentId, module); + if (!payload && _require.original) + return _require.original.apply(window, arguments); + + if (callback) { + callback(); + } + + return payload; + } + else { + if (_require.original) + return _require.original.apply(window, arguments); + } +}; + +var normalizeModule = function(parentId, moduleName) { + // normalize plugin requires + if (moduleName.indexOf("!") !== -1) { + var chunks = moduleName.split("!"); + return normalizeModule(parentId, chunks[0]) + "!" + normalizeModule(parentId, chunks[1]); + } + // normalize relative requires + if (moduleName.charAt(0) == ".") { + var base = parentId.split("/").slice(0, -1).join("/"); + moduleName = base + "/" + moduleName; + + while(moduleName.indexOf(".") !== -1 && previous != moduleName) { + var previous = moduleName; + moduleName = moduleName.replace(/\/\.\//, "/").replace(/[^\/]+\/\.\.\//, ""); + } + } + + return moduleName; +}; + +/** + * Internal function to lookup moduleNames and resolve them by calling the + * definition function if needed. + */ +var lookup = function(parentId, moduleName) { + + moduleName = normalizeModule(parentId, moduleName); + + var module = _define.modules[moduleName]; + if (!module) { + return null; + } + + if (typeof module === 'function') { + var exports = {}; + var mod = { + id: moduleName, + uri: '', + exports: exports, + packaged: true + }; + + var req = function(module, callback) { + return _require(moduleName, module, callback); + }; + + var returnValue = module(req, exports, mod); + exports = returnValue || mod.exports; + + // cache the resulting module object for next time + _define.modules[moduleName] = exports; + return exports; + } + + return module; +}; + +function exportAce(ns) { + + if (typeof requirejs !== "undefined") { + + var define = global.define; + global.define = function(id, deps, callback) { + if (typeof callback !== "function") + return define.apply(this, arguments); + + return ace.define(id, deps, function(require, exports, module) { + if (deps[2] == "module") + module.packaged = true; + return callback.apply(this, arguments); + }); + }; + global.define.packaged = true; + + return; + } + + var require = function(module, callback) { + return _require("", module, callback); + }; + require.packaged = true; + + var root = global; + if (ns) { + if (!global[ns]) + global[ns] = {}; + root = global[ns]; + } + + if (root.define) + _define.original = root.define; + + root.define = _define; + + if (root.require) + _require.original = root.require; + + root.require = require; +} + +exportAce(ACE_NAMESPACE); + +})();/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Skywriter. + * + * The Initial Developer of the Original Code is + * Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Kevin Dangoor (kdangoor@mozilla.com) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +/** + * class Ace + * + * The main class required to set up an Ace instance in the browser. + * + * + **/ + +ace.define('ace/ace', ['require', 'exports', 'module' , 'ace/lib/fixoldbrowsers', 'ace/lib/dom', 'ace/lib/event', 'ace/editor', 'ace/edit_session', 'ace/undomanager', 'ace/virtual_renderer', 'ace/multi_select', 'ace/worker/worker_client', 'ace/keyboard/hash_handler', 'ace/keyboard/state_handler', 'ace/placeholder', 'ace/config', 'ace/theme/textmate'], function(require, exports, module) { +"use strict"; + +require("./lib/fixoldbrowsers"); + +var Dom = require("./lib/dom"); +var Event = require("./lib/event"); + +var Editor = require("./editor").Editor; +var EditSession = require("./edit_session").EditSession; +var UndoManager = require("./undomanager").UndoManager; +var Renderer = require("./virtual_renderer").VirtualRenderer; +var MultiSelect = require("./multi_select").MultiSelect; + +// The following require()s are for inclusion in the built ace file +require("./worker/worker_client"); +require("./keyboard/hash_handler"); +require("./keyboard/state_handler"); +require("./placeholder"); +require("./config").init(); + + /** + * Ace.edit(el) -> Editor + * - el (String | DOMElement): Either the id of an element, or the element itself + * + * This method embeds the Ace editor into the DOM, at the element provided by `el`. + * + **/ +exports.edit = function(el) { + if (typeof(el) == "string") { + el = document.getElementById(el); + } + + var doc = new EditSession(Dom.getInnerText(el)); + doc.setUndoManager(new UndoManager()); + el.innerHTML = ''; + + var editor = new Editor(new Renderer(el, require("./theme/textmate"))); + new MultiSelect(editor); + editor.setSession(doc); + + var env = {}; + env.document = doc; + env.editor = editor; + editor.resize(); + Event.addListener(window, "resize", function() { + editor.resize(); + }); + el.env = env; + // Store env on editor such that it can be accessed later on from + // the returned object. + editor.env = env; + return editor; +}; + +}); +// vim:set ts=4 sts=4 sw=4 st: +// -- kriskowal Kris Kowal Copyright (C) 2009-2010 MIT License +// -- tlrobinson Tom Robinson Copyright (C) 2009-2010 MIT License (Narwhal Project) +// -- dantman Daniel Friesen Copyright(C) 2010 XXX No License Specified +// -- fschaefer Florian Schäfer Copyright (C) 2010 MIT License +// -- Irakli Gozalishvili Copyright (C) 2010 MIT License + +/*! + Copyright (c) 2009, 280 North Inc. http://280north.com/ + MIT License. http://github.com/280north/narwhal/blob/master/README.md +*/ + +ace.define('ace/lib/fixoldbrowsers', ['require', 'exports', 'module' , 'ace/lib/regexp', 'ace/lib/es5-shim'], function(require, exports, module) { +"use strict"; + +require("./regexp"); +require("./es5-shim"); + +}); +/* + * Based on code from: + * + * XRegExp 1.5.0 + * (c) 2007-2010 Steven Levithan + * MIT License + * + * Provides an augmented, extensible, cross-browser implementation of regular expressions, + * including support for additional syntax, flags, and methods + */ + +ace.define('ace/lib/regexp', ['require', 'exports', 'module' ], function(require, exports, module) { +"use strict"; + + //--------------------------------- + // Private variables + //--------------------------------- + + var real = { + exec: RegExp.prototype.exec, + test: RegExp.prototype.test, + match: String.prototype.match, + replace: String.prototype.replace, + split: String.prototype.split + }, + compliantExecNpcg = real.exec.call(/()??/, "")[1] === undefined, // check `exec` handling of nonparticipating capturing groups + compliantLastIndexIncrement = function () { + var x = /^/g; + real.test.call(x, ""); + return !x.lastIndex; + }(); + + //--------------------------------- + // Overriden native methods + //--------------------------------- + + // Adds named capture support (with backreferences returned as `result.name`), and fixes two + // cross-browser issues per ES3: + // - Captured values for nonparticipating capturing groups should be returned as `undefined`, + // rather than the empty string. + // - `lastIndex` should not be incremented after zero-length matches. + RegExp.prototype.exec = function (str) { + var match = real.exec.apply(this, arguments), + name, r2; + if ( typeof(str) == 'string' && match) { + // Fix browsers whose `exec` methods don't consistently return `undefined` for + // nonparticipating capturing groups + if (!compliantExecNpcg && match.length > 1 && indexOf(match, "") > -1) { + r2 = RegExp(this.source, real.replace.call(getNativeFlags(this), "g", "")); + // Using `str.slice(match.index)` rather than `match[0]` in case lookahead allowed + // matching due to characters outside the match + real.replace.call(str.slice(match.index), r2, function () { + for (var i = 1; i < arguments.length - 2; i++) { + if (arguments[i] === undefined) + match[i] = undefined; + } + }); + } + // Attach named capture properties + if (this._xregexp && this._xregexp.captureNames) { + for (var i = 1; i < match.length; i++) { + name = this._xregexp.captureNames[i - 1]; + if (name) + match[name] = match[i]; + } + } + // Fix browsers that increment `lastIndex` after zero-length matches + if (!compliantLastIndexIncrement && this.global && !match[0].length && (this.lastIndex > match.index)) + this.lastIndex--; + } + return match; + }; + + // Don't override `test` if it won't change anything + if (!compliantLastIndexIncrement) { + // Fix browser bug in native method + RegExp.prototype.test = function (str) { + // Use the native `exec` to skip some processing overhead, even though the overriden + // `exec` would take care of the `lastIndex` fix + var match = real.exec.call(this, str); + // Fix browsers that increment `lastIndex` after zero-length matches + if (match && this.global && !match[0].length && (this.lastIndex > match.index)) + this.lastIndex--; + return !!match; + }; + } + + //--------------------------------- + // Private helper functions + //--------------------------------- + + function getNativeFlags (regex) { + return (regex.global ? "g" : "") + + (regex.ignoreCase ? "i" : "") + + (regex.multiline ? "m" : "") + + (regex.extended ? "x" : "") + // Proposed for ES4; included in AS3 + (regex.sticky ? "y" : ""); + }; + + function indexOf (array, item, from) { + if (Array.prototype.indexOf) // Use the native array method if available + return array.indexOf(item, from); + for (var i = from || 0; i < array.length; i++) { + if (array[i] === item) + return i; + } + return -1; + }; + +}); +// vim: ts=4 sts=4 sw=4 expandtab +// -- kriskowal Kris Kowal Copyright (C) 2009-2011 MIT License +// -- tlrobinson Tom Robinson Copyright (C) 2009-2010 MIT License (Narwhal Project) +// -- dantman Daniel Friesen Copyright (C) 2010 XXX TODO License or CLA +// -- fschaefer Florian Schäfer Copyright (C) 2010 MIT License +// -- Gozala Irakli Gozalishvili Copyright (C) 2010 MIT License +// -- kitcambridge Kit Cambridge Copyright (C) 2011 MIT License +// -- kossnocorp Sasha Koss XXX TODO License or CLA +// -- bryanforbes Bryan Forbes XXX TODO License or CLA +// -- killdream Quildreen Motta Copyright (C) 2011 MIT Licence +// -- michaelficarra Michael Ficarra Copyright (C) 2011 3-clause BSD License +// -- sharkbrainguy Gerard Paapu Copyright (C) 2011 MIT License +// -- bbqsrc Brendan Molloy (C) 2011 Creative Commons Zero (public domain) +// -- iwyg XXX TODO License or CLA +// -- DomenicDenicola Domenic Denicola Copyright (C) 2011 MIT License +// -- xavierm02 Montillet Xavier XXX TODO License or CLA +// -- Raynos Raynos XXX TODO License or CLA +// -- samsonjs Sami Samhuri Copyright (C) 2010 MIT License +// -- rwldrn Rick Waldron Copyright (C) 2011 MIT License +// -- lexer Alexey Zakharov XXX TODO License or CLA + +/*! + Copyright (c) 2009, 280 North Inc. http://280north.com/ + MIT License. http://github.com/280north/narwhal/blob/master/README.md +*/ + +ace.define('ace/lib/es5-shim', ['require', 'exports', 'module' ], function(require, exports, module) { + +/* + * Brings an environment as close to ECMAScript 5 compliance + * as is possible with the facilities of erstwhile engines. + * + * Annotated ES5: http://es5.github.com/ (specific links below) + * ES5 Spec: http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf + * + * @module + */ + +/*whatsupdoc*/ + +// +// Function +// ======== +// + +// ES-5 15.3.4.5 +// http://es5.github.com/#x15.3.4.5 + +if (!Function.prototype.bind) { + Function.prototype.bind = function bind(that) { // .length is 1 + // 1. Let Target be the this value. + var target = this; + // 2. If IsCallable(Target) is false, throw a TypeError exception. + if (typeof target != "function") + throw new TypeError(); // TODO message + // 3. Let A be a new (possibly empty) internal list of all of the + // argument values provided after thisArg (arg1, arg2 etc), in order. + // XXX slicedArgs will stand in for "A" if used + var args = slice.call(arguments, 1); // for normal call + // 4. Let F be a new native ECMAScript object. + // 11. Set the [[Prototype]] internal property of F to the standard + // built-in Function prototype object as specified in 15.3.3.1. + // 12. Set the [[Call]] internal property of F as described in + // 15.3.4.5.1. + // 13. Set the [[Construct]] internal property of F as described in + // 15.3.4.5.2. + // 14. Set the [[HasInstance]] internal property of F as described in + // 15.3.4.5.3. + var bound = function () { + + if (this instanceof bound) { + // 15.3.4.5.2 [[Construct]] + // When the [[Construct]] internal method of a function object, + // F that was created using the bind function is called with a + // list of arguments ExtraArgs, the following steps are taken: + // 1. Let target be the value of F's [[TargetFunction]] + // internal property. + // 2. If target has no [[Construct]] internal method, a + // TypeError exception is thrown. + // 3. Let boundArgs be the value of F's [[BoundArgs]] internal + // property. + // 4. Let args be a new list containing the same values as the + // list boundArgs in the same order followed by the same + // values as the list ExtraArgs in the same order. + // 5. Return the result of calling the [[Construct]] internal + // method of target providing args as the arguments. + + var F = function(){}; + F.prototype = target.prototype; + var self = new F; + + var result = target.apply( + self, + args.concat(slice.call(arguments)) + ); + if (result !== null && Object(result) === result) + return result; + return self; + + } else { + // 15.3.4.5.1 [[Call]] + // When the [[Call]] internal method of a function object, F, + // which was created using the bind function is called with a + // this value and a list of arguments ExtraArgs, the following + // steps are taken: + // 1. Let boundArgs be the value of F's [[BoundArgs]] internal + // property. + // 2. Let boundThis be the value of F's [[BoundThis]] internal + // property. + // 3. Let target be the value of F's [[TargetFunction]] internal + // property. + // 4. Let args be a new list containing the same values as the + // list boundArgs in the same order followed by the same + // values as the list ExtraArgs in the same order. + // 5. Return the result of calling the [[Call]] internal method + // of target providing boundThis as the this value and + // providing args as the arguments. + + // equiv: target.call(this, ...boundArgs, ...args) + return target.apply( + that, + args.concat(slice.call(arguments)) + ); + + } + + }; + // XXX bound.length is never writable, so don't even try + // + // 15. If the [[Class]] internal property of Target is "Function", then + // a. Let L be the length property of Target minus the length of A. + // b. Set the length own property of F to either 0 or L, whichever is + // larger. + // 16. Else set the length own property of F to 0. + // 17. Set the attributes of the length own property of F to the values + // specified in 15.3.5.1. + + // TODO + // 18. Set the [[Extensible]] internal property of F to true. + + // TODO + // 19. Let thrower be the [[ThrowTypeError]] function Object (13.2.3). + // 20. Call the [[DefineOwnProperty]] internal method of F with + // arguments "caller", PropertyDescriptor {[[Get]]: thrower, [[Set]]: + // thrower, [[Enumerable]]: false, [[Configurable]]: false}, and + // false. + // 21. Call the [[DefineOwnProperty]] internal method of F with + // arguments "arguments", PropertyDescriptor {[[Get]]: thrower, + // [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false}, + // and false. + + // TODO + // NOTE Function objects created using Function.prototype.bind do not + // have a prototype property or the [[Code]], [[FormalParameters]], and + // [[Scope]] internal properties. + // XXX can't delete prototype in pure-js. + + // 22. Return F. + return bound; + }; +} + +// Shortcut to an often accessed properties, in order to avoid multiple +// dereference that costs universally. +// _Please note: Shortcuts are defined after `Function.prototype.bind` as we +// us it in defining shortcuts. +var call = Function.prototype.call; +var prototypeOfArray = Array.prototype; +var prototypeOfObject = Object.prototype; +var slice = prototypeOfArray.slice; +var toString = call.bind(prototypeOfObject.toString); +var owns = call.bind(prototypeOfObject.hasOwnProperty); + +// If JS engine supports accessors creating shortcuts. +var defineGetter; +var defineSetter; +var lookupGetter; +var lookupSetter; +var supportsAccessors; +if ((supportsAccessors = owns(prototypeOfObject, "__defineGetter__"))) { + defineGetter = call.bind(prototypeOfObject.__defineGetter__); + defineSetter = call.bind(prototypeOfObject.__defineSetter__); + lookupGetter = call.bind(prototypeOfObject.__lookupGetter__); + lookupSetter = call.bind(prototypeOfObject.__lookupSetter__); +} + +// +// Array +// ===== +// + +// ES5 15.4.3.2 +// http://es5.github.com/#x15.4.3.2 +// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/isArray +if (!Array.isArray) { + Array.isArray = function isArray(obj) { + return toString(obj) == "[object Array]"; + }; +} + +// The IsCallable() check in the Array functions +// has been replaced with a strict check on the +// internal class of the object to trap cases where +// the provided function was actually a regular +// expression literal, which in V8 and +// JavaScriptCore is a typeof "function". Only in +// V8 are regular expression literals permitted as +// reduce parameters, so it is desirable in the +// general case for the shim to match the more +// strict and common behavior of rejecting regular +// expressions. + +// ES5 15.4.4.18 +// http://es5.github.com/#x15.4.4.18 +// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/forEach +if (!Array.prototype.forEach) { + Array.prototype.forEach = function forEach(fun /*, thisp*/) { + var self = toObject(this), + thisp = arguments[1], + i = 0, + length = self.length >>> 0; + + // If no callback function or if callback is not a callable function + if (toString(fun) != "[object Function]") { + throw new TypeError(); // TODO message + } + + while (i < length) { + if (i in self) { + // Invoke the callback function with call, passing arguments: + // context, property value, property key, thisArg object context + fun.call(thisp, self[i], i, self); + } + i++; + } + }; +} + +// ES5 15.4.4.19 +// http://es5.github.com/#x15.4.4.19 +// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/map +if (!Array.prototype.map) { + Array.prototype.map = function map(fun /*, thisp*/) { + var self = toObject(this), + length = self.length >>> 0, + result = Array(length), + thisp = arguments[1]; + + // If no callback function or if callback is not a callable function + if (toString(fun) != "[object Function]") { + throw new TypeError(); // TODO message + } + + for (var i = 0; i < length; i++) { + if (i in self) + result[i] = fun.call(thisp, self[i], i, self); + } + return result; + }; +} + +// ES5 15.4.4.20 +// http://es5.github.com/#x15.4.4.20 +// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/filter +if (!Array.prototype.filter) { + Array.prototype.filter = function filter(fun /*, thisp */) { + var self = toObject(this), + length = self.length >>> 0, + result = [], + thisp = arguments[1]; + + // If no callback function or if callback is not a callable function + if (toString(fun) != "[object Function]") { + throw new TypeError(); // TODO message + } + + for (var i = 0; i < length; i++) { + if (i in self && fun.call(thisp, self[i], i, self)) + result.push(self[i]); + } + return result; + }; +} + +// ES5 15.4.4.16 +// http://es5.github.com/#x15.4.4.16 +// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/every +if (!Array.prototype.every) { + Array.prototype.every = function every(fun /*, thisp */) { + var self = toObject(this), + length = self.length >>> 0, + thisp = arguments[1]; + + // If no callback function or if callback is not a callable function + if (toString(fun) != "[object Function]") { + throw new TypeError(); // TODO message + } + + for (var i = 0; i < length; i++) { + if (i in self && !fun.call(thisp, self[i], i, self)) + return false; + } + return true; + }; +} + +// ES5 15.4.4.17 +// http://es5.github.com/#x15.4.4.17 +// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/some +if (!Array.prototype.some) { + Array.prototype.some = function some(fun /*, thisp */) { + var self = toObject(this), + length = self.length >>> 0, + thisp = arguments[1]; + + // If no callback function or if callback is not a callable function + if (toString(fun) != "[object Function]") { + throw new TypeError(); // TODO message + } + + for (var i = 0; i < length; i++) { + if (i in self && fun.call(thisp, self[i], i, self)) + return true; + } + return false; + }; +} + +// ES5 15.4.4.21 +// http://es5.github.com/#x15.4.4.21 +// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/reduce +if (!Array.prototype.reduce) { + Array.prototype.reduce = function reduce(fun /*, initial*/) { + var self = toObject(this), + length = self.length >>> 0; + + // If no callback function or if callback is not a callable function + if (toString(fun) != "[object Function]") { + throw new TypeError(); // TODO message + } + + // no value to return if no initial value and an empty array + if (!length && arguments.length == 1) + throw new TypeError(); // TODO message + + var i = 0; + var result; + if (arguments.length >= 2) { + result = arguments[1]; + } else { + do { + if (i in self) { + result = self[i++]; + break; + } + + // if array contains no values, no initial value to return + if (++i >= length) + throw new TypeError(); // TODO message + } while (true); + } + + for (; i < length; i++) { + if (i in self) + result = fun.call(void 0, result, self[i], i, self); + } + + return result; + }; +} + +// ES5 15.4.4.22 +// http://es5.github.com/#x15.4.4.22 +// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/reduceRight +if (!Array.prototype.reduceRight) { + Array.prototype.reduceRight = function reduceRight(fun /*, initial*/) { + var self = toObject(this), + length = self.length >>> 0; + + // If no callback function or if callback is not a callable function + if (toString(fun) != "[object Function]") { + throw new TypeError(); // TODO message + } + + // no value to return if no initial value, empty array + if (!length && arguments.length == 1) + throw new TypeError(); // TODO message + + var result, i = length - 1; + if (arguments.length >= 2) { + result = arguments[1]; + } else { + do { + if (i in self) { + result = self[i--]; + break; + } + + // if array contains no values, no initial value to return + if (--i < 0) + throw new TypeError(); // TODO message + } while (true); + } + + do { + if (i in this) + result = fun.call(void 0, result, self[i], i, self); + } while (i--); + + return result; + }; +} + +// ES5 15.4.4.14 +// http://es5.github.com/#x15.4.4.14 +// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf +if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function indexOf(sought /*, fromIndex */ ) { + var self = toObject(this), + length = self.length >>> 0; + + if (!length) + return -1; + + var i = 0; + if (arguments.length > 1) + i = toInteger(arguments[1]); + + // handle negative indices + i = i >= 0 ? i : Math.max(0, length + i); + for (; i < length; i++) { + if (i in self && self[i] === sought) { + return i; + } + } + return -1; + }; +} + +// ES5 15.4.4.15 +// http://es5.github.com/#x15.4.4.15 +// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/lastIndexOf +if (!Array.prototype.lastIndexOf) { + Array.prototype.lastIndexOf = function lastIndexOf(sought /*, fromIndex */) { + var self = toObject(this), + length = self.length >>> 0; + + if (!length) + return -1; + var i = length - 1; + if (arguments.length > 1) + i = Math.min(i, toInteger(arguments[1])); + // handle negative indices + i = i >= 0 ? i : length - Math.abs(i); + for (; i >= 0; i--) { + if (i in self && sought === self[i]) + return i; + } + return -1; + }; +} + +// +// Object +// ====== +// + +// ES5 15.2.3.2 +// http://es5.github.com/#x15.2.3.2 +if (!Object.getPrototypeOf) { + // https://github.com/kriskowal/es5-shim/issues#issue/2 + // http://ejohn.org/blog/objectgetprototypeof/ + // recommended by fschaefer on github + Object.getPrototypeOf = function getPrototypeOf(object) { + return object.__proto__ || ( + object.constructor ? + object.constructor.prototype : + prototypeOfObject + ); + }; +} + +// ES5 15.2.3.3 +// http://es5.github.com/#x15.2.3.3 +if (!Object.getOwnPropertyDescriptor) { + var ERR_NON_OBJECT = "Object.getOwnPropertyDescriptor called on a " + + "non-object: "; + Object.getOwnPropertyDescriptor = function getOwnPropertyDescriptor(object, property) { + if ((typeof object != "object" && typeof object != "function") || object === null) + throw new TypeError(ERR_NON_OBJECT + object); + // If object does not owns property return undefined immediately. + if (!owns(object, property)) + return; + + var descriptor, getter, setter; + + // If object has a property then it's for sure both `enumerable` and + // `configurable`. + descriptor = { enumerable: true, configurable: true }; + + // If JS engine supports accessor properties then property may be a + // getter or setter. + if (supportsAccessors) { + // Unfortunately `__lookupGetter__` will return a getter even + // if object has own non getter property along with a same named + // inherited getter. To avoid misbehavior we temporary remove + // `__proto__` so that `__lookupGetter__` will return getter only + // if it's owned by an object. + var prototype = object.__proto__; + object.__proto__ = prototypeOfObject; + + var getter = lookupGetter(object, property); + var setter = lookupSetter(object, property); + + // Once we have getter and setter we can put values back. + object.__proto__ = prototype; + + if (getter || setter) { + if (getter) descriptor.get = getter; + if (setter) descriptor.set = setter; + + // If it was accessor property we're done and return here + // in order to avoid adding `value` to the descriptor. + return descriptor; + } + } + + // If we got this far we know that object has an own property that is + // not an accessor so we set it as a value and return descriptor. + descriptor.value = object[property]; + return descriptor; + }; +} + +// ES5 15.2.3.4 +// http://es5.github.com/#x15.2.3.4 +if (!Object.getOwnPropertyNames) { + Object.getOwnPropertyNames = function getOwnPropertyNames(object) { + return Object.keys(object); + }; +} + +// ES5 15.2.3.5 +// http://es5.github.com/#x15.2.3.5 +if (!Object.create) { + Object.create = function create(prototype, properties) { + var object; + if (prototype === null) { + object = { "__proto__": null }; + } else { + if (typeof prototype != "object") + throw new TypeError("typeof prototype["+(typeof prototype)+"] != 'object'"); + var Type = function () {}; + Type.prototype = prototype; + object = new Type(); + // IE has no built-in implementation of `Object.getPrototypeOf` + // neither `__proto__`, but this manually setting `__proto__` will + // guarantee that `Object.getPrototypeOf` will work as expected with + // objects created using `Object.create` + object.__proto__ = prototype; + } + if (properties !== void 0) + Object.defineProperties(object, properties); + return object; + }; +} + +// ES5 15.2.3.6 +// http://es5.github.com/#x15.2.3.6 + +// Patch for WebKit and IE8 standard mode +// Designed by hax +// related issue: https://github.com/kriskowal/es5-shim/issues#issue/5 +// IE8 Reference: +// http://msdn.microsoft.com/en-us/library/dd282900.aspx +// http://msdn.microsoft.com/en-us/library/dd229916.aspx +// WebKit Bugs: +// https://bugs.webkit.org/show_bug.cgi?id=36423 + +function doesDefinePropertyWork(object) { + try { + Object.defineProperty(object, "sentinel", {}); + return "sentinel" in object; + } catch (exception) { + // returns falsy + } +} + +// check whether defineProperty works if it's given. Otherwise, +// shim partially. +if (Object.defineProperty) { + var definePropertyWorksOnObject = doesDefinePropertyWork({}); + var definePropertyWorksOnDom = typeof document == "undefined" || + doesDefinePropertyWork(document.createElement("div")); + if (!definePropertyWorksOnObject || !definePropertyWorksOnDom) { + var definePropertyFallback = Object.defineProperty; + } +} + +if (!Object.defineProperty || definePropertyFallback) { + var ERR_NON_OBJECT_DESCRIPTOR = "Property description must be an object: "; + var ERR_NON_OBJECT_TARGET = "Object.defineProperty called on non-object: " + var ERR_ACCESSORS_NOT_SUPPORTED = "getters & setters can not be defined " + + "on this javascript engine"; + + Object.defineProperty = function defineProperty(object, property, descriptor) { + if ((typeof object != "object" && typeof object != "function") || object === null) + throw new TypeError(ERR_NON_OBJECT_TARGET + object); + if ((typeof descriptor != "object" && typeof descriptor != "function") || descriptor === null) + throw new TypeError(ERR_NON_OBJECT_DESCRIPTOR + descriptor); + + // make a valiant attempt to use the real defineProperty + // for I8's DOM elements. + if (definePropertyFallback) { + try { + return definePropertyFallback.call(Object, object, property, descriptor); + } catch (exception) { + // try the shim if the real one doesn't work + } + } + + // If it's a data property. + if (owns(descriptor, "value")) { + // fail silently if "writable", "enumerable", or "configurable" + // are requested but not supported + /* + // alternate approach: + if ( // can't implement these features; allow false but not true + !(owns(descriptor, "writable") ? descriptor.writable : true) || + !(owns(descriptor, "enumerable") ? descriptor.enumerable : true) || + !(owns(descriptor, "configurable") ? descriptor.configurable : true) + ) + throw new RangeError( + "This implementation of Object.defineProperty does not " + + "support configurable, enumerable, or writable." + ); + */ + + if (supportsAccessors && (lookupGetter(object, property) || + lookupSetter(object, property))) + { + // As accessors are supported only on engines implementing + // `__proto__` we can safely override `__proto__` while defining + // a property to make sure that we don't hit an inherited + // accessor. + var prototype = object.__proto__; + object.__proto__ = prototypeOfObject; + // Deleting a property anyway since getter / setter may be + // defined on object itself. + delete object[property]; + object[property] = descriptor.value; + // Setting original `__proto__` back now. + object.__proto__ = prototype; + } else { + object[property] = descriptor.value; + } + } else { + if (!supportsAccessors) + throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED); + // If we got that far then getters and setters can be defined !! + if (owns(descriptor, "get")) + defineGetter(object, property, descriptor.get); + if (owns(descriptor, "set")) + defineSetter(object, property, descriptor.set); + } + + return object; + }; +} + +// ES5 15.2.3.7 +// http://es5.github.com/#x15.2.3.7 +if (!Object.defineProperties) { + Object.defineProperties = function defineProperties(object, properties) { + for (var property in properties) { + if (owns(properties, property)) + Object.defineProperty(object, property, properties[property]); + } + return object; + }; +} + +// ES5 15.2.3.8 +// http://es5.github.com/#x15.2.3.8 +if (!Object.seal) { + Object.seal = function seal(object) { + // this is misleading and breaks feature-detection, but + // allows "securable" code to "gracefully" degrade to working + // but insecure code. + return object; + }; +} + +// ES5 15.2.3.9 +// http://es5.github.com/#x15.2.3.9 +if (!Object.freeze) { + Object.freeze = function freeze(object) { + // this is misleading and breaks feature-detection, but + // allows "securable" code to "gracefully" degrade to working + // but insecure code. + return object; + }; +} + +// detect a Rhino bug and patch it +try { + Object.freeze(function () {}); +} catch (exception) { + Object.freeze = (function freeze(freezeObject) { + return function freeze(object) { + if (typeof object == "function") { + return object; + } else { + return freezeObject(object); + } + }; + })(Object.freeze); +} + +// ES5 15.2.3.10 +// http://es5.github.com/#x15.2.3.10 +if (!Object.preventExtensions) { + Object.preventExtensions = function preventExtensions(object) { + // this is misleading and breaks feature-detection, but + // allows "securable" code to "gracefully" degrade to working + // but insecure code. + return object; + }; +} + +// ES5 15.2.3.11 +// http://es5.github.com/#x15.2.3.11 +if (!Object.isSealed) { + Object.isSealed = function isSealed(object) { + return false; + }; +} + +// ES5 15.2.3.12 +// http://es5.github.com/#x15.2.3.12 +if (!Object.isFrozen) { + Object.isFrozen = function isFrozen(object) { + return false; + }; +} + +// ES5 15.2.3.13 +// http://es5.github.com/#x15.2.3.13 +if (!Object.isExtensible) { + Object.isExtensible = function isExtensible(object) { + // 1. If Type(O) is not Object throw a TypeError exception. + if (Object(object) === object) { + throw new TypeError(); // TODO message + } + // 2. Return the Boolean value of the [[Extensible]] internal property of O. + var name = ''; + while (owns(object, name)) { + name += '?'; + } + object[name] = true; + var returnValue = owns(object, name); + delete object[name]; + return returnValue; + }; +} + +// ES5 15.2.3.14 +// http://es5.github.com/#x15.2.3.14 +if (!Object.keys) { + // http://whattheheadsaid.com/2010/10/a-safer-object-keys-compatibility-implementation + var hasDontEnumBug = true, + dontEnums = [ + "toString", + "toLocaleString", + "valueOf", + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable", + "constructor" + ], + dontEnumsLength = dontEnums.length; + + for (var key in {"toString": null}) + hasDontEnumBug = false; + + Object.keys = function keys(object) { + + if ((typeof object != "object" && typeof object != "function") || object === null) + throw new TypeError("Object.keys called on a non-object"); + + var keys = []; + for (var name in object) { + if (owns(object, name)) { + keys.push(name); + } + } + + if (hasDontEnumBug) { + for (var i = 0, ii = dontEnumsLength; i < ii; i++) { + var dontEnum = dontEnums[i]; + if (owns(object, dontEnum)) { + keys.push(dontEnum); + } + } + } + + return keys; + }; + +} + +// +// Date +// ==== +// + +// ES5 15.9.5.43 +// http://es5.github.com/#x15.9.5.43 +// This function returns a String value represent the instance in time +// represented by this Date object. The format of the String is the Date Time +// string format defined in 15.9.1.15. All fields are present in the String. +// The time zone is always UTC, denoted by the suffix Z. If the time value of +// this object is not a finite Number a RangeError exception is thrown. +if (!Date.prototype.toISOString || (new Date(-62198755200000).toISOString().indexOf('-000001') === -1)) { + Date.prototype.toISOString = function toISOString() { + var result, length, value, year; + if (!isFinite(this)) + throw new RangeError; + + // the date time string format is specified in 15.9.1.15. + result = [this.getUTCMonth() + 1, this.getUTCDate(), + this.getUTCHours(), this.getUTCMinutes(), this.getUTCSeconds()]; + year = this.getUTCFullYear(); + year = (year < 0 ? '-' : (year > 9999 ? '+' : '')) + ('00000' + Math.abs(year)).slice(0 <= year && year <= 9999 ? -4 : -6); + + length = result.length; + while (length--) { + value = result[length]; + // pad months, days, hours, minutes, and seconds to have two digits. + if (value < 10) + result[length] = "0" + value; + } + // pad milliseconds to have three digits. + return year + "-" + result.slice(0, 2).join("-") + "T" + result.slice(2).join(":") + "." + + ("000" + this.getUTCMilliseconds()).slice(-3) + "Z"; + } +} + +// ES5 15.9.4.4 +// http://es5.github.com/#x15.9.4.4 +if (!Date.now) { + Date.now = function now() { + return new Date().getTime(); + }; +} + +// ES5 15.9.5.44 +// http://es5.github.com/#x15.9.5.44 +// This function provides a String representation of a Date object for use by +// JSON.stringify (15.12.3). +if (!Date.prototype.toJSON) { + Date.prototype.toJSON = function toJSON(key) { + // When the toJSON method is called with argument key, the following + // steps are taken: + + // 1. Let O be the result of calling ToObject, giving it the this + // value as its argument. + // 2. Let tv be ToPrimitive(O, hint Number). + // 3. If tv is a Number and is not finite, return null. + // XXX + // 4. Let toISO be the result of calling the [[Get]] internal method of + // O with argument "toISOString". + // 5. If IsCallable(toISO) is false, throw a TypeError exception. + if (typeof this.toISOString != "function") + throw new TypeError(); // TODO message + // 6. Return the result of calling the [[Call]] internal method of + // toISO with O as the this value and an empty argument list. + return this.toISOString(); + + // NOTE 1 The argument is ignored. + + // NOTE 2 The toJSON function is intentionally generic; it does not + // require that its this value be a Date object. Therefore, it can be + // transferred to other kinds of objects for use as a method. However, + // it does require that any such object have a toISOString method. An + // object is free to use the argument key to filter its + // stringification. + }; +} + +// ES5 15.9.4.2 +// http://es5.github.com/#x15.9.4.2 +// based on work shared by Daniel Friesen (dantman) +// http://gist.github.com/303249 +if (Date.parse("+275760-09-13T00:00:00.000Z") !== 8.64e15) { + // XXX global assignment won't work in embeddings that use + // an alternate object for the context. + Date = (function(NativeDate) { + + // Date.length === 7 + var Date = function Date(Y, M, D, h, m, s, ms) { + var length = arguments.length; + if (this instanceof NativeDate) { + var date = length == 1 && String(Y) === Y ? // isString(Y) + // We explicitly pass it through parse: + new NativeDate(Date.parse(Y)) : + // We have to manually make calls depending on argument + // length here + length >= 7 ? new NativeDate(Y, M, D, h, m, s, ms) : + length >= 6 ? new NativeDate(Y, M, D, h, m, s) : + length >= 5 ? new NativeDate(Y, M, D, h, m) : + length >= 4 ? new NativeDate(Y, M, D, h) : + length >= 3 ? new NativeDate(Y, M, D) : + length >= 2 ? new NativeDate(Y, M) : + length >= 1 ? new NativeDate(Y) : + new NativeDate(); + // Prevent mixups with unfixed Date object + date.constructor = Date; + return date; + } + return NativeDate.apply(this, arguments); + }; + + // 15.9.1.15 Date Time String Format. + var isoDateExpression = new RegExp("^" + + "(\\d{4}|[\+\-]\\d{6})" + // four-digit year capture or sign + 6-digit extended year + "(?:-(\\d{2})" + // optional month capture + "(?:-(\\d{2})" + // optional day capture + "(?:" + // capture hours:minutes:seconds.milliseconds + "T(\\d{2})" + // hours capture + ":(\\d{2})" + // minutes capture + "(?:" + // optional :seconds.milliseconds + ":(\\d{2})" + // seconds capture + "(?:\\.(\\d{3}))?" + // milliseconds capture + ")?" + + "(?:" + // capture UTC offset component + "Z|" + // UTC capture + "(?:" + // offset specifier +/-hours:minutes + "([-+])" + // sign capture + "(\\d{2})" + // hours offset capture + ":(\\d{2})" + // minutes offset capture + ")" + + ")?)?)?)?" + + "$"); + + // Copy any custom methods a 3rd party library may have added + for (var key in NativeDate) + Date[key] = NativeDate[key]; + + // Copy "native" methods explicitly; they may be non-enumerable + Date.now = NativeDate.now; + Date.UTC = NativeDate.UTC; + Date.prototype = NativeDate.prototype; + Date.prototype.constructor = Date; + + // Upgrade Date.parse to handle simplified ISO 8601 strings + Date.parse = function parse(string) { + var match = isoDateExpression.exec(string); + if (match) { + match.shift(); // kill match[0], the full match + // parse months, days, hours, minutes, seconds, and milliseconds + for (var i = 1; i < 7; i++) { + // provide default values if necessary + match[i] = +(match[i] || (i < 3 ? 1 : 0)); + // match[1] is the month. Months are 0-11 in JavaScript + // `Date` objects, but 1-12 in ISO notation, so we + // decrement. + if (i == 1) + match[i]--; + } + + // parse the UTC offset component + var minuteOffset = +match.pop(), hourOffset = +match.pop(), sign = match.pop(); + + // compute the explicit time zone offset if specified + var offset = 0; + if (sign) { + // detect invalid offsets and return early + if (hourOffset > 23 || minuteOffset > 59) + return NaN; + + // express the provided time zone offset in minutes. The offset is + // negative for time zones west of UTC; positive otherwise. + offset = (hourOffset * 60 + minuteOffset) * 6e4 * (sign == "+" ? -1 : 1); + } + + // Date.UTC for years between 0 and 99 converts year to 1900 + year + // The Gregorian calendar has a 400-year cycle, so + // to Date.UTC(year + 400, .... ) - 12622780800000 == Date.UTC(year, ...), + // where 12622780800000 - number of milliseconds in Gregorian calendar 400 years + var year = +match[0]; + if (0 <= year && year <= 99) { + match[0] = year + 400; + return NativeDate.UTC.apply(this, match) + offset - 12622780800000; + } + + // compute a new UTC date value, accounting for the optional offset + return NativeDate.UTC.apply(this, match) + offset; + } + return NativeDate.parse.apply(this, arguments); + }; + + return Date; + })(Date); +} + +// +// String +// ====== +// + +// ES5 15.5.4.20 +// http://es5.github.com/#x15.5.4.20 +var ws = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003" + + "\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028" + + "\u2029\uFEFF"; +if (!String.prototype.trim || ws.trim()) { + // http://blog.stevenlevithan.com/archives/faster-trim-javascript + // http://perfectionkills.com/whitespace-deviations/ + ws = "[" + ws + "]"; + var trimBeginRegexp = new RegExp("^" + ws + ws + "*"), + trimEndRegexp = new RegExp(ws + ws + "*$"); + String.prototype.trim = function trim() { + return String(this).replace(trimBeginRegexp, "").replace(trimEndRegexp, ""); + }; +} + +// +// Util +// ====== +// + +// ES5 9.4 +// http://es5.github.com/#x9.4 +// http://jsperf.com/to-integer +var toInteger = function (n) { + n = +n; + if (n !== n) // isNaN + n = 0; + else if (n !== 0 && n !== (1/0) && n !== -(1/0)) + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + return n; +}; + +var prepareString = "a"[0] != "a", + // ES5 9.9 + // http://es5.github.com/#x9.9 + toObject = function (o) { + if (o == null) { // this matches both null and undefined + throw new TypeError(); // TODO message + } + // If the implementation doesn't support by-index access of + // string characters (ex. IE < 7), split the string + if (prepareString && typeof o == "string" && o) { + return o.split(""); + } + return Object(o); + }; +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * Mihai Sucan + * Irakli Gozalishvili (http://jeditoolkit.com) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/lib/dom', ['require', 'exports', 'module' ], function(require, exports, module) { +"use strict"; + +var XHTML_NS = "http://www.w3.org/1999/xhtml"; + +exports.createElement = function(tag, ns) { + return document.createElementNS ? + document.createElementNS(ns || XHTML_NS, tag) : + document.createElement(tag); +}; + +exports.setText = function(elem, text) { + if (elem.innerText !== undefined) { + elem.innerText = text; + } + if (elem.textContent !== undefined) { + elem.textContent = text; + } +}; + +exports.hasCssClass = function(el, name) { + var classes = el.className.split(/\s+/g); + return classes.indexOf(name) !== -1; +}; + +/* +* Add a CSS class to the list of classes on the given node +*/ +exports.addCssClass = function(el, name) { + if (!exports.hasCssClass(el, name)) { + el.className += " " + name; + } +}; + +/* +* Remove a CSS class from the list of classes on the given node +*/ +exports.removeCssClass = function(el, name) { + var classes = el.className.split(/\s+/g); + while (true) { + var index = classes.indexOf(name); + if (index == -1) { + break; + } + classes.splice(index, 1); + } + el.className = classes.join(" "); +}; + +exports.toggleCssClass = function(el, name) { + var classes = el.className.split(/\s+/g), add = true; + while (true) { + var index = classes.indexOf(name); + if (index == -1) { + break; + } + add = false; + classes.splice(index, 1); + } + if(add) + classes.push(name); + + el.className = classes.join(" "); + return add; +}; + +/* + * Add or remove a CSS class from the list of classes on the given node + * depending on the value of include + */ +exports.setCssClass = function(node, className, include) { + if (include) { + exports.addCssClass(node, className); + } else { + exports.removeCssClass(node, className); + } +}; + +exports.hasCssString = function(id, doc) { + var index = 0, sheets; + doc = doc || document; + + if (doc.createStyleSheet && (sheets = doc.styleSheets)) { + while (index < sheets.length) + if (sheets[index++].owningElement.id === id) return true; + } else if ((sheets = doc.getElementsByTagName("style"))) { + while (index < sheets.length) + if (sheets[index++].id === id) return true; + } + + return false; +}; + +exports.importCssString = function importCssString(cssText, id, doc) { + doc = doc || document; + // If style is already imported return immediately. + if (id && exports.hasCssString(id, doc)) + return null; + + var style; + + if (doc.createStyleSheet) { + style = doc.createStyleSheet(); + style.cssText = cssText; + if (id) + style.owningElement.id = id; + } else { + style = doc.createElementNS + ? doc.createElementNS(XHTML_NS, "style") + : doc.createElement("style"); + + style.appendChild(doc.createTextNode(cssText)); + if (id) + style.id = id; + + var head = doc.getElementsByTagName("head")[0] || doc.documentElement; + head.appendChild(style); + } +}; + +exports.importCssStylsheet = function(uri, doc) { + if (doc.createStyleSheet) { + doc.createStyleSheet(uri); + } else { + var link = exports.createElement('link'); + link.rel = 'stylesheet'; + link.href = uri; + + var head = doc.getElementsByTagName("head")[0] || doc.documentElement; + head.appendChild(link); + } +}; + +exports.getInnerWidth = function(element) { + return ( + parseInt(exports.computedStyle(element, "paddingLeft"), 10) + + parseInt(exports.computedStyle(element, "paddingRight"), 10) + + element.clientWidth + ); +}; + +exports.getInnerHeight = function(element) { + return ( + parseInt(exports.computedStyle(element, "paddingTop"), 10) + + parseInt(exports.computedStyle(element, "paddingBottom"), 10) + + element.clientHeight + ); +}; + +if (window.pageYOffset !== undefined) { + exports.getPageScrollTop = function() { + return window.pageYOffset; + }; + + exports.getPageScrollLeft = function() { + return window.pageXOffset; + }; +} +else { + exports.getPageScrollTop = function() { + return document.body.scrollTop; + }; + + exports.getPageScrollLeft = function() { + return document.body.scrollLeft; + }; +} + +if (window.getComputedStyle) + exports.computedStyle = function(element, style) { + if (style) + return (window.getComputedStyle(element, "") || {})[style] || ""; + return window.getComputedStyle(element, "") || {}; + }; +else + exports.computedStyle = function(element, style) { + if (style) + return element.currentStyle[style]; + return element.currentStyle; + }; + +exports.scrollbarWidth = function(document) { + + var inner = exports.createElement("p"); + inner.style.width = "100%"; + inner.style.minWidth = "0px"; + inner.style.height = "200px"; + + var outer = exports.createElement("div"); + var style = outer.style; + + style.position = "absolute"; + style.left = "-10000px"; + style.overflow = "hidden"; + style.width = "200px"; + style.minWidth = "0px"; + style.height = "150px"; + + outer.appendChild(inner); + + var body = document.body || document.documentElement; + body.appendChild(outer); + + var noScrollbar = inner.offsetWidth; + + style.overflow = "scroll"; + var withScrollbar = inner.offsetWidth; + + if (noScrollbar == withScrollbar) { + withScrollbar = outer.clientWidth; + } + + body.removeChild(outer); + + return noScrollbar-withScrollbar; +}; + +/* + * Optimized set innerHTML. This is faster than plain innerHTML if the element + * already contains a lot of child elements. + * + * See http://blog.stevenlevithan.com/archives/faster-than-innerhtml for details + */ +exports.setInnerHtml = function(el, innerHtml) { + var element = el.cloneNode(false);//document.createElement("div"); + element.innerHTML = innerHtml; + el.parentNode.replaceChild(element, el); + return element; +}; + +exports.setInnerText = function(el, innerText) { + var document = el.ownerDocument; + if (document.body && "textContent" in document.body) + el.textContent = innerText; + else + el.innerText = innerText; + +}; + +exports.getInnerText = function(el) { + var document = el.ownerDocument; + if (document.body && "textContent" in document.body) + return el.textContent; + else + return el.innerText || el.textContent || ""; +}; + +exports.getParentWindow = function(document) { + return document.defaultView || document.parentWindow; +}; + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/lib/event', ['require', 'exports', 'module' , 'ace/lib/keys', 'ace/lib/useragent', 'ace/lib/dom'], function(require, exports, module) { +"use strict"; + +var keys = require("./keys"); +var useragent = require("./useragent"); +var dom = require("./dom"); + +exports.addListener = function(elem, type, callback) { + if (elem.addEventListener) { + return elem.addEventListener(type, callback, false); + } + if (elem.attachEvent) { + var wrapper = function() { + callback(window.event); + }; + callback._wrapper = wrapper; + elem.attachEvent("on" + type, wrapper); + } +}; + +exports.removeListener = function(elem, type, callback) { + if (elem.removeEventListener) { + return elem.removeEventListener(type, callback, false); + } + if (elem.detachEvent) { + elem.detachEvent("on" + type, callback._wrapper || callback); + } +}; + +/* +* Prevents propagation and clobbers the default action of the passed event +*/ +exports.stopEvent = function(e) { + exports.stopPropagation(e); + exports.preventDefault(e); + return false; +}; + +exports.stopPropagation = function(e) { + if (e.stopPropagation) + e.stopPropagation(); + else + e.cancelBubble = true; +}; + +exports.preventDefault = function(e) { + if (e.preventDefault) + e.preventDefault(); + else + e.returnValue = false; +}; + +/* + * @return {Number} 0 for left button, 1 for middle button, 2 for right button + */ +exports.getButton = function(e) { + if (e.type == "dblclick") + return 0; + else if (e.type == "contextmenu") + return 2; + + // DOM Event + if (e.preventDefault) { + return e.button; + } + // old IE + else { + return {1:0, 2:2, 4:1}[e.button]; + } +}; + +if (document.documentElement.setCapture) { + exports.capture = function(el, eventHandler, releaseCaptureHandler) { + function onMouseMove(e) { + eventHandler(e); + return exports.stopPropagation(e); + } + + var called = false; + function onReleaseCapture(e) { + eventHandler(e); + + if (!called) { + called = true; + releaseCaptureHandler(e); + } + + exports.removeListener(el, "mousemove", eventHandler); + exports.removeListener(el, "mouseup", onReleaseCapture); + exports.removeListener(el, "losecapture", onReleaseCapture); + + el.releaseCapture(); + } + + exports.addListener(el, "mousemove", eventHandler); + exports.addListener(el, "mouseup", onReleaseCapture); + exports.addListener(el, "losecapture", onReleaseCapture); + el.setCapture(); + }; +} +else { + exports.capture = function(el, eventHandler, releaseCaptureHandler) { + function onMouseMove(e) { + eventHandler(e); + e.stopPropagation(); + } + + function onMouseUp(e) { + eventHandler && eventHandler(e); + releaseCaptureHandler && releaseCaptureHandler(e); + + document.removeEventListener("mousemove", onMouseMove, true); + document.removeEventListener("mouseup", onMouseUp, true); + + e.stopPropagation(); + } + + document.addEventListener("mousemove", onMouseMove, true); + document.addEventListener("mouseup", onMouseUp, true); + }; +} + +exports.addMouseWheelListener = function(el, callback) { + var factor = 8; + var listener = function(e) { + if (e.wheelDelta !== undefined) { + if (e.wheelDeltaX !== undefined) { + e.wheelX = -e.wheelDeltaX / factor; + e.wheelY = -e.wheelDeltaY / factor; + } else { + e.wheelX = 0; + e.wheelY = -e.wheelDelta / factor; + } + } + else { + if (e.axis && e.axis == e.HORIZONTAL_AXIS) { + e.wheelX = (e.detail || 0) * 5; + e.wheelY = 0; + } else { + e.wheelX = 0; + e.wheelY = (e.detail || 0) * 5; + } + } + callback(e); + }; + exports.addListener(el, "DOMMouseScroll", listener); + exports.addListener(el, "mousewheel", listener); +}; + +exports.addMultiMouseDownListener = function(el, button, count, timeout, callback) { + var clicks = 0; + var startX, startY; + + var listener = function(e) { + clicks += 1; + if (clicks == 1) { + startX = e.clientX; + startY = e.clientY; + + setTimeout(function() { + clicks = 0; + }, timeout || 600); + } + + var isButton = exports.getButton(e) == button; + if (!isButton || Math.abs(e.clientX - startX) > 5 || Math.abs(e.clientY - startY) > 5) + clicks = 0; + + if (clicks == count) { + clicks = 0; + callback(e); + } + + if (isButton) + return exports.preventDefault(e); + }; + + exports.addListener(el, "mousedown", listener); + useragent.isOldIE && exports.addListener(el, "dblclick", listener); +}; + +function normalizeCommandKeys(callback, e, keyCode) { + var hashId = 0; + if (useragent.isOpera && useragent.isMac) { + hashId = 0 | (e.metaKey ? 1 : 0) | (e.altKey ? 2 : 0) + | (e.shiftKey ? 4 : 0) | (e.ctrlKey ? 8 : 0); + } else { + hashId = 0 | (e.ctrlKey ? 1 : 0) | (e.altKey ? 2 : 0) + | (e.shiftKey ? 4 : 0) | (e.metaKey ? 8 : 0); + } + + if (keyCode in keys.MODIFIER_KEYS) { + switch (keys.MODIFIER_KEYS[keyCode]) { + case "Alt": + hashId = 2; + break; + case "Shift": + hashId = 4; + break; + case "Ctrl": + hashId = 1; + break; + default: + hashId = 8; + break; + } + keyCode = 0; + } + + if (hashId & 8 && (keyCode == 91 || keyCode == 93)) { + keyCode = 0; + } + + // If there is no hashID and the keyCode is not a function key, then + // we don't call the callback as we don't handle a command key here + // (it's a normal key/character input). + if (!hashId && !(keyCode in keys.FUNCTION_KEYS) && !(keyCode in keys.PRINTABLE_KEYS)) { + return false; + } + return callback(e, hashId, keyCode); +} + +exports.addCommandKeyListener = function(el, callback) { + var addListener = exports.addListener; + if (useragent.isOldGecko || useragent.isOpera) { + // Old versions of Gecko aka. Firefox < 4.0 didn't repeat the keydown + // event if the user pressed the key for a longer time. Instead, the + // keydown event was fired once and later on only the keypress event. + // To emulate the 'right' keydown behavior, the keyCode of the initial + // keyDown event is stored and in the following keypress events the + // stores keyCode is used to emulate a keyDown event. + var lastKeyDownKeyCode = null; + addListener(el, "keydown", function(e) { + lastKeyDownKeyCode = e.keyCode; + }); + addListener(el, "keypress", function(e) { + return normalizeCommandKeys(callback, e, lastKeyDownKeyCode); + }); + } else { + var lastDown = null; + + addListener(el, "keydown", function(e) { + lastDown = e.keyIdentifier || e.keyCode; + return normalizeCommandKeys(callback, e, e.keyCode); + }); + } +}; + +if (window.postMessage) { + var postMessageId = 1; + exports.nextTick = function(callback, win) { + win = win || window; + var messageName = "zero-timeout-message-" + postMessageId; + exports.addListener(win, "message", function listener(e) { + if (e.data == messageName) { + exports.stopPropagation(e); + exports.removeListener(win, "message", listener); + callback(); + } + }); + win.postMessage(messageName, "*"); + }; +} +else { + exports.nextTick = function(callback, win) { + win = win || window; + window.setTimeout(callback, 0); + }; +} + +}); +/*! @license +========================================================================== +SproutCore -- JavaScript Application Framework +copyright 2006-2009, Sprout Systems Inc., Apple Inc. and contributors. + +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. + +SproutCore and the SproutCore logo are trademarks of Sprout Systems, Inc. + +For more information about SproutCore, visit http://www.sproutcore.com + + +========================================================================== +@license */ + +// Most of the following code is taken from SproutCore with a few changes. + +ace.define('ace/lib/keys', ['require', 'exports', 'module' , 'ace/lib/oop'], function(require, exports, module) { +"use strict"; + +var oop = require("./oop"); + +/* + * Helper functions and hashes for key handling. + */ +var Keys = (function() { + var ret = { + MODIFIER_KEYS: { + 16: 'Shift', 17: 'Ctrl', 18: 'Alt', 224: 'Meta' + }, + + KEY_MODS: { + "ctrl": 1, "alt": 2, "option" : 2, + "shift": 4, "meta": 8, "command": 8 + }, + + FUNCTION_KEYS : { + 8 : "Backspace", + 9 : "Tab", + 13 : "Return", + 19 : "Pause", + 27 : "Esc", + 32 : "Space", + 33 : "PageUp", + 34 : "PageDown", + 35 : "End", + 36 : "Home", + 37 : "Left", + 38 : "Up", + 39 : "Right", + 40 : "Down", + 44 : "Print", + 45 : "Insert", + 46 : "Delete", + 96 : "Numpad0", + 97 : "Numpad1", + 98 : "Numpad2", + 99 : "Numpad3", + 100: "Numpad4", + 101: "Numpad5", + 102: "Numpad6", + 103: "Numpad7", + 104: "Numpad8", + 105: "Numpad9", + 112: "F1", + 113: "F2", + 114: "F3", + 115: "F4", + 116: "F5", + 117: "F6", + 118: "F7", + 119: "F8", + 120: "F9", + 121: "F10", + 122: "F11", + 123: "F12", + 144: "Numlock", + 145: "Scrolllock" + }, + + PRINTABLE_KEYS: { + 32: ' ', 48: '0', 49: '1', 50: '2', 51: '3', 52: '4', 53: '5', + 54: '6', 55: '7', 56: '8', 57: '9', 59: ';', 61: '=', 65: 'a', + 66: 'b', 67: 'c', 68: 'd', 69: 'e', 70: 'f', 71: 'g', 72: 'h', + 73: 'i', 74: 'j', 75: 'k', 76: 'l', 77: 'm', 78: 'n', 79: 'o', + 80: 'p', 81: 'q', 82: 'r', 83: 's', 84: 't', 85: 'u', 86: 'v', + 87: 'w', 88: 'x', 89: 'y', 90: 'z', 107: '+', 109: '-', 110: '.', + 188: ',', 190: '.', 191: '/', 192: '`', 219: '[', 220: '\\', + 221: ']', 222: '\"' + } + }; + + // A reverse map of FUNCTION_KEYS + for (var i in ret.FUNCTION_KEYS) { + var name = ret.FUNCTION_KEYS[i].toUpperCase(); + ret[name] = parseInt(i, 10); + } + + // Add the MODIFIER_KEYS, FUNCTION_KEYS and PRINTABLE_KEYS to the KEY + // variables as well. + oop.mixin(ret, ret.MODIFIER_KEYS); + oop.mixin(ret, ret.PRINTABLE_KEYS); + oop.mixin(ret, ret.FUNCTION_KEYS); + + return ret; +})(); +oop.mixin(exports, Keys); + +exports.keyCodeToString = function(keyCode) { + return (Keys[keyCode] || String.fromCharCode(keyCode)).toLowerCase(); +} + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/lib/oop', ['require', 'exports', 'module' ], function(require, exports, module) { +"use strict"; + +exports.inherits = (function() { + var tempCtor = function() {}; + return function(ctor, superCtor) { + tempCtor.prototype = superCtor.prototype; + ctor.super_ = superCtor.prototype; + ctor.prototype = new tempCtor(); + ctor.prototype.constructor = ctor; + }; +}()); + +exports.mixin = function(obj, mixin) { + for (var key in mixin) { + obj[key] = mixin[key]; + } +}; + +exports.implement = function(proto, mixin) { + exports.mixin(proto, mixin); +}; + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/lib/useragent', ['require', 'exports', 'module' ], function(require, exports, module) { +"use strict"; + +var os = (navigator.platform.match(/mac|win|linux/i) || ["other"])[0].toLowerCase(); +var ua = navigator.userAgent; + +// Is the user using a browser that identifies itself as Windows +exports.isWin = (os == "win"); + +// Is the user using a browser that identifies itself as Mac OS +exports.isMac = (os == "mac"); + +// Is the user using a browser that identifies itself as Linux +exports.isLinux = (os == "linux"); + +exports.isIE = + navigator.appName == "Microsoft Internet Explorer" + && parseFloat(navigator.userAgent.match(/MSIE ([0-9]+[\.0-9]+)/)[1]); + +exports.isOldIE = exports.isIE && exports.isIE < 9; + +// Is this Firefox or related? +exports.isGecko = exports.isMozilla = window.controllers && window.navigator.product === "Gecko"; + +// oldGecko == rev < 2.0 +exports.isOldGecko = exports.isGecko && parseInt((navigator.userAgent.match(/rv\:(\d+)/)||[])[1], 10) < 4; + +// Is this Opera +exports.isOpera = window.opera && Object.prototype.toString.call(window.opera) == "[object Opera]"; + +// Is the user using a browser that identifies itself as WebKit +exports.isWebKit = parseFloat(ua.split("WebKit/")[1]) || undefined; + +exports.isChrome = parseFloat(ua.split(" Chrome/")[1]) || undefined; + +exports.isAIR = ua.indexOf("AdobeAIR") >= 0; + +exports.isIPad = ua.indexOf("iPad") >= 0; + +exports.isTouchPad = ua.indexOf("TouchPad") >= 0; + +/* + * I hate doing this, but we need some way to determine if the user is on a Mac + * The reason is that users have different expectations of their key combinations. + * + * Take copy as an example, Mac people expect to use CMD or APPLE + C + * Windows folks expect to use CTRL + C + */ +exports.OS = { + LINUX: "LINUX", + MAC: "MAC", + WINDOWS: "WINDOWS" +}; + +/* + * Return an exports.OS constant + */ +exports.getOS = function() { + if (exports.isMac) { + return exports.OS.MAC; + } else if (exports.isLinux) { + return exports.OS.LINUX; + } else { + return exports.OS.WINDOWS; + } +}; + +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * Irakli Gozalishvili (http://jeditoolkit.com) + * Julian Viereck + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/editor', ['require', 'exports', 'module' , 'ace/lib/fixoldbrowsers', 'ace/lib/oop', 'ace/lib/lang', 'ace/lib/useragent', 'ace/keyboard/textinput', 'ace/mouse/mouse_handler', 'ace/mouse/fold_handler', 'ace/keyboard/keybinding', 'ace/edit_session', 'ace/search', 'ace/range', 'ace/lib/event_emitter', 'ace/commands/command_manager', 'ace/commands/default_commands'], function(require, exports, module) { +"use strict"; + +require("./lib/fixoldbrowsers"); + +var oop = require("./lib/oop"); +var lang = require("./lib/lang"); +var useragent = require("./lib/useragent"); +var TextInput = require("./keyboard/textinput").TextInput; +var MouseHandler = require("./mouse/mouse_handler").MouseHandler; +var FoldHandler = require("./mouse/fold_handler").FoldHandler; +//var TouchHandler = require("./touch_handler").TouchHandler; +var KeyBinding = require("./keyboard/keybinding").KeyBinding; +var EditSession = require("./edit_session").EditSession; +var Search = require("./search").Search; +var Range = require("./range").Range; +var EventEmitter = require("./lib/event_emitter").EventEmitter; +var CommandManager = require("./commands/command_manager").CommandManager; +var defaultCommands = require("./commands/default_commands").commands; + +/** + * class Editor + * + * The main entry point into the Ace functionality. The `Editor` manages the `EditSession` (which manages `Document`s), as well as the `VirtualRenderer`, which draws everything to the screen. Event sessions dealing with the mouse and keyboard are bubbled up from `Document` to the `Editor`, which decides what to do with them. + * + **/ + +/** + * new Editor(renderer, session) + * - renderer (VirtualRenderer): Associated `VirtualRenderer` that draws everything + * - session (EditSession): The `EditSession` to refer to + * + * Creates a new `Editor` object. + * + **/ +var Editor = function(renderer, session) { + var container = renderer.getContainerElement(); + this.container = container; + this.renderer = renderer; + + this.commands = new CommandManager(useragent.isMac ? "mac" : "win", defaultCommands); + this.textInput = new TextInput(renderer.getTextAreaContainer(), this); + this.renderer.textarea = this.textInput.getElement(); + this.keyBinding = new KeyBinding(this); + + // TODO detect touch event support + if (useragent.isIPad) { + //this.$mouseHandler = new TouchHandler(this); + } else { + this.$mouseHandler = new MouseHandler(this); + new FoldHandler(this); + } + + this.$blockScrolling = 0; + this.$search = new Search().set({ + wrap: true + }); + + this.setSession(session || new EditSession("")); +}; + +(function(){ + + oop.implement(this, EventEmitter); + + /** + * Editor.setKeyboardHandler(keyboardHandler) + * + * Sets a new keyboard handler. + **/ + this.setKeyboardHandler = function(keyboardHandler) { + this.keyBinding.setKeyboardHandler(keyboardHandler); + }; + + /** related to: KeyBinding + * Editor.getKeyboardHandler() -> String + * + * Returns the keyboard handler. + **/ + this.getKeyboardHandler = function() { + return this.keyBinding.getKeyboardHandler(); + }; + + /** + * Editor.setSession(session) + * - session (EditSession): The new session to use + * + * Sets a new editsession to use. This method also emits the `'changeSession'` event. + **/ + this.setSession = function(session) { + if (this.session == session) + return; + + if (this.session) { + var oldSession = this.session; + this.session.removeEventListener("change", this.$onDocumentChange); + this.session.removeEventListener("changeMode", this.$onChangeMode); + this.session.removeEventListener("tokenizerUpdate", this.$onTokenizerUpdate); + this.session.removeEventListener("changeTabSize", this.$onChangeTabSize); + this.session.removeEventListener("changeWrapLimit", this.$onChangeWrapLimit); + this.session.removeEventListener("changeWrapMode", this.$onChangeWrapMode); + this.session.removeEventListener("onChangeFold", this.$onChangeFold); + this.session.removeEventListener("changeFrontMarker", this.$onChangeFrontMarker); + this.session.removeEventListener("changeBackMarker", this.$onChangeBackMarker); + this.session.removeEventListener("changeBreakpoint", this.$onChangeBreakpoint); + this.session.removeEventListener("changeAnnotation", this.$onChangeAnnotation); + this.session.removeEventListener("changeOverwrite", this.$onCursorChange); + this.session.removeEventListener("changeScrollTop", this.$onScrollTopChange); + this.session.removeEventListener("changeLeftTop", this.$onScrollLeftChange); + + var selection = this.session.getSelection(); + selection.removeEventListener("changeCursor", this.$onCursorChange); + selection.removeEventListener("changeSelection", this.$onSelectionChange); + } + + this.session = session; + + this.$onDocumentChange = this.onDocumentChange.bind(this); + session.addEventListener("change", this.$onDocumentChange); + this.renderer.setSession(session); + + this.$onChangeMode = this.onChangeMode.bind(this); + session.addEventListener("changeMode", this.$onChangeMode); + + this.$onTokenizerUpdate = this.onTokenizerUpdate.bind(this); + session.addEventListener("tokenizerUpdate", this.$onTokenizerUpdate); + + this.$onChangeTabSize = this.renderer.updateText.bind(this.renderer); + session.addEventListener("changeTabSize", this.$onChangeTabSize); + + this.$onChangeWrapLimit = this.onChangeWrapLimit.bind(this); + session.addEventListener("changeWrapLimit", this.$onChangeWrapLimit); + + this.$onChangeWrapMode = this.onChangeWrapMode.bind(this); + session.addEventListener("changeWrapMode", this.$onChangeWrapMode); + + this.$onChangeFold = this.onChangeFold.bind(this); + session.addEventListener("changeFold", this.$onChangeFold); + + this.$onChangeFrontMarker = this.onChangeFrontMarker.bind(this); + this.session.addEventListener("changeFrontMarker", this.$onChangeFrontMarker); + + this.$onChangeBackMarker = this.onChangeBackMarker.bind(this); + this.session.addEventListener("changeBackMarker", this.$onChangeBackMarker); + + this.$onChangeBreakpoint = this.onChangeBreakpoint.bind(this); + this.session.addEventListener("changeBreakpoint", this.$onChangeBreakpoint); + + this.$onChangeAnnotation = this.onChangeAnnotation.bind(this); + this.session.addEventListener("changeAnnotation", this.$onChangeAnnotation); + + this.$onCursorChange = this.onCursorChange.bind(this); + this.session.addEventListener("changeOverwrite", this.$onCursorChange); + + this.$onScrollTopChange = this.onScrollTopChange.bind(this); + this.session.addEventListener("changeScrollTop", this.$onScrollTopChange); + + this.$onScrollLeftChange = this.onScrollLeftChange.bind(this); + this.session.addEventListener("changeScrollLeft", this.$onScrollLeftChange); + + this.selection = session.getSelection(); + this.selection.addEventListener("changeCursor", this.$onCursorChange); + + this.$onSelectionChange = this.onSelectionChange.bind(this); + this.selection.addEventListener("changeSelection", this.$onSelectionChange); + + this.onChangeMode(); + + this.$blockScrolling += 1; + this.onCursorChange(); + this.$blockScrolling -= 1; + + this.onScrollTopChange(); + this.onScrollLeftChange(); + this.onSelectionChange(); + this.onChangeFrontMarker(); + this.onChangeBackMarker(); + this.onChangeBreakpoint(); + this.onChangeAnnotation(); + this.session.getUseWrapMode() && this.renderer.adjustWrapLimit(); + this.renderer.updateFull(); + + this._emit("changeSession", { + session: session, + oldSession: oldSession + }); + }; + + /** + * Editor.getSession() -> EditSession + * + * Returns the current session being used. + **/ + this.getSession = function() { + return this.session; + }; + + /** + * Editor.getSelection() -> String + * + * Returns the currently highlighted selection. + **/ + this.getSelection = function() { + return this.selection; + }; + + /** related to: VirtualRenderer.onResize + * Editor.resize() + * + * {:VirtualRenderer.onResize} + **/ + this.resize = function() { + this.renderer.onResize(); + }; + + /** + * Editor.setTheme(theme) + * + * {:VirtualRenderer.setTheme} + **/ + this.setTheme = function(theme) { + this.renderer.setTheme(theme); + }; + + /** related to: VirtualRenderer.getTheme + * Editor.getTheme() -> String + * + * {:VirtualRenderer.getTheme} + **/ + this.getTheme = function() { + return this.renderer.getTheme(); + }; + + /** related to: VirtualRenderer.setStyle + * Editor.setStyle(style) + * + * {:VirtualRenderer.setStyle} + **/ + this.setStyle = function(style) { + this.renderer.setStyle(style); + }; + + /** related to: VirtualRenderer.unsetStyle + * Editor.unsetStyle(style) + * + * {:VirtualRenderer.unsetStyle} + **/ + this.unsetStyle = function(style) { + this.renderer.unsetStyle(style); + }; + + /** + * Editor.setFontSize(size) + * - size (Number): A font size + * + * Set a new font size (in pixels) for the editor text. + **/ + this.setFontSize = function(size) { + this.container.style.fontSize = size; + this.renderer.updateFontSize(); + }; + + /** internal, hide + * Editor.$highlightBrackets() + * + **/ + this.$highlightBrackets = function() { + if (this.session.$bracketHighlight) { + this.session.removeMarker(this.session.$bracketHighlight); + this.session.$bracketHighlight = null; + } + + if (this.$highlightPending) { + return; + } + + // perform highlight async to not block the browser during navigation + var self = this; + this.$highlightPending = true; + setTimeout(function() { + self.$highlightPending = false; + + var pos = self.session.findMatchingBracket(self.getCursorPosition()); + if (pos) { + var range = new Range(pos.row, pos.column, pos.row, pos.column+1); + self.session.$bracketHighlight = self.session.addMarker(range, "ace_bracket", "text"); + } + }, 10); + }; + + /** + * Editor.focus() + * + * Brings the current `textInput` into focus. + **/ + this.focus = function() { + // Safari needs the timeout + // iOS and Firefox need it called immediately + // to be on the save side we do both + var _self = this; + setTimeout(function() { + _self.textInput.focus(); + }); + this.textInput.focus(); + }; + + /** + * Editor.isFocused() -> Boolean + * + * Returns true if the current `textInput` is in focus. + **/ + this.isFocused = function() { + return this.textInput.isFocused(); + }; + + /** + * Editor.blur() + * + * Blurs the current `textInput`. + **/ + this.blur = function() { + this.textInput.blur(); + }; + + /** + * Editor@onFocus() + * + * Emitted once the editor comes into focus. + **/ + this.onFocus = function() { + this.renderer.showCursor(); + this.renderer.visualizeFocus(); + this._emit("focus"); + }; + + /** + * Editor@onBlur() + * + * Emitted once the editor has been blurred. + **/ + this.onBlur = function() { + this.renderer.hideCursor(); + this.renderer.visualizeBlur(); + this._emit("blur"); + }; + + this.$cursorChange = function() { + this.renderer.updateCursor(); + }; + + /** + * Editor@onDocumentChange(e) + * - e (Object): Contains a single property, `data`, which has the delta of changes + * + * Emitted whenever the document is changed. + * + **/ + this.onDocumentChange = function(e) { + var delta = e.data; + var range = delta.range; + var lastRow; + + if (range.start.row == range.end.row && delta.action != "insertLines" && delta.action != "removeLines") + lastRow = range.end.row; + else + lastRow = Infinity; + this.renderer.updateLines(range.start.row, lastRow); + + this._emit("change", e); + + // update cursor because tab characters can influence the cursor position + this.$cursorChange(); + }; + + /** + * Editor@onTokenizerUpdate(e) + * - e (Object): Contains a single property, `data`, which indicates the changed rows + * + * Emitted when the a tokenizer is updated. + **/ + this.onTokenizerUpdate = function(e) { + var rows = e.data; + this.renderer.updateLines(rows.first, rows.last); + }; + + /** + * Editor@onScrollTopChange() + * + * Emitted when the scroll top changes. + **/ + this.onScrollTopChange = function() { + this.renderer.scrollToY(this.session.getScrollTop()); + }; + + /** + * Editor@onScrollLeftChange() + * + * Emitted when the scroll left changes. + **/ + this.onScrollLeftChange = function() { + this.renderer.scrollToX(this.session.getScrollLeft()); + }; + + /** + * Editor@onCursorChange() + * + * Emitted when the cursor changes. + **/ + this.onCursorChange = function() { + this.$cursorChange(); + + if (!this.$blockScrolling) { + this.renderer.scrollCursorIntoView(); + } + + this.$highlightBrackets(); + this.$updateHighlightActiveLine(); + }; + + /** internal, hide + * Editor.$updateHighlightActiveLine() + * + * + **/ + this.$updateHighlightActiveLine = function() { + var session = this.getSession(); + + if (session.$highlightLineMarker) + session.removeMarker(session.$highlightLineMarker); + + session.$highlightLineMarker = null; + + if (this.$highlightActiveLine) { + var cursor = this.getCursorPosition(); + var foldLine = this.session.getFoldLine(cursor.row); + + if ((this.getSelectionStyle() != "line" || !this.selection.isMultiLine())) { + var range; + if (foldLine) { + range = new Range(foldLine.start.row, 0, foldLine.end.row + 1, 0); + } else { + range = new Range(cursor.row, 0, cursor.row+1, 0); + } + session.$highlightLineMarker = session.addMarker(range, "ace_active_line", "background"); + } + } + }; + + + /** + * Editor@onSelectionChange(e) + * - e (Object): Contains a single property, `data`, which has the delta of changes + * + * Emitted when a selection has changed. + **/ + this.onSelectionChange = function(e) { + var session = this.getSession(); + + if (session.$selectionMarker) { + session.removeMarker(session.$selectionMarker); + } + session.$selectionMarker = null; + + if (!this.selection.isEmpty()) { + var range = this.selection.getRange(); + var style = this.getSelectionStyle(); + session.$selectionMarker = session.addMarker(range, "ace_selection", style); + } else { + this.$updateHighlightActiveLine(); + } + + if (this.$highlightSelectedWord) + this.session.getMode().highlightSelection(this); + }; + + /** + * Editor@onChangeFrontMarker() + * + * Emitted when a front marker changes. + **/ + this.onChangeFrontMarker = function() { + this.renderer.updateFrontMarkers(); + }; + + /** + * Editor@onChangeBackMarker() + * + * Emitted when a back marker changes. + **/ + this.onChangeBackMarker = function() { + this.renderer.updateBackMarkers(); + }; + + /** + * Editor@onChangeBreakpoint() + * + * Emitted when a breakpoint changes. + **/ + this.onChangeBreakpoint = function() { + this.renderer.setBreakpoints(this.session.getBreakpoints()); + }; + + /** + * Editor@onChangeAnnotation() + * + * Emitted when an annotation changes. + **/ + this.onChangeAnnotation = function() { + this.renderer.setAnnotations(this.session.getAnnotations()); + }; + + /** + * Editor@onChangeMode() + * + * Emitted when the mode changes. + **/ + this.onChangeMode = function() { + this.renderer.updateText(); + }; + + /** + * Editor@onChangeWrapLimit() + * + * Emitted when the wrap limit changes. + **/ + this.onChangeWrapLimit = function() { + this.renderer.updateFull(); + }; + + /** + * Editor@onChangeWrapMode() + * + * Emitted when the wrap mode changes. + **/ + this.onChangeWrapMode = function() { + this.renderer.onResize(true); + }; + + /** + * Editor@onChangeFold() + * + * Emitted when the code folds change. + **/ + this.onChangeFold = function() { + // Update the active line marker as due to folding changes the current + // line range on the screen might have changed. + this.$updateHighlightActiveLine(); + // TODO: This might be too much updating. Okay for now. + this.renderer.updateFull(); + }; + + /** + * Editor.getCopyText() -> String + * + * Returns the string of text currently highlighted. + **/ + this.getCopyText = function() { + var text = ""; + if (!this.selection.isEmpty()) + text = this.session.getTextRange(this.getSelectionRange()); + + this._emit("copy", text); + return text; + }; + + /** + * Editor.onCopy() + * + * Called whenever a text "copy" happens. + **/ + this.onCopy = function() { + this.commands.exec("copy", this); + }; + + /** + * Editor.onCut() + * + * called whenever a text "cut" happens. + **/ + this.onCut = function() { + this.commands.exec("cut", this); + }; + + /** + * Editor.onPaste() + * + * called whenever a text "paste" happens. + **/ + this.onPaste = function(text) { + this._emit("paste", text); + this.insert(text); + }; + + /** + * Editor.insert(text) + * - text (String): The new text to add + * + * Inserts `text` into wherever the cursor is pointing. + **/ + this.insert = function(text) { + var session = this.session; + var mode = session.getMode(); + + var cursor = this.getCursorPosition(); + + if (this.getBehavioursEnabled()) { + // Get a transform if the current mode wants one. + var transform = mode.transformAction(session.getState(cursor.row), 'insertion', this, session, text); + if (transform) + text = transform.text; + } + + text = text.replace("\t", this.session.getTabString()); + + // remove selected text + if (!this.selection.isEmpty()) { + cursor = this.session.remove(this.getSelectionRange()); + this.clearSelection(); + } + else if (this.session.getOverwrite()) { + var range = new Range.fromPoints(cursor, cursor); + range.end.column += text.length; + this.session.remove(range); + } + + this.clearSelection(); + + var start = cursor.column; + var lineState = session.getState(cursor.row); + var shouldOutdent = mode.checkOutdent(lineState, session.getLine(cursor.row), text); + var line = session.getLine(cursor.row); + var lineIndent = mode.getNextLineIndent(lineState, line.slice(0, cursor.column), session.getTabString()); + var end = session.insert(cursor, text); + + if (transform && transform.selection) { + if (transform.selection.length == 2) { // Transform relative to the current column + this.selection.setSelectionRange( + new Range(cursor.row, start + transform.selection[0], + cursor.row, start + transform.selection[1])); + } else { // Transform relative to the current row. + this.selection.setSelectionRange( + new Range(cursor.row + transform.selection[0], + transform.selection[1], + cursor.row + transform.selection[2], + transform.selection[3])); + } + } + + var lineState = session.getState(cursor.row); + + // TODO disabled multiline auto indent + // possibly doing the indent before inserting the text + // if (cursor.row !== end.row) { + if (session.getDocument().isNewLine(text)) { + this.moveCursorTo(cursor.row+1, 0); + + var size = session.getTabSize(); + var minIndent = Number.MAX_VALUE; + + for (var row = cursor.row + 1; row <= end.row; ++row) { + var indent = 0; + + line = session.getLine(row); + for (var i = 0; i < line.length; ++i) + if (line.charAt(i) == '\t') + indent += size; + else if (line.charAt(i) == ' ') + indent += 1; + else + break; + if (/[^\s]/.test(line)) + minIndent = Math.min(indent, minIndent); + } + + for (var row = cursor.row + 1; row <= end.row; ++row) { + var outdent = minIndent; + + line = session.getLine(row); + for (var i = 0; i < line.length && outdent > 0; ++i) + if (line.charAt(i) == '\t') + outdent -= size; + else if (line.charAt(i) == ' ') + outdent -= 1; + session.remove(new Range(row, 0, row, i)); + } + session.indentRows(cursor.row + 1, end.row, lineIndent); + } + if (shouldOutdent) + mode.autoOutdent(lineState, session, cursor.row); + }; + + /** + * Editor@onTextInput(text, pasted) + * - text (String): The text entered + * - pasted (Boolean): Identifies whether the text was pasted (`true`) or not + * + * Emitted when text is entered. + **/ + this.onTextInput = function(text) { + this.keyBinding.onTextInput(text); + }; + + /** + * Editor@onCommandKey(e, hashId, keyCode) + * + * Emitted when the command-key is pressed. + **/ + this.onCommandKey = function(e, hashId, keyCode) { + this.keyBinding.onCommandKey(e, hashId, keyCode); + }; + + /** related to: EditSession.setOverwrite + * Editor.setOverwrite(overwrite) + * - overwrite (Boolean): Defines wheter or not to set overwrites + * + * Pass in `true` to enable overwrites in your session, or `false` to disable. If overwrites is enabled, any text you enter will type over any text after it. If the value of `overwrite` changes, this function also emites the `changeOverwrite` event. + * + **/ + this.setOverwrite = function(overwrite) { + this.session.setOverwrite(overwrite); + }; + + /** related to: EditSession.getOverwrite + * Editor.getOverwrite() -> Boolean + * + * Returns `true` if overwrites are enabled; `false` otherwise. + **/ + this.getOverwrite = function() { + return this.session.getOverwrite(); + }; + + /** related to: EditSession.toggleOverwrite + * Editor.toggleOverwrite() + * + * Sets the value of overwrite to the opposite of whatever it currently is. + **/ + this.toggleOverwrite = function() { + this.session.toggleOverwrite(); + }; + + /** + * Editor.setScrollSpeed(speed) + * - speed (Number): A value indicating the new speed + * + * Sets how fast the mouse scrolling should do. + * + **/ + this.setScrollSpeed = function(speed) { + this.$mouseHandler.setScrollSpeed(speed); + }; + + /** + * Editor.getScrollSpeed() -> Number + * + * Returns the value indicating how fast the mouse scroll speed is. + **/ + this.getScrollSpeed = function() { + return this.$mouseHandler.getScrollSpeed(); + }; + + /** + * Editor.setDragDelay(dragDelay) + * - dragDelay (Number): A value indicating the new delay + * + * Sets the delay (in milliseconds) of the mouse drag. + * + **/ + this.setDragDelay = function(dragDelay) { + this.$mouseHandler.setDragDelay(dragDelay); + }; + + /** + * Editor.getDragDelay() -> Number + * + * Returns the current mouse drag delay. + **/ + this.getDragDelay = function() { + return this.$mouseHandler.getDragDelay(); + }; + + this.$selectionStyle = "line"; + /** + * Editor.setSelectionStyle(style) + * - style (String): The new selection style + * + * Indicates how selections should occur. By default, selections are set to "line". This function also emits the `'changeSelectionStyle'` event. + * + **/ + this.setSelectionStyle = function(style) { + if (this.$selectionStyle == style) return; + + this.$selectionStyle = style; + this.onSelectionChange(); + this._emit("changeSelectionStyle", {data: style}); + }; + + /** + * Editor.getSelectionStyle() -> String + * + * Returns the current selection style. + **/ + this.getSelectionStyle = function() { + return this.$selectionStyle; + }; + + this.$highlightActiveLine = true; + + /** + * Editor.setHighlightActiveLine(shouldHighlight) + * - shouldHighlight (Boolean): Set to `true` to highlight the current line + * + * Determines whether or not the current line should be highlighted. + * + **/ + this.setHighlightActiveLine = function(shouldHighlight) { + if (this.$highlightActiveLine == shouldHighlight) + return; + + this.$highlightActiveLine = shouldHighlight; + this.$updateHighlightActiveLine(); + }; + + /** + * Editor.getHighlightActiveLine() -> Boolean + * + * Returns `true` if current lines are always highlighted. + **/ + this.getHighlightActiveLine = function() { + return this.$highlightActiveLine; + }; + + this.$highlightGutterLine = true; + this.setHighlightGutterLine = function(shouldHighlight) { + if (this.$highlightGutterLine == shouldHighlight) + return; + + this.renderer.setHighlightGutterLine(shouldHighlight); + }; + + this.getHighlightGutterLine = function() { + return this.$highlightGutterLine; + }; + + this.$highlightSelectedWord = true; + /** + * Editor.setHighlightSelectedWord(shouldHighlight) + * - shouldHighlight (Boolean): Set to `true` to highlight the currently selected word + * + * Determines if the currently selected word should be highlighted. + **/ + this.setHighlightSelectedWord = function(shouldHighlight) { + if (this.$highlightSelectedWord == shouldHighlight) + return; + + this.$highlightSelectedWord = shouldHighlight; + if (shouldHighlight) + this.session.getMode().highlightSelection(this); + else + this.session.getMode().clearSelectionHighlight(this); + }; + + /** + * Editor.getHighlightSelectedWord() -> Boolean + * + * Returns `true` if currently highlighted words are to be highlighted. + **/ + this.getHighlightSelectedWord = function() { + return this.$highlightSelectedWord; + }; + + this.setAnimatedScroll = function(shouldAnimate){ + this.renderer.setAnimatedScroll(shouldAnimate); + }; + + this.getAnimatedScroll = function(){ + return this.renderer.getAnimatedScroll(); + }; + + /** + * Editor.setShowInvisibles(showInvisibles) + * - showInvisibles (Boolean): Specifies whether or not to show invisible characters + * + * If `showInvisibiles` is set to `true`, invisible characters—like spaces or new lines—are show in the editor. + **/ + this.setShowInvisibles = function(showInvisibles) { + if (this.getShowInvisibles() == showInvisibles) + return; + + this.renderer.setShowInvisibles(showInvisibles); + }; + + /** + * Editor.getShowInvisibles() -> Boolean + * + * Returns `true` if invisible characters are being shown. + **/ + this.getShowInvisibles = function() { + return this.renderer.getShowInvisibles(); + }; + + /** + * Editor.setShowPrintMargin(showPrintMargin) + * - showPrintMargin (Boolean): Specifies whether or not to show the print margin + * + * If `showPrintMargin` is set to `true`, the print margin is shown in the editor. + **/ + this.setShowPrintMargin = function(showPrintMargin) { + this.renderer.setShowPrintMargin(showPrintMargin); + }; + + /** + * Editor.getShowPrintMargin() -> Boolean + * + * Returns `true` if the print margin is being shown. + **/ + this.getShowPrintMargin = function() { + return this.renderer.getShowPrintMargin(); + }; + + /** + * Editor.setPrintMarginColumn(showPrintMargin) + * - showPrintMargin (Number): Specifies the new print margin + * + * Sets the column defining where the print margin should be. + * + **/ + this.setPrintMarginColumn = function(showPrintMargin) { + this.renderer.setPrintMarginColumn(showPrintMargin); + }; + + /** + * Editor.getPrintMarginColumn() -> Number + * + * Returns the column number of where the print margin is. + **/ + this.getPrintMarginColumn = function() { + return this.renderer.getPrintMarginColumn(); + }; + + this.$readOnly = false; + /** + * Editor.setReadOnly(readOnly) + * - readOnly (Boolean): Specifies whether the editor can be modified or not + * + * If `readOnly` is true, then the editor is set to read-only mode, and none of the content can change. + **/ + this.setReadOnly = function(readOnly) { + this.$readOnly = readOnly; + }; + + /** + * Editor.getReadOnly() -> Boolean + * + * Returns `true` if the editor is set to read-only mode. + **/ + this.getReadOnly = function() { + return this.$readOnly; + }; + + this.$modeBehaviours = true; + + /** + * Editor.setBehavioursEnabled() + * - enabled (Boolean): Enables or disables behaviors + * + * Specifies whether to use behaviors or not. ["Behaviors" in this case is the auto-pairing of special characters, like quotation marks, parenthesis, or brackets.]{: #BehaviorsDef} + **/ + this.setBehavioursEnabled = function (enabled) { + this.$modeBehaviours = enabled; + }; + + /** + * Editor.getBehavioursEnabled() -> Boolean + * + * Returns `true` if the behaviors are currently enabled. {:BehaviorsDef} + **/ + this.getBehavioursEnabled = function () { + return this.$modeBehaviours; + }; + + /** + * Editor.setShowFoldWidgets(show) + * - show (Boolean): Specifies whether the fold widgets are shown + * + * Indicates whether the fold widgets are shown or not. + **/ + this.setShowFoldWidgets = function(show) { + var gutter = this.renderer.$gutterLayer; + if (gutter.getShowFoldWidgets() == show) + return; + + this.renderer.$gutterLayer.setShowFoldWidgets(show); + this.$showFoldWidgets = show; + this.renderer.updateFull(); + }; + + /** + * Editor.getShowFoldWidgets() -> Boolean + * + * Returns `true` if the fold widgets are shown. + **/ + this.getShowFoldWidgets = function() { + return this.renderer.$gutterLayer.getShowFoldWidgets(); + }; + + this.setFadeFoldWidgets = function(show) { + this.renderer.setFadeFoldWidgets(show); + }; + + this.getFadeFoldWidgets = function() { + return this.renderer.getFadeFoldWidgets(); + }; + + /** + * Editor.remove(dir) + * - dir (String): The direction of the deletion to occur, either "left" or "right" + * + * Removes words of text from the editor. A "word" is defined as a string of characters bookended by whitespace. + * + **/ + this.remove = function(dir) { + if (this.selection.isEmpty()){ + if (dir == "left") + this.selection.selectLeft(); + else + this.selection.selectRight(); + } + + var range = this.getSelectionRange(); + if (this.getBehavioursEnabled()) { + var session = this.session; + var state = session.getState(range.start.row); + var new_range = session.getMode().transformAction(state, 'deletion', this, session, range); + if (new_range) + range = new_range; + } + + this.session.remove(range); + this.clearSelection(); + }; + + /** + * Editor.removeWordRight() + * + * Removes the word directly to the right of the current selection. + **/ + this.removeWordRight = function() { + if (this.selection.isEmpty()) + this.selection.selectWordRight(); + + this.session.remove(this.getSelectionRange()); + this.clearSelection(); + }; + + /** + * Editor.removeWordLeft() + * + * Removes the word directly to the left of the current selection. + **/ + this.removeWordLeft = function() { + if (this.selection.isEmpty()) + this.selection.selectWordLeft(); + + this.session.remove(this.getSelectionRange()); + this.clearSelection(); + }; + + /** + * Editor.removeToLineStart() + * + * Removes all the words to the left of the current selection, until the start of the line. + **/ + this.removeToLineStart = function() { + if (this.selection.isEmpty()) + this.selection.selectLineStart(); + + this.session.remove(this.getSelectionRange()); + this.clearSelection(); + }; + + /** + * Editor.removeToLineEnd() + * + * Removes all the words to the right of the current selection, until the end of the line. + **/ + this.removeToLineEnd = function() { + if (this.selection.isEmpty()) + this.selection.selectLineEnd(); + + var range = this.getSelectionRange(); + if (range.start.column == range.end.column && range.start.row == range.end.row) { + range.end.column = 0; + range.end.row++; + } + + this.session.remove(range); + this.clearSelection(); + }; + + /** + * Editor.splitLine() + * + * Splits the line at the current selection (by inserting an `'\n'`). + **/ + this.splitLine = function() { + if (!this.selection.isEmpty()) { + this.session.remove(this.getSelectionRange()); + this.clearSelection(); + } + + var cursor = this.getCursorPosition(); + this.insert("\n"); + this.moveCursorToPosition(cursor); + }; + + /** + * Editor.transposeLetters() + * + * Transposes current line. + **/ + this.transposeLetters = function() { + if (!this.selection.isEmpty()) { + return; + } + + var cursor = this.getCursorPosition(); + var column = cursor.column; + if (column === 0) + return; + + var line = this.session.getLine(cursor.row); + var swap, range; + if (column < line.length) { + swap = line.charAt(column) + line.charAt(column-1); + range = new Range(cursor.row, column-1, cursor.row, column+1); + } + else { + swap = line.charAt(column-1) + line.charAt(column-2); + range = new Range(cursor.row, column-2, cursor.row, column); + } + this.session.replace(range, swap); + }; + + /** + * Editor.toLowerCase() + * + * Converts the current selection entirely into lowercase. + **/ + this.toLowerCase = function() { + var originalRange = this.getSelectionRange(); + if (this.selection.isEmpty()) { + this.selection.selectWord(); + } + + var range = this.getSelectionRange(); + var text = this.session.getTextRange(range); + this.session.replace(range, text.toLowerCase()); + this.selection.setSelectionRange(originalRange); + }; + + /** + * Editor.toUpperCase() + * + * Converts the current selection entirely into uppercase. + **/ + this.toUpperCase = function() { + var originalRange = this.getSelectionRange(); + if (this.selection.isEmpty()) { + this.selection.selectWord(); + } + + var range = this.getSelectionRange(); + var text = this.session.getTextRange(range); + this.session.replace(range, text.toUpperCase()); + this.selection.setSelectionRange(originalRange); + }; + + /** related to: EditSession.indentRows + * Editor.indent() + * + * Indents the current line. + **/ + this.indent = function() { + var session = this.session; + var range = this.getSelectionRange(); + + if (range.start.row < range.end.row || range.start.column < range.end.column) { + var rows = this.$getSelectedRows(); + session.indentRows(rows.first, rows.last, "\t"); + } else { + var indentString; + + if (this.session.getUseSoftTabs()) { + var size = session.getTabSize(), + position = this.getCursorPosition(), + column = session.documentToScreenColumn(position.row, position.column), + count = (size - column % size); + + indentString = lang.stringRepeat(" ", count); + } else + indentString = "\t"; + return this.insert(indentString); + } + }; + + /** related to: EditSession.outdentRows + * Editor.blockOutdent() + * + * Outdents the current line. + **/ + this.blockOutdent = function() { + var selection = this.session.getSelection(); + this.session.outdentRows(selection.getRange()); + }; + + /** + * Editor.toggleCommentLines() + * + * Given the currently selected range, this function either comments all lines or uncomments all lines (depending on whether it's commented or not). + **/ + this.toggleCommentLines = function() { + var state = this.session.getState(this.getCursorPosition().row); + var rows = this.$getSelectedRows(); + this.session.getMode().toggleCommentLines(state, this.session, rows.first, rows.last); + }; + + /** related to: EditSession.remove + * Editor.removeLines() + * + * Removes all the lines in the current selection + **/ + this.removeLines = function() { + var rows = this.$getSelectedRows(); + var range; + if (rows.first === 0 || rows.last+1 < this.session.getLength()) + range = new Range(rows.first, 0, rows.last+1, 0); + else + range = new Range( + rows.first-1, this.session.getLine(rows.first-1).length, + rows.last, this.session.getLine(rows.last).length + ); + this.session.remove(range); + this.clearSelection(); + }; + + /** related to: EditSession.moveLinesDown + * Editor.moveLinesDown() -> Number + * + (Number): On success, it returns -1. + * + * Shifts all the selected lines down one row. + * + * + * + **/ + this.moveLinesDown = function() { + this.$moveLines(function(firstRow, lastRow) { + return this.session.moveLinesDown(firstRow, lastRow); + }); + }; + + /** related to: EditSession.moveLinesUp + * Editor.moveLinesUp() -> Number + * + (Number): On success, it returns -1. + * + * Shifts all the selected lines up one row. + * + * + **/ + this.moveLinesUp = function() { + this.$moveLines(function(firstRow, lastRow) { + return this.session.moveLinesUp(firstRow, lastRow); + }); + }; + + /** related to: EditSession.moveText + * Editor.moveText(fromRange, toPosition) -> Range + * - fromRange (Range): The range of text you want moved within the document + * - toPosition (Object): The location (row and column) where you want to move the text to + * + (Range): The new range where the text was moved to. + * + * Moves a range of text from the given range to the given position. `toPosition` is an object that looks like this: + * + * { row: newRowLocation, column: newColumnLocation } + * + * + **/ + this.moveText = function(range, toPosition) { + if (this.$readOnly) + return null; + + return this.session.moveText(range, toPosition); + }; + + /** related to: EditSession.duplicateLines + * Editor.copyLinesUp() -> Number + * + (Number): On success, returns 0. + * + * Copies all the selected lines up one row. + * + * + **/ + this.copyLinesUp = function() { + this.$moveLines(function(firstRow, lastRow) { + this.session.duplicateLines(firstRow, lastRow); + return 0; + }); + }; + + /** related to: EditSession.duplicateLines + * Editor.copyLinesDown() -> Number + * + (Number): On success, returns the number of new rows added; in other words, `lastRow - firstRow + 1`. + * + * Copies all the selected lines down one row. + * + * + * + **/ + this.copyLinesDown = function() { + this.$moveLines(function(firstRow, lastRow) { + return this.session.duplicateLines(firstRow, lastRow); + }); + }; + + + /** + * Editor.$moveLines(mover) + * - mover (Function): A method to call on each selected row + * + * Executes a specific function, which can be anything that manipulates selected lines, such as copying them, duplicating them, or shifting them. + * + **/ + this.$moveLines = function(mover) { + var rows = this.$getSelectedRows(); + var selection = this.selection; + if (!selection.isMultiLine()) { + var range = selection.getRange(); + var reverse = selection.isBackwards(); + } + + var linesMoved = mover.call(this, rows.first, rows.last); + + if (range) { + range.start.row += linesMoved; + range.end.row += linesMoved; + selection.setSelectionRange(range, reverse); + } + else { + selection.setSelectionAnchor(rows.last+linesMoved+1, 0); + selection.$moveSelection(function() { + selection.moveCursorTo(rows.first+linesMoved, 0); + }); + } + }; + + /** + * Editor.$getSelectedRows() -> Object + * + * Returns an object indicating the currently selected rows. The object looks like this: + * + * { first: range.start.row, last: range.end.row } + * + **/ + this.$getSelectedRows = function() { + var range = this.getSelectionRange().collapseRows(); + + return { + first: range.start.row, + last: range.end.row + }; + }; + + /** internal, hide + * Editor@onCompositionStart(text) + * - text (String): The text being written + * + * + **/ + this.onCompositionStart = function(text) { + this.renderer.showComposition(this.getCursorPosition()); + }; + + /** internal, hide + * Editor@onCompositionUpdate(text) + * - text (String): The text being written + * + * + **/ + this.onCompositionUpdate = function(text) { + this.renderer.setCompositionText(text); + }; + + /** internal, hide + * Editor@onCompositionEnd() + * + * + **/ + this.onCompositionEnd = function() { + this.renderer.hideComposition(); + }; + + /** related to: VirtualRenderer.getFirstVisibleRow + * Editor.getFirstVisibleRow() -> Number + * + * {:VirtualRenderer.getFirstVisibleRow} + **/ + this.getFirstVisibleRow = function() { + return this.renderer.getFirstVisibleRow(); + }; + + /** related to: VirtualRenderer.getLastVisibleRow + * Editor.getLastVisibleRow() -> Number + * + * {:VirtualRenderer.getLastVisibleRow} + **/ + this.getLastVisibleRow = function() { + return this.renderer.getLastVisibleRow(); + }; + + /** + * Editor.isRowVisible(row) -> Boolean + * - row (Number): The row to check + * + * Indicates if the row is currently visible on the screen. + **/ + this.isRowVisible = function(row) { + return (row >= this.getFirstVisibleRow() && row <= this.getLastVisibleRow()); + }; + + /** + * Editor.isRowFullyVisible(row) -> Boolean + * - row (Number): The row to check + * + * Indicates if the entire row is currently visible on the screen. + **/ + this.isRowFullyVisible = function(row) { + return (row >= this.renderer.getFirstFullyVisibleRow() && row <= this.renderer.getLastFullyVisibleRow()); + }; + + /** + * Editor.$getVisibleRowCount() -> Number + * + * Returns the number of currently visibile rows. + **/ + this.$getVisibleRowCount = function() { + return this.renderer.getScrollBottomRow() - this.renderer.getScrollTopRow() + 1; + }; + + this.$moveByPage = function(dir, select) { + var renderer = this.renderer; + var config = this.renderer.layerConfig; + var rows = dir * Math.floor(config.height / config.lineHeight); + + this.$blockScrolling++; + if (select == true) { + this.selection.$moveSelection(function(){ + this.moveCursorBy(rows, 0); + }); + } else if (select == false) { + this.selection.moveCursorBy(rows, 0); + this.selection.clearSelection(); + } + this.$blockScrolling--; + + var scrollTop = renderer.scrollTop; + + renderer.scrollBy(0, rows * config.lineHeight); + if (select != null) + renderer.scrollCursorIntoView(null, 0.5); + + renderer.animateScrolling(scrollTop); + }; + + /** + * Editor.selectPageDown() + * + * Selects the text from the current position of the document until where a "page down" finishes. + **/ + this.selectPageDown = function() { + this.$moveByPage(1, true); + }; + + /** + * Editor.selectPageUp() + * + * Selects the text from the current position of the document until where a "page up" finishes. + **/ + this.selectPageUp = function() { + this.$moveByPage(-1, true); + }; + + /** + * Editor.gotoPageDown() + * + * Shifts the document to wherever "page down" is, as well as moving the cursor position. + **/ + this.gotoPageDown = function() { + this.$moveByPage(1, false); + }; + + /** + * Editor.gotoPageUp() + * + * Shifts the document to wherever "page up" is, as well as moving the cursor position. + **/ + this.gotoPageUp = function() { + this.$moveByPage(-1, false); + }; + + /** + * Editor.scrollPageDown() + * + * Scrolls the document to wherever "page down" is, without changing the cursor position. + **/ + this.scrollPageDown = function() { + this.$moveByPage(1); + }; + + /** + * Editor.scrollPageUp() + * + * Scrolls the document to wherever "page up" is, without changing the cursor position. + **/ + this.scrollPageUp = function() { + this.$moveByPage(-1); + }; + + /** related to: VirtualRenderer.scrollToRow + * Editor.scrollToRow(row) + * - row (Number): The row to move to + * + * Moves the editor to the specified row. + * + **/ + this.scrollToRow = function(row) { + this.renderer.scrollToRow(row); + }; + + /** related to: VirtualRenderer.scrollToLine + * Editor.scrollToLine(line, center) + * - line (Number): The line to scroll to + * - center (Boolean): If `true` + * - animate (Boolean): If `true` animates scrolling + * - callback (Function): Function to be called when the animation has finished + * + * TODO scrolls a to line, if center == true, puts line in middle of screen or attempts to) + **/ + this.scrollToLine = function(line, center, animate, callback) { + this.renderer.scrollToLine(line, center, animate, callback); + }; + + /** + * Editor.centerSelection() + * + * Attempts to center the current selection on the screen. + **/ + this.centerSelection = function() { + var range = this.getSelectionRange(); + var line = Math.floor(range.start.row + (range.end.row - range.start.row) / 2); + this.renderer.scrollToLine(line, true); + }; + + /** related to: Selection.getCursor + * Editor.getCursorPosition() -> Object + * + (Object): This returns an object that looks something like this:
+ * ```{ row: currRow, column: currCol }``` + * + * Gets the current position of the cursor. + * + * + * + **/ + this.getCursorPosition = function() { + return this.selection.getCursor(); + }; + + /** related to: EditSession.documentToScreenPosition + * Editor.getCursorPositionScreen() -> Number + * + * Returns the screen position of the cursor. + **/ + this.getCursorPositionScreen = function() { + return this.session.documentToScreenPosition(this.getCursorPosition()); + }; + + /** related to: Selection.getRange + * Editor.getSelectionRange() -> Range + * + * {:Selection.getRange} + **/ + this.getSelectionRange = function() { + return this.selection.getRange(); + }; + + + /** related to: Selection.selectAll + * Editor.selectAll() + * + * Selects all the text in editor. + **/ + this.selectAll = function() { + this.$blockScrolling += 1; + this.selection.selectAll(); + this.$blockScrolling -= 1; + }; + + /** related to: Selection.clearSelection + * Editor.clearSelection() + * + * {:Selection.clearSelection} + **/ + this.clearSelection = function() { + this.selection.clearSelection(); + }; + + /** related to: Selection.moveCursorTo + * Editor.moveCursorTo(row, column) + * - row (Number): The new row number + * - column (Number): The new column number + * + * Moves the cursor to the specified row and column. Note that this does not de-select the current selection. + * + **/ + this.moveCursorTo = function(row, column) { + this.selection.moveCursorTo(row, column); + }; + + /** related to: Selection.moveCursorToPosition + * Editor.moveCursorToPosition(pos) + * - pos (Object): An object with two properties, row and column + * + * Moves the cursor to the position indicated by `pos.row` and `pos.column`. + * + **/ + this.moveCursorToPosition = function(pos) { + this.selection.moveCursorToPosition(pos); + }; + + /** + * Editor.jumpToMatching() + * + * Moves the cursor's row and column to the next matching bracket. + * + **/ + this.jumpToMatching = function() { + var cursor = this.getCursorPosition(); + var pos = this.session.findMatchingBracket(cursor); + if (!pos) { + cursor.column += 1; + pos = this.session.findMatchingBracket(cursor); + } + if (!pos) { + cursor.column -= 2; + pos = this.session.findMatchingBracket(cursor); + } + + if (pos) { + this.clearSelection(); + this.moveCursorTo(pos.row, pos.column); + } + }; + + /** + * Editor.gotoLine(lineNumber, column) + * - lineNumber (Number): The line number to go to + * - column (Number): A column number to go to + * - animate (Boolean): If `true` animates scolling + * + * Moves the cursor to the specified line number, and also into the indiciated column. + * + **/ + this.gotoLine = function(lineNumber, column, animate) { + this.selection.clearSelection(); + this.session.unfold({row: lineNumber - 1, column: column || 0}); + + this.$blockScrolling += 1; + this.moveCursorTo(lineNumber - 1, column || 0); + this.$blockScrolling -= 1; + + if (!this.isRowFullyVisible(lineNumber - 1)) + this.scrollToLine(lineNumber - 1, true, animate); + }; + + /** related to: Editor.moveCursorTo + * Editor.navigateTo(row, column) + * - row (Number): The new row number + * - column (Number): The new column number + * + * Moves the cursor to the specified row and column. Note that this does de-select the current selection. + * + **/ + this.navigateTo = function(row, column) { + this.clearSelection(); + this.moveCursorTo(row, column); + }; + + /** + * Editor.navigateUp(times) + * - times (Number): The number of times to change navigation + * + * Moves the cursor up in the document the specified number of times. Note that this does de-select the current selection. + **/ + this.navigateUp = function(times) { + this.selection.clearSelection(); + times = times || 1; + this.selection.moveCursorBy(-times, 0); + }; + + /** + * Editor.navigateDown(times) + * - times (Number): The number of times to change navigation + * + * Moves the cursor down in the document the specified number of times. Note that this does de-select the current selection. + **/ + this.navigateDown = function(times) { + this.selection.clearSelection(); + times = times || 1; + this.selection.moveCursorBy(times, 0); + }; + + /** + * Editor.navigateLeft(times) + * - times (Number): The number of times to change navigation + * + * Moves the cursor left in the document the specified number of times. Note that this does de-select the current selection. + **/ + this.navigateLeft = function(times) { + if (!this.selection.isEmpty()) { + var selectionStart = this.getSelectionRange().start; + this.moveCursorToPosition(selectionStart); + } + else { + times = times || 1; + while (times--) { + this.selection.moveCursorLeft(); + } + } + this.clearSelection(); + }; + + /** + * Editor.navigateRight(times) + * - times (Number): The number of times to change navigation + * + * Moves the cursor right in the document the specified number of times. Note that this does de-select the current selection. + **/ + this.navigateRight = function(times) { + if (!this.selection.isEmpty()) { + var selectionEnd = this.getSelectionRange().end; + this.moveCursorToPosition(selectionEnd); + } + else { + times = times || 1; + while (times--) { + this.selection.moveCursorRight(); + } + } + this.clearSelection(); + }; + + /** + * Editor.navigateLineStart() + * + * Moves the cursor to the start of the current line. Note that this does de-select the current selection. + **/ + this.navigateLineStart = function() { + this.selection.moveCursorLineStart(); + this.clearSelection(); + }; + + /** + * Editor.navigateLineEnd() + * + * Moves the cursor to the end of the current line. Note that this does de-select the current selection. + **/ + this.navigateLineEnd = function() { + this.selection.moveCursorLineEnd(); + this.clearSelection(); + }; + + /** + * Editor.navigateFileEnd() + * + * Moves the cursor to the end of the current file. Note that this does de-select the current selection. + **/ + this.navigateFileEnd = function() { + var scrollTop = this.renderer.scrollTop; + this.selection.moveCursorFileEnd(); + this.clearSelection(); + this.renderer.animateScrolling(scrollTop); + }; + + /** + * Editor.navigateFileStart() + * + * Moves the cursor to the start of the current file. Note that this does de-select the current selection. + **/ + this.navigateFileStart = function() { + var scrollTop = this.renderer.scrollTop; + this.selection.moveCursorFileStart(); + this.clearSelection(); + this.renderer.animateScrolling(scrollTop); + }; + + /** + * Editor.navigateWordRight() + * + * Moves the cursor to the word immediately to the right of the current position. Note that this does de-select the current selection. + **/ + this.navigateWordRight = function() { + this.selection.moveCursorWordRight(); + this.clearSelection(); + }; + + /** + * Editor.navigateWordLeft() + * + * Moves the cursor to the word immediately to the left of the current position. Note that this does de-select the current selection. + **/ + this.navigateWordLeft = function() { + this.selection.moveCursorWordLeft(); + this.clearSelection(); + }; + + /** + * Editor.replace(replacement, options) + * - replacement (String): The text to replace with + * - options (Object): The [[Search `Search`]] options to use + * + * Replaces the first occurance of `options.needle` with the value in `replacement`. + **/ + this.replace = function(replacement, options) { + if (options) + this.$search.set(options); + + var range = this.$search.find(this.session); + var replaced = 0; + if (!range) + return replaced; + + if (this.$tryReplace(range, replacement)) { + replaced = 1; + } + if (range !== null) { + this.selection.setSelectionRange(range); + this.renderer.scrollSelectionIntoView(range.start, range.end); + } + + return replaced; + }; + + /** + * Editor.replaceAll(replacement, options) + * - replacement (String): The text to replace with + * - options (Object): The [[Search `Search`]] options to use + * + * Replaces all occurances of `options.needle` with the value in `replacement`. + **/ + this.replaceAll = function(replacement, options) { + if (options) { + this.$search.set(options); + } + + var ranges = this.$search.findAll(this.session); + var replaced = 0; + if (!ranges.length) + return replaced; + + this.$blockScrolling += 1; + + var selection = this.getSelectionRange(); + this.clearSelection(); + this.selection.moveCursorTo(0, 0); + + for (var i = ranges.length - 1; i >= 0; --i) { + if(this.$tryReplace(ranges[i], replacement)) { + replaced++; + } + } + + this.selection.setSelectionRange(selection); + this.$blockScrolling -= 1; + + return replaced; + }; + + this.$tryReplace = function(range, replacement) { + var input = this.session.getTextRange(range); + replacement = this.$search.replace(input, replacement); + if (replacement !== null) { + range.end = this.session.replace(range, replacement); + return range; + } else { + return null; + } + }; + + /** related to: Search.getOptions + * Editor.getLastSearchOptions() -> Object + * + * {:Search.getOptions} For more information on `options`, see [[Search `Search`]]. + **/ + this.getLastSearchOptions = function() { + return this.$search.getOptions(); + }; + + /** related to: Search.find + * Editor.find(needle, options) + * - needle (String): The text to search for + * - options (Object): An object defining various search properties + * - animate (Boolean): If `true` animate scrolling + * + * Attempts to find `needle` within the document. For more information on `options`, see [[Search `Search`]]. + **/ + this.find = function(needle, options, animate) { + this.clearSelection(); + options = options || {}; + options.needle = needle; + this.$search.set(options); + this.$find(false, animate); + }; + + /** related to: Editor.find + * Editor.findNext(options) + * - options (Object): search options + * - animate (Boolean): If `true` animate scrolling + * + * Performs another search for `needle` in the document. For more information on `options`, see [[Search `Search`]]. + **/ + this.findNext = function(options, animate) { + options = options || {}; + this.$search.set(options); + this.$find(false, animate); + }; + + /** related to: Editor.find + * Editor.findPrevious(options) + * - options (Object): search options + * - animate (Boolean): If `true` animate scrolling + * + * Performs a search for `needle` backwards. For more information on `options`, see [[Search `Search`]]. + **/ + this.findPrevious = function(options, animate) { + options = options || {}; + this.$search.set(options); + this.$find(true, animate); + }; + + this.$find = function(backwards, animate) { + if (!this.selection.isEmpty()) + this.$search.set({needle: this.session.getTextRange(this.getSelectionRange())}); + + if (typeof backwards != "undefined") + this.$search.set({backwards: backwards}); + + var range = this.$search.find(this.session); + if (range) { + this.$blockScrolling += 1; + this.session.unfold(range); + this.selection.setSelectionRange(range); + this.$blockScrolling -= 1; + + var scrollTop = this.renderer.scrollTop; + this.renderer.scrollSelectionIntoView(range.start, range.end, 0.5); + this.renderer.animateScrolling(scrollTop); + } + }; + + /** related to: UndoManager.undo + * Editor.undo() + * + * {:UndoManager.undo} + **/ + this.undo = function() { + this.$blockScrolling++; + this.session.getUndoManager().undo(); + this.$blockScrolling--; + this.renderer.scrollCursorIntoView(null, 0.5); + }; + + /** related to: UndoManager.redo + * Editor.redo() + * + * {:UndoManager.redo} + **/ + this.redo = function() { + this.$blockScrolling++; + this.session.getUndoManager().redo(); + this.$blockScrolling--; + this.renderer.scrollCursorIntoView(null, 0.5); + }; + + /** + * Editor.destroy() + * + * Cleans up the entire editor. + **/ + this.destroy = function() { + this.renderer.destroy(); + }; + +}).call(Editor.prototype); + + +exports.Editor = Editor; +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/lib/lang', ['require', 'exports', 'module' ], function(require, exports, module) { +"use strict"; + +exports.stringReverse = function(string) { + return string.split("").reverse().join(""); +}; + +exports.stringRepeat = function (string, count) { + return new Array(count + 1).join(string); +}; + +var trimBeginRegexp = /^\s\s*/; +var trimEndRegexp = /\s\s*$/; + +exports.stringTrimLeft = function (string) { + return string.replace(trimBeginRegexp, ''); +}; + +exports.stringTrimRight = function (string) { + return string.replace(trimEndRegexp, ''); +}; + +exports.copyObject = function(obj) { + var copy = {}; + for (var key in obj) { + copy[key] = obj[key]; + } + return copy; +}; + +exports.copyArray = function(array){ + var copy = []; + for (var i=0, l=array.length; i + * Mihai Sucan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/keyboard/textinput', ['require', 'exports', 'module' , 'ace/lib/event', 'ace/lib/useragent', 'ace/lib/dom'], function(require, exports, module) { +"use strict"; + +var event = require("../lib/event"); +var useragent = require("../lib/useragent"); +var dom = require("../lib/dom"); + +var TextInput = function(parentNode, host) { + + var text = dom.createElement("textarea"); + if (useragent.isTouchPad) + text.setAttribute("x-palm-disable-auto-cap", true); + + text.setAttribute("wrap", "off"); + + text.style.left = "-10000px"; + text.style.position = "fixed"; + parentNode.insertBefore(text, parentNode.firstChild); + + var PLACEHOLDER = String.fromCharCode(0); + sendText(); + + var inCompostion = false; + var copied = false; + var pasted = false; + var tempStyle = ''; + + function select() { + try { + text.select(); + } catch (e) {} + } + + function sendText(valueToSend) { + if (!copied) { + var value = valueToSend || text.value; + if (value) { + if (value.length > 1) { + if (value.charAt(0) == PLACEHOLDER) + value = value.substr(1); + else if (value.charAt(value.length - 1) == PLACEHOLDER) + value = value.slice(0, -1); + } + + if (value && value != PLACEHOLDER) { + if (pasted) + host.onPaste(value); + else + host.onTextInput(value); + } + + // If editor is no longer focused we quit immediately, since + // it means that something else is in charge now. + if (!isFocused()) + return false; + } + } + + copied = false; + pasted = false; + + // Safari doesn't fire copy events if no text is selected + text.value = PLACEHOLDER; + select(); + } + + var onTextInput = function(e) { + setTimeout(function () { + if (!inCompostion) + sendText(e.data); + }, 0); + }; + + var onPropertyChange = function(e) { + if (useragent.isOldIE && text.value.charCodeAt(0) > 128) return; + setTimeout(function() { + if (!inCompostion) + sendText(); + }, 0); + }; + + var onCompositionStart = function(e) { + inCompostion = true; + host.onCompositionStart(); + if (!useragent.isGecko) setTimeout(onCompositionUpdate, 0); + }; + + var onCompositionUpdate = function() { + if (!inCompostion) return; + host.onCompositionUpdate(text.value); + }; + + var onCompositionEnd = function(e) { + inCompostion = false; + host.onCompositionEnd(); + }; + + var onCopy = function(e) { + copied = true; + var copyText = host.getCopyText(); + if(copyText) + text.value = copyText; + else + e.preventDefault(); + select(); + setTimeout(function () { + sendText(); + }, 0); + }; + + var onCut = function(e) { + copied = true; + var copyText = host.getCopyText(); + if(copyText) { + text.value = copyText; + host.onCut(); + } else + e.preventDefault(); + select(); + setTimeout(function () { + sendText(); + }, 0); + }; + + event.addCommandKeyListener(text, host.onCommandKey.bind(host)); + + if (useragent.isOldIE) { + var keytable = { 13:1, 27:1 }; + event.addListener(text, "keyup", function (e) { + if (inCompostion && (!text.value || keytable[e.keyCode])) + setTimeout(onCompositionEnd, 0); + if ((text.value.charCodeAt(0)|0) < 129) { + return; + } + inCompostion ? onCompositionUpdate() : onCompositionStart(); + }); + } + + if ("onpropertychange" in text && !("oninput" in text)) + event.addListener(text, "propertychange", onPropertyChange); + else + event.addListener(text, "input", onTextInput); + + event.addListener(text, "paste", function(e) { + // Mark that the next input text comes from past. + pasted = true; + // Some browsers support the event.clipboardData API. Use this to get + // the pasted content which increases speed if pasting a lot of lines. + if (e.clipboardData && e.clipboardData.getData) { + sendText(e.clipboardData.getData("text/plain")); + e.preventDefault(); + } + else { + // If a browser doesn't support any of the things above, use the regular + // method to detect the pasted input. + onPropertyChange(); + } + }); + + if ("onbeforecopy" in text && typeof clipboardData !== "undefined") { + event.addListener(text, "beforecopy", function(e) { + var copyText = host.getCopyText(); + if (copyText) + clipboardData.setData("Text", copyText); + else + e.preventDefault(); + }); + event.addListener(parentNode, "keydown", function(e) { + if (e.ctrlKey && e.keyCode == 88) { + var copyText = host.getCopyText(); + if (copyText) { + clipboardData.setData("Text", copyText); + host.onCut(); + } + event.preventDefault(e); + } + }); + } + else { + event.addListener(text, "copy", onCopy); + event.addListener(text, "cut", onCut); + } + + event.addListener(text, "compositionstart", onCompositionStart); + if (useragent.isGecko) { + event.addListener(text, "text", onCompositionUpdate); + } + if (useragent.isWebKit) { + event.addListener(text, "keyup", onCompositionUpdate); + } + event.addListener(text, "compositionend", onCompositionEnd); + + event.addListener(text, "blur", function() { + host.onBlur(); + }); + + event.addListener(text, "focus", function() { + host.onFocus(); + select(); + }); + + this.focus = function() { + host.onFocus(); + select(); + text.focus(); + }; + + this.blur = function() { + text.blur(); + }; + + function isFocused() { + return document.activeElement === text; + } + this.isFocused = isFocused; + + this.getElement = function() { + return text; + }; + + this.onContextMenu = function(mousePos, isEmpty){ + if (mousePos) { + if (!tempStyle) + tempStyle = text.style.cssText; + + text.style.cssText = + 'position:fixed; z-index:1000;' + + 'left:' + (mousePos.x - 2) + 'px; top:' + (mousePos.y - 2) + 'px;'; + + } + if (isEmpty) + text.value=''; + }; + + this.onContextMenuClose = function(){ + setTimeout(function () { + if (tempStyle) { + text.style.cssText = tempStyle; + tempStyle = ''; + } + sendText(); + }, 0); + }; +}; + +exports.TextInput = TextInput; +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * Mihai Sucan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mouse/mouse_handler', ['require', 'exports', 'module' , 'ace/lib/event', 'ace/mouse/default_handlers', 'ace/mouse/default_gutter_handler', 'ace/mouse/mouse_event'], function(require, exports, module) { +"use strict"; + +var event = require("../lib/event"); +var DefaultHandlers = require("./default_handlers").DefaultHandlers; +var DefaultGutterHandler = require("./default_gutter_handler").GutterHandler; +var MouseEvent = require("./mouse_event").MouseEvent; + +var MouseHandler = function(editor) { + this.editor = editor; + + new DefaultHandlers(this); + new DefaultGutterHandler(this); + + event.addListener(editor.container, "mousedown", function(e) { + editor.focus(); + return event.preventDefault(e); + }); + event.addListener(editor.container, "selectstart", function(e) { + return event.preventDefault(e); + }); + + var mouseTarget = editor.renderer.getMouseEventTarget(); + event.addListener(mouseTarget, "mousedown", this.onMouseEvent.bind(this, "mousedown")); + event.addListener(mouseTarget, "click", this.onMouseEvent.bind(this, "click")); + event.addListener(mouseTarget, "mousemove", this.onMouseMove.bind(this, "mousemove")); + event.addMultiMouseDownListener(mouseTarget, 0, 2, 500, this.onMouseEvent.bind(this, "dblclick")); + event.addMultiMouseDownListener(mouseTarget, 0, 3, 600, this.onMouseEvent.bind(this, "tripleclick")); + event.addMultiMouseDownListener(mouseTarget, 0, 4, 600, this.onMouseEvent.bind(this, "quadclick")); + event.addMouseWheelListener(editor.container, this.onMouseWheel.bind(this, "mousewheel")); + + var gutterEl = editor.renderer.$gutter; + event.addListener(gutterEl, "mousedown", this.onMouseEvent.bind(this, "guttermousedown")); + event.addListener(gutterEl, "click", this.onMouseEvent.bind(this, "gutterclick")); + event.addListener(gutterEl, "dblclick", this.onMouseEvent.bind(this, "gutterdblclick")); + event.addListener(gutterEl, "mousemove", this.onMouseMove.bind(this, "gutter")); +}; + +(function() { + + this.$scrollSpeed = 1; + this.setScrollSpeed = function(speed) { + this.$scrollSpeed = speed; + }; + + this.getScrollSpeed = function() { + return this.$scrollSpeed; + }; + + this.onMouseEvent = function(name, e) { + this.editor._emit(name, new MouseEvent(e, this.editor)); + }; + + this.$dragDelay = 250; + this.setDragDelay = function(dragDelay) { + this.$dragDelay = dragDelay; + }; + + this.getDragDelay = function() { + return this.$dragDelay; + }; + + this.onMouseMove = function(name, e) { + // optimization, because mousemove doesn't have a default handler. + var listeners = this.editor._eventRegistry && this.editor._eventRegistry.mousemove; + if (!listeners || !listeners.length) + return; + + this.editor._emit(name, new MouseEvent(e, this.editor)); + }; + + this.onMouseWheel = function(name, e) { + var mouseEvent = new MouseEvent(e, this.editor); + mouseEvent.speed = this.$scrollSpeed * 2; + mouseEvent.wheelX = e.wheelX; + mouseEvent.wheelY = e.wheelY; + + this.editor._emit(name, mouseEvent); + }; + + this.setState = function(state) { + this.state = state; + }; + + this.captureMouse = function(ev, state) { + if (state) + this.setState(state); + + this.x = ev.x; + this.y = ev.y; + + // do not move textarea during selection + var kt = this.editor.renderer.$keepTextAreaAtCursor; + this.editor.renderer.$keepTextAreaAtCursor = false; + + var self = this; + var onMouseSelection = function(e) { + self.x = e.clientX; + self.y = e.clientY; + }; + + var onMouseSelectionEnd = function(e) { + clearInterval(timerId); + self[self.state + "End"] && self[self.state + "End"](e); + self.$clickSelection = null; + self.editor.renderer.$keepTextAreaAtCursor = kt; + self.editor.renderer.$moveTextAreaToCursor(); + }; + + var onSelectionInterval = function() { + self[self.state] && self[self.state](); + } + + event.capture(this.editor.container, onMouseSelection, onMouseSelectionEnd); + var timerId = setInterval(onSelectionInterval, 20); + + ev.preventDefault(); + }; +}).call(MouseHandler.prototype); + +exports.MouseHandler = MouseHandler; +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * Mike de Boer + * Harutyun Amirjanyan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mouse/default_handlers', ['require', 'exports', 'module' , 'ace/lib/dom', 'ace/lib/browser_focus'], function(require, exports, module) { +"use strict"; + +var dom = require("../lib/dom"); +var BrowserFocus = require("../lib/browser_focus").BrowserFocus; + + +var DRAG_OFFSET = 5; // pixels + + + +function DefaultHandlers(mouseHandler) { + mouseHandler.$clickSelection = null; + mouseHandler.browserFocus = new BrowserFocus(); + + var editor = mouseHandler.editor; + editor.setDefaultHandler("mousedown", this.onMouseDown.bind(mouseHandler)); + editor.setDefaultHandler("dblclick", this.onDoubleClick.bind(mouseHandler)); + editor.setDefaultHandler("tripleclick", this.onTripleClick.bind(mouseHandler)); + editor.setDefaultHandler("quadclick", this.onQuadClick.bind(mouseHandler)); + editor.setDefaultHandler("mousewheel", this.onScroll.bind(mouseHandler)); + + var exports = ["select", "startSelect", "drag", "dragEnd", "dragWait", + "dragWaitEnd", "startDrag"]; + + exports.forEach(function(x) { + mouseHandler[x] = this[x]; + }, this); + + mouseHandler.selectByLines = this.extendSelectionBy.bind(mouseHandler, "getLineRange"); + mouseHandler.selectByWords = this.extendSelectionBy.bind(mouseHandler, "getWordRange"); +} + +(function() { + + this.onMouseDown = function(ev) { + this.mousedownEvent = ev; + var inSelection = ev.inSelection(); + var pos = ev.getDocumentPosition(); + var editor = this.editor; + var _self = this; + + this.ev = ev + var selectionRange = editor.getSelectionRange(); + var selectionEmpty = selectionRange.isEmpty(); + + var button = ev.getButton(); + if (button !== 0) { + if (selectionEmpty) { + editor.moveCursorToPosition(pos); + editor.selection.clearSelection(); + } + // 2: contextmenu, 1: linux paste + this.moveTextarea = function() { + editor.textInput.onContextMenu({x: _self.x, y: _self.y}); + }; + this.moveTextareaEnd = editor.textInput.onContextMenuClose; + + editor.textInput.onContextMenu({x: this.x, y: this.y}, selectionEmpty); + this.captureMouse(ev, "moveTextarea"); + + return; + } + + // if this click caused the editor to be focused should not clear the + // selection + if (inSelection && !editor.isFocused()) { + editor.focus(); + return; + } + + if (!inSelection || this.$clickSelection || ev.getShiftKey()) { + // Directly pick STATE_SELECT, since the user is not clicking inside + // a selection. + this.startSelect(pos); + } else if (inSelection) { + var e = ev.domEvent; + if ((e.ctrlKey || e.altKey)) { + this.startDrag(); + } else { + this.mousedownEvent.time = (new Date()).getTime(); + this.setState("dragWait"); + } + } + + this.captureMouse(ev) + }; + + this.startSelect = function(pos) { + pos = pos || this.editor.renderer.screenToTextCoordinates(this.x, this.y); + if (this.mousedownEvent.getShiftKey()) { + this.editor.selection.selectToPosition(pos); + } + else if (!this.$clickSelection) { + this.editor.moveCursorToPosition(pos); + this.editor.selection.clearSelection(); + } + this.setState("select"); + } + + this.select = function() { + var anchor, editor = this.editor; + var cursor = editor.renderer.screenToTextCoordinates(this.x, this.y); + + if (this.$clickSelection) { + var cmp = this.$clickSelection.comparePoint(cursor); + + if (cmp == -1) { + anchor = this.$clickSelection.end; + } else if (cmp == 1) { + anchor = this.$clickSelection.start; + } else { + cursor = this.$clickSelection.end; + anchor = this.$clickSelection.start; + } + editor.selection.setSelectionAnchor(anchor.row, anchor.column); + } + editor.selection.selectToPosition(cursor); + + editor.renderer.scrollCursorIntoView(); + }; + + this.extendSelectionBy = function(unitName) { + var anchor, editor = this.editor; + var cursor = editor.renderer.screenToTextCoordinates(this.x, this.y); + var range = editor.selection[unitName](cursor.row, cursor.column); + + if (this.$clickSelection) { + var cmpStart = this.$clickSelection.comparePoint(range.start); + var cmpEnd = this.$clickSelection.comparePoint(range.end); + + if (cmpStart == -1 && cmpEnd <= 0) { + anchor = this.$clickSelection.end; + cursor = range.start; + } else if (cmpEnd == 1 && cmpStart >= 0) { + anchor = this.$clickSelection.start; + cursor = range.end; + } else if (cmpStart == -1 && cmpEnd == 1) { + cursor = range.end; + anchor = range.start; + } else { + cursor = this.$clickSelection.end; + anchor = this.$clickSelection.start; + } + editor.selection.setSelectionAnchor(anchor.row, anchor.column); + } + editor.selection.selectToPosition(cursor); + + editor.renderer.scrollCursorIntoView(); + }; + + this.startDrag = function() { + var editor = this.editor; + this.setState("drag"); + this.dragRange = editor.getSelectionRange(); + var style = editor.getSelectionStyle(); + this.dragSelectionMarker = editor.session.addMarker(this.dragRange, "ace_selection", style); + editor.clearSelection(); + dom.addCssClass(editor.container, "ace_dragging"); + if (!this.$dragKeybinding) { + this.$dragKeybinding = { + handleKeyboard: function(data, hashId, keyString, keyCode) { + if (keyString == "esc") + return {command: this.command}; + }, + command: { + exec: function(editor) { + var self = editor.$mouseHandler; + self.dragCursor = null + self.dragEnd(); + self.startSelect(); + } + } + } + } + + editor.keyBinding.addKeyboardHandler(this.$dragKeybinding); + }; + + this.dragWait = function() { + var distance = calcDistance(this.mousedownEvent.x, this.mousedownEvent.y, this.x, this.y); + var time = (new Date()).getTime(); + var editor = this.editor; + + if (distance > DRAG_OFFSET) { + this.startSelect(); + } else if ((time - this.mousedownEvent.time) > editor.getDragDelay()) { + this.startDrag() + } + }; + + this.dragWaitEnd = function(e) { + this.mousedownEvent.domEvent = e; + this.startSelect(); + }; + + this.drag = function() { + var editor = this.editor; + this.dragCursor = editor.renderer.screenToTextCoordinates(this.x, this.y); + editor.moveCursorToPosition(this.dragCursor); + editor.renderer.scrollCursorIntoView(); + }; + + this.dragEnd = function(e) { + var editor = this.editor; + var dragCursor = this.dragCursor; + var dragRange = this.dragRange; + dom.removeCssClass(editor.container, "ace_dragging"); + editor.session.removeMarker(this.dragSelectionMarker); + editor.keyBinding.removeKeyboardHandler(this.$dragKeybinding); + + if (!dragCursor) + return; + + editor.clearSelection(); + if (e && (e.ctrlKey || e.altKey)) { + var session = editor.session; + var newRange = dragRange; + newRange.end = session.insert(dragCursor, session.getTextRange(dragRange)); + newRange.start = dragCursor; + } else if (dragRange.contains(dragCursor.row, dragCursor.column)) { + return; + } else { + var newRange = editor.moveText(dragRange, dragCursor); + } + + if (!newRange) + return; + + editor.selection.setSelectionRange(newRange); + }; + + this.onDoubleClick = function(ev) { + var pos = ev.getDocumentPosition(); + var editor = this.editor; + + this.setState("selectByWords"); + + editor.moveCursorToPosition(pos); + editor.selection.selectWord(); + this.$clickSelection = editor.getSelectionRange(); + }; + + this.onTripleClick = function(ev) { + var pos = ev.getDocumentPosition(); + var editor = this.editor; + + this.setState("selectByLines"); + + editor.moveCursorToPosition(pos); + editor.selection.selectLine(); + this.$clickSelection = editor.getSelectionRange(); + }; + + this.onQuadClick = function(ev) { + var editor = this.editor; + + editor.selectAll(); + this.$clickSelection = editor.getSelectionRange(); + this.setState("select"); + }; + + this.onScroll = function(ev) { + var editor = this.editor; + var isScrolable = editor.renderer.isScrollableBy(ev.wheelX * ev.speed, ev.wheelY * ev.speed); + if (isScrolable) { + this.$passScrollEvent = false; + } else { + if (this.$passScrollEvent) + return; + + if (!this.$scrollStopTimeout) { + var self = this; + this.$scrollStopTimeout = setTimeout(function() { + self.$passScrollEvent = true; + self.$scrollStopTimeout = null; + }, 200); + } + } + + editor.renderer.scrollBy(ev.wheelX * ev.speed, ev.wheelY * ev.speed); + return ev.preventDefault(); + }; + +}).call(DefaultHandlers.prototype); + +exports.DefaultHandlers = DefaultHandlers; + +function calcDistance(ax, ay, bx, by) { + return Math.sqrt(Math.pow(bx - ax, 2) + Math.pow(by - ay, 2)); +} + +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * Irakli Gozalishvili (http://jeditoolkit.com) + * Julian Viereck + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/lib/browser_focus', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/lib/event', 'ace/lib/event_emitter'], function(require, exports, module) { +"use strict"; + +var oop = require("./oop"); +var event = require("./event"); +var EventEmitter = require("./event_emitter").EventEmitter; + +/* + * This class keeps track of the focus state of the given window. + * Focus changes for example when the user switches a browser tab, + * goes to the location bar or switches to another application. + */ +var BrowserFocus = function(win) { + win = win || window; + + this.lastFocus = new Date().getTime(); + this._isFocused = true; + + var _self = this; + + // IE < 9 supports focusin and focusout events + if ("onfocusin" in win.document) { + event.addListener(win.document, "focusin", function(e) { + _self._setFocused(true); + }); + + event.addListener(win.document, "focusout", function(e) { + _self._setFocused(!!e.toElement); + }); + } + else { + event.addListener(win, "blur", function(e) { + _self._setFocused(false); + }); + + event.addListener(win, "focus", function(e) { + _self._setFocused(true); + }); + } +}; + +(function(){ + + oop.implement(this, EventEmitter); + + this.isFocused = function() { + return this._isFocused; + }; + + this._setFocused = function(isFocused) { + if (this._isFocused == isFocused) + return; + + if (isFocused) + this.lastFocus = new Date().getTime(); + + this._isFocused = isFocused; + this._emit("changeFocus"); + }; + +}).call(BrowserFocus.prototype); + + +exports.BrowserFocus = BrowserFocus; +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * Irakli Gozalishvili (http://jeditoolkit.com) + * Mike de Boer + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/lib/event_emitter', ['require', 'exports', 'module' ], function(require, exports, module) { +"use strict"; + +var EventEmitter = {}; + +EventEmitter._emit = +EventEmitter._dispatchEvent = function(eventName, e) { + this._eventRegistry = this._eventRegistry || {}; + this._defaultHandlers = this._defaultHandlers || {}; + + var listeners = this._eventRegistry[eventName] || []; + var defaultHandler = this._defaultHandlers[eventName]; + if (!listeners.length && !defaultHandler) + return; + + e = e || {}; + e.type = eventName; + + if (!e.stopPropagation) { + e.stopPropagation = function() { + this.propagationStopped = true; + }; + } + + if (!e.preventDefault) { + e.preventDefault = function() { + this.defaultPrevented = true; + }; + } + + for (var i=0; i + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mouse/default_gutter_handler', ['require', 'exports', 'module' ], function(require, exports, module) { +"use strict"; + +function GutterHandler(mouseHandler) { + var editor = mouseHandler.editor; + + mouseHandler.editor.setDefaultHandler("guttermousedown", function(e) { + if (e.domEvent.target.className.indexOf("ace_gutter-cell") == -1) + return; + + if (!editor.isFocused()) + return; + + var row = e.getDocumentPosition().row; + var selection = editor.session.selection; + + selection.moveCursorTo(row, 0); + selection.selectLine(); + + mouseHandler.$clickSelection = selection.getRange(); + mouseHandler.captureMouse(e, "selectByLines"); + }); +} + +exports.GutterHandler = GutterHandler; + +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mouse/mouse_event', ['require', 'exports', 'module' , 'ace/lib/event'], function(require, exports, module) { +"use strict"; + +var event = require("../lib/event"); + +/* + * Custom Ace mouse event + */ +var MouseEvent = exports.MouseEvent = function(domEvent, editor) { + this.domEvent = domEvent; + this.editor = editor; + + this.x = this.clientX = domEvent.clientX; + this.y = this.clientY = domEvent.clientY; + + this.$pos = null; + this.$inSelection = null; + + this.propagationStopped = false; + this.defaultPrevented = false; +}; + +(function() { + + this.stopPropagation = function() { + event.stopPropagation(this.domEvent); + this.propagationStopped = true; + }; + + this.preventDefault = function() { + event.preventDefault(this.domEvent); + this.defaultPrevented = true; + }; + + this.stop = function() { + this.stopPropagation(); + this.preventDefault(); + }; + + /* + * Get the document position below the mouse cursor + * + * @return {Object} 'row' and 'column' of the document position + */ + this.getDocumentPosition = function() { + if (this.$pos) + return this.$pos; + + this.$pos = this.editor.renderer.screenToTextCoordinates(this.clientX, this.clientY); + return this.$pos; + }; + + /* + * Check if the mouse cursor is inside of the text selection + * + * @return {Boolean} whether the mouse cursor is inside of the selection + */ + this.inSelection = function() { + if (this.$inSelection !== null) + return this.$inSelection; + + var editor = this.editor; + + if (editor.getReadOnly()) { + this.$inSelection = false; + } + else { + var selectionRange = editor.getSelectionRange(); + if (selectionRange.isEmpty()) + this.$inSelection = false; + else { + var pos = this.getDocumentPosition(); + this.$inSelection = selectionRange.contains(pos.row, pos.column); + } + } + return this.$inSelection; + }; + + /* + * Get the clicked mouse button + * + * @return {Number} 0 for left button, 1 for middle button, 2 for right button + */ + this.getButton = function() { + return event.getButton(this.domEvent); + }; + + /* + * @return {Boolean} whether the shift key was pressed when the event was emitted + */ + this.getShiftKey = function() { + return this.domEvent.shiftKey; + }; + + this.getAccelKey = function() { + return this.domEvent.ctrlKey || this.domEvent.metaKey ; + }; + +}).call(MouseEvent.prototype); + +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mouse/fold_handler', ['require', 'exports', 'module' ], function(require, exports, module) { +"use strict"; + +function FoldHandler(editor) { + + editor.on("click", function(e) { + var position = e.getDocumentPosition(); + var session = editor.session; + + // If the user clicked on a fold, then expand it. + var fold = session.getFoldAt(position.row, position.column, 1); + if (fold) { + if (e.getAccelKey()) + session.removeFold(fold); + else + session.expandFold(fold); + + e.stop(); + } + }); + + editor.on("gutterclick", function(e) { + if (e.domEvent.target.className.indexOf("ace_fold-widget") != -1) { + var row = e.getDocumentPosition().row; + editor.session.onFoldWidgetClick(row, e.domEvent); + e.stop(); + } + }); +} + +exports.FoldHandler = FoldHandler; + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * Julian Viereck + * Harutyun Amirjanyan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/keyboard/keybinding', ['require', 'exports', 'module' , 'ace/lib/keys', 'ace/lib/event', 'ace/commands/default_commands'], function(require, exports, module) { +"use strict"; + +var keyUtil = require("../lib/keys"); +var event = require("../lib/event"); +require("../commands/default_commands"); + +var KeyBinding = function(editor) { + this.$editor = editor; + this.$data = { }; + this.$handlers = []; + this.setDefaultHandler(editor.commands); +}; + +(function() { + this.setDefaultHandler = function(keyboardHandler) { + this.removeKeyboardHandler(this.$defaultHandler); + this.$defaultHandler = keyboardHandler; + if (keyboardHandler) + this.$handlers.unshift(keyboardHandler); + this.$data = { }; + }; + + this.setKeyboardHandler = function(keyboardHandler) { + if (this.$handlers[this.$handlers.length - 1] == keyboardHandler) + return; + this.$data = { }; + this.$handlers = []; + this.setDefaultHandler(this.$defaultHandler); + if (keyboardHandler) + this.$handlers.push(keyboardHandler); + }; + + this.addKeyboardHandler = function(keyboardHandler) { + this.removeKeyboardHandler(keyboardHandler); + this.$handlers.push(keyboardHandler); + }; + + this.removeKeyboardHandler = function(keyboardHandler) { + var i = this.$handlers.indexOf(keyboardHandler); + if (i == -1) + return false; + this.$handlers.splice(i, 1); + return true; + }; + + this.getKeyboardHandler = function() { + return this.$handlers[this.$handlers.length - 1]; + }; + + this.$callKeyboardHandlers = function (hashId, keyString, keyCode, e) { + var toExecute; + for (var i = this.$handlers.length; i--;) { + toExecute = this.$handlers[i].handleKeyboard( + this.$data, hashId, keyString, keyCode, e + ); + if (toExecute && toExecute.command) + break; + } + + if (!toExecute || !toExecute.command) + return false; + + var success = false; + var commands = this.$editor.commands; + + // allow keyboardHandler to consume keys + if (toExecute.command != "null") + success = commands.exec(toExecute.command, this.$editor, toExecute.args, e); + else + success = true; + + if (success && e) + event.stopEvent(e); + + return success; + }; + + this.onCommandKey = function(e, hashId, keyCode) { + var keyString = keyUtil.keyCodeToString(keyCode); + this.$callKeyboardHandlers(hashId, keyString, keyCode, e); + }; + + this.onTextInput = function(text) { + var success = false; + if (text.length == 1) + success = this.$callKeyboardHandlers(0, text); + if (!success) + this.$editor.commands.exec("insertstring", this.$editor, text); + }; + +}).call(KeyBinding.prototype); + +exports.KeyBinding = KeyBinding; +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * Julian Viereck + * Mihai Sucan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/commands/default_commands', ['require', 'exports', 'module' , 'ace/lib/lang'], function(require, exports, module) { +"use strict"; + +var lang = require("../lib/lang"); + +function bindKey(win, mac) { + return { + win: win, + mac: mac + }; +} + +exports.commands = [{ + name: "selectall", + bindKey: bindKey("Ctrl-A", "Command-A"), + exec: function(editor) { editor.selectAll(); }, + readOnly: true +}, { + name: "centerselection", + bindKey: bindKey(null, "Ctrl-L"), + exec: function(editor) { editor.centerSelection(); }, + readOnly: true +}, { + name: "gotoline", + bindKey: bindKey("Ctrl-L", "Command-L"), + exec: function(editor) { + var line = parseInt(prompt("Enter line number:"), 10); + if (!isNaN(line)) { + editor.gotoLine(line); + } + }, + readOnly: true +}, { + name: "fold", + bindKey: bindKey("Alt-L", "Alt-L"), + exec: function(editor) { editor.session.toggleFold(false); }, + readOnly: true +}, { + name: "unfold", + bindKey: bindKey("Alt-Shift-L", "Alt-Shift-L"), + exec: function(editor) { editor.session.toggleFold(true); }, + readOnly: true +}, { + name: "foldall", + bindKey: bindKey("Alt-0", "Alt-0"), + exec: function(editor) { editor.session.foldAll(); }, + readOnly: true +}, { + name: "unfoldall", + bindKey: bindKey("Alt-Shift-0", "Alt-Shift-0"), + exec: function(editor) { editor.session.unfold(); }, + readOnly: true +}, { + name: "findnext", + bindKey: bindKey("Ctrl-K", "Command-G"), + exec: function(editor) { editor.findNext(); }, + readOnly: true +}, { + name: "findprevious", + bindKey: bindKey("Ctrl-Shift-K", "Command-Shift-G"), + exec: function(editor) { editor.findPrevious(); }, + readOnly: true +}, { + name: "find", + bindKey: bindKey("Ctrl-F", "Command-F"), + exec: function(editor) { + var needle = prompt("Find:", editor.getCopyText()); + editor.find(needle); + }, + readOnly: true +}, { + name: "overwrite", + bindKey: bindKey("Insert", "Insert"), + exec: function(editor) { editor.toggleOverwrite(); }, + readOnly: true +}, { + name: "selecttostart", + bindKey: bindKey("Ctrl-Shift-Home|Alt-Shift-Up", "Command-Shift-Up"), + exec: function(editor) { editor.getSelection().selectFileStart(); }, + readOnly: true +}, { + name: "gotostart", + bindKey: bindKey("Ctrl-Home|Ctrl-Up", "Command-Home|Command-Up"), + exec: function(editor) { editor.navigateFileStart(); }, + readOnly: true +}, { + name: "selectup", + bindKey: bindKey("Shift-Up", "Shift-Up"), + exec: function(editor) { editor.getSelection().selectUp(); }, + multiSelectAction: "forEach", + readOnly: true +}, { + name: "golineup", + bindKey: bindKey("Up", "Up|Ctrl-P"), + exec: function(editor, args) { editor.navigateUp(args.times); }, + multiSelectAction: "forEach", + readOnly: true +}, { + name: "selecttoend", + bindKey: bindKey("Ctrl-Shift-End|Alt-Shift-Down", "Command-Shift-Down"), + exec: function(editor) { editor.getSelection().selectFileEnd(); }, + multiSelectAction: "forEach", + readOnly: true +}, { + name: "gotoend", + bindKey: bindKey("Ctrl-End|Ctrl-Down", "Command-End|Command-Down"), + exec: function(editor) { editor.navigateFileEnd(); }, + multiSelectAction: "forEach", + readOnly: true +}, { + name: "selectdown", + bindKey: bindKey("Shift-Down", "Shift-Down"), + exec: function(editor) { editor.getSelection().selectDown(); }, + multiSelectAction: "forEach", + readOnly: true +}, { + name: "golinedown", + bindKey: bindKey("Down", "Down|Ctrl-N"), + exec: function(editor, args) { editor.navigateDown(args.times); }, + multiSelectAction: "forEach", + readOnly: true +}, { + name: "selectwordleft", + bindKey: bindKey("Ctrl-Shift-Left", "Option-Shift-Left"), + exec: function(editor) { editor.getSelection().selectWordLeft(); }, + multiSelectAction: "forEach", + readOnly: true +}, { + name: "gotowordleft", + bindKey: bindKey("Ctrl-Left", "Option-Left"), + exec: function(editor) { editor.navigateWordLeft(); }, + multiSelectAction: "forEach", + readOnly: true +}, { + name: "selecttolinestart", + bindKey: bindKey("Alt-Shift-Left", "Command-Shift-Left"), + exec: function(editor) { editor.getSelection().selectLineStart(); }, + multiSelectAction: "forEach", + readOnly: true +}, { + name: "gotolinestart", + bindKey: bindKey("Alt-Left|Home", "Command-Left|Home|Ctrl-A"), + exec: function(editor) { editor.navigateLineStart(); }, + multiSelectAction: "forEach", + readOnly: true +}, { + name: "selectleft", + bindKey: bindKey("Shift-Left", "Shift-Left"), + exec: function(editor) { editor.getSelection().selectLeft(); }, + multiSelectAction: "forEach", + readOnly: true +}, { + name: "gotoleft", + bindKey: bindKey("Left", "Left|Ctrl-B"), + exec: function(editor, args) { editor.navigateLeft(args.times); }, + multiSelectAction: "forEach", + readOnly: true +}, { + name: "selectwordright", + bindKey: bindKey("Ctrl-Shift-Right", "Option-Shift-Right"), + exec: function(editor) { editor.getSelection().selectWordRight(); }, + multiSelectAction: "forEach", + readOnly: true +}, { + name: "gotowordright", + bindKey: bindKey("Ctrl-Right", "Option-Right"), + exec: function(editor) { editor.navigateWordRight(); }, + multiSelectAction: "forEach", + readOnly: true +}, { + name: "selecttolineend", + bindKey: bindKey("Alt-Shift-Right", "Command-Shift-Right"), + exec: function(editor) { editor.getSelection().selectLineEnd(); }, + multiSelectAction: "forEach", + readOnly: true +}, { + name: "gotolineend", + bindKey: bindKey("Alt-Right|End", "Command-Right|End|Ctrl-E"), + exec: function(editor) { editor.navigateLineEnd(); }, + multiSelectAction: "forEach", + readOnly: true +}, { + name: "selectright", + bindKey: bindKey("Shift-Right", "Shift-Right"), + exec: function(editor) { editor.getSelection().selectRight(); }, + multiSelectAction: "forEach", + readOnly: true +}, { + name: "gotoright", + bindKey: bindKey("Right", "Right|Ctrl-F"), + exec: function(editor, args) { editor.navigateRight(args.times); }, + multiSelectAction: "forEach", + readOnly: true +}, { + name: "selectpagedown", + bindKey: bindKey("Shift-PageDown", "Shift-PageDown"), + exec: function(editor) { editor.selectPageDown(); }, + readOnly: true +}, { + name: "pagedown", + bindKey: bindKey(null, "PageDown"), + exec: function(editor) { editor.scrollPageDown(); }, + readOnly: true +}, { + name: "gotopagedown", + bindKey: bindKey("PageDown", "Option-PageDown|Ctrl-V"), + exec: function(editor) { editor.gotoPageDown(); }, + readOnly: true +}, { + name: "selectpageup", + bindKey: bindKey("Shift-PageUp", "Shift-PageUp"), + exec: function(editor) { editor.selectPageUp(); }, + readOnly: true +}, { + name: "pageup", + bindKey: bindKey(null, "PageUp"), + exec: function(editor) { editor.scrollPageUp(); }, + readOnly: true +}, { + name: "gotopageup", + bindKey: bindKey("PageUp", "Option-PageUp"), + exec: function(editor) { editor.gotoPageUp(); }, + readOnly: true +}, { + name: "selectlinestart", + bindKey: bindKey("Shift-Home", "Shift-Home"), + exec: function(editor) { editor.getSelection().selectLineStart(); }, + multiSelectAction: "forEach", + readOnly: true +}, { + name: "selectlineend", + bindKey: bindKey("Shift-End", "Shift-End"), + exec: function(editor) { editor.getSelection().selectLineEnd(); }, + multiSelectAction: "forEach", + readOnly: true +}, { + name: "togglerecording", + bindKey: bindKey("Ctrl-Alt-E", "Command-Option-E"), + exec: function(editor) { editor.commands.toggleRecording(); }, + readOnly: true +}, { + name: "replaymacro", + bindKey: bindKey("Ctrl-Shift-E", "Command-Shift-E"), + exec: function(editor) { editor.commands.replay(editor); }, + readOnly: true +}, { + name: "jumptomatching", + bindKey: bindKey("Ctrl-Shift-P", "Ctrl-Shift-P"), + exec: function(editor) { editor.jumpToMatching(); }, + multiSelectAction: "forEach", + readOnly: true +}, + +// commands disabled in readOnly mode +{ + name: "cut", + exec: function(editor) { + var range = editor.getSelectionRange(); + editor._emit("cut", range); + + if (!editor.selection.isEmpty()) { + editor.session.remove(range); + editor.clearSelection(); + } + }, + multiSelectAction: "forEach" +}, { + name: "removeline", + bindKey: bindKey("Ctrl-D", "Command-D"), + exec: function(editor) { editor.removeLines(); }, + multiSelectAction: "forEach" +}, { + name: "togglecomment", + bindKey: bindKey("Ctrl-/", "Command-/"), + exec: function(editor) { editor.toggleCommentLines(); }, + multiSelectAction: "forEach" +}, { + name: "replace", + bindKey: bindKey("Ctrl-R", "Command-Option-F"), + exec: function(editor) { + var needle = prompt("Find:", editor.getCopyText()); + if (!needle) + return; + var replacement = prompt("Replacement:"); + if (!replacement) + return; + editor.replace(replacement, {needle: needle}); + } +}, { + name: "replaceall", + bindKey: bindKey("Ctrl-Shift-R", "Command-Shift-Option-F"), + exec: function(editor) { + var needle = prompt("Find:"); + if (!needle) + return; + var replacement = prompt("Replacement:"); + if (!replacement) + return; + editor.replaceAll(replacement, {needle: needle}); + } +}, { + name: "undo", + bindKey: bindKey("Ctrl-Z", "Command-Z"), + exec: function(editor) { editor.undo(); } +}, { + name: "redo", + bindKey: bindKey("Ctrl-Shift-Z|Ctrl-Y", "Command-Shift-Z|Command-Y"), + exec: function(editor) { editor.redo(); } +}, { + name: "copylinesup", + bindKey: bindKey("Ctrl-Alt-Up", "Command-Option-Up"), + exec: function(editor) { editor.copyLinesUp(); } +}, { + name: "movelinesup", + bindKey: bindKey("Alt-Up", "Option-Up"), + exec: function(editor) { editor.moveLinesUp(); } +}, { + name: "copylinesdown", + bindKey: bindKey("Ctrl-Alt-Down", "Command-Option-Down"), + exec: function(editor) { editor.copyLinesDown(); } +}, { + name: "movelinesdown", + bindKey: bindKey("Alt-Down", "Option-Down"), + exec: function(editor) { editor.moveLinesDown(); } +}, { + name: "del", + bindKey: bindKey("Delete", "Delete|Ctrl-D"), + exec: function(editor) { editor.remove("right"); }, + multiSelectAction: "forEach" +}, { + name: "backspace", + bindKey: bindKey( + "Command-Backspace|Option-Backspace|Shift-Backspace|Backspace", + "Ctrl-Backspace|Command-Backspace|Shift-Backspace|Backspace|Ctrl-H" + ), + exec: function(editor) { editor.remove("left"); }, + multiSelectAction: "forEach" +}, { + name: "removetolinestart", + bindKey: bindKey("Alt-Backspace", "Command-Backspace"), + exec: function(editor) { editor.removeToLineStart(); }, + multiSelectAction: "forEach" +}, { + name: "removetolineend", + bindKey: bindKey("Alt-Delete", "Ctrl-K"), + exec: function(editor) { editor.removeToLineEnd(); }, + multiSelectAction: "forEach" +}, { + name: "removewordleft", + bindKey: bindKey("Ctrl-Backspace", "Alt-Backspace|Ctrl-Alt-Backspace"), + exec: function(editor) { editor.removeWordLeft(); }, + multiSelectAction: "forEach" +}, { + name: "removewordright", + bindKey: bindKey("Ctrl-Delete", "Alt-Delete"), + exec: function(editor) { editor.removeWordRight(); }, + multiSelectAction: "forEach" +}, { + name: "outdent", + bindKey: bindKey("Shift-Tab", "Shift-Tab"), + exec: function(editor) { editor.blockOutdent(); }, + multiSelectAction: "forEach" +}, { + name: "indent", + bindKey: bindKey("Tab", "Tab"), + exec: function(editor) { editor.indent(); }, + multiSelectAction: "forEach" +}, { + name: "insertstring", + exec: function(editor, str) { editor.insert(str); }, + multiSelectAction: "forEach" +}, { + name: "inserttext", + exec: function(editor, args) { + editor.insert(lang.stringRepeat(args.text || "", args.times || 1)); + }, + multiSelectAction: "forEach" +}, { + name: "splitline", + bindKey: bindKey(null, "Ctrl-O"), + exec: function(editor) { editor.splitLine(); }, + multiSelectAction: "forEach" +}, { + name: "transposeletters", + bindKey: bindKey("Ctrl-T", "Ctrl-T"), + exec: function(editor) { editor.transposeLetters(); }, + multiSelectAction: function(editor) {editor.transposeSelections(1); } +}, { + name: "touppercase", + bindKey: bindKey("Ctrl-U", "Ctrl-U"), + exec: function(editor) { editor.toUpperCase(); }, + multiSelectAction: "forEach" +}, { + name: "tolowercase", + bindKey: bindKey("Ctrl-Shift-U", "Ctrl-Shift-U"), + exec: function(editor) { editor.toLowerCase(); }, + multiSelectAction: "forEach" +}]; + +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * Mihai Sucan + * Julian Viereck + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/edit_session', ['require', 'exports', 'module' , 'ace/config', 'ace/lib/oop', 'ace/lib/lang', 'ace/lib/net', 'ace/lib/event_emitter', 'ace/selection', 'ace/mode/text', 'ace/range', 'ace/document', 'ace/background_tokenizer', 'ace/edit_session/folding', 'ace/edit_session/bracket_match'], function(require, exports, module) { +"use strict"; + +var config = require("./config"); +var oop = require("./lib/oop"); +var lang = require("./lib/lang"); +var net = require("./lib/net"); +var EventEmitter = require("./lib/event_emitter").EventEmitter; +var Selection = require("./selection").Selection; +var TextMode = require("./mode/text").Mode; +var Range = require("./range").Range; +var Document = require("./document").Document; +var BackgroundTokenizer = require("./background_tokenizer").BackgroundTokenizer; + +/** + * class EditSession + * + * Stores various states related to a [[Document `Document`]]. A single `EditSession` can be in charge of several `Document`s. + * + **/ + +/** + * new EditSession(text, mode) + * - text (Document | String): If `text` is a `Document`, it associates the `EditSession` with it. Otherwise, a new `Document` is created, with the initial text + * - mode (TextMode): The inital language mode to use for the document + * + * Sets up a new `EditSession` and associates it with the given `Document` and `TextMode`. + * + **/ + +var EditSession = function(text, mode) { + this.$modified = true; + this.$breakpoints = []; + this.$frontMarkers = {}; + this.$backMarkers = {}; + this.$markerId = 1; + this.$rowCache = []; + this.$wrapData = []; + this.$foldData = []; + this.$undoSelect = true; + this.$foldData.toString = function() { + var str = ""; + this.forEach(function(foldLine) { + str += "\n" + foldLine.toString(); + }); + return str; + } + + if (text instanceof Document) { + this.setDocument(text); + } else { + this.setDocument(new Document(text)); + } + + this.selection = new Selection(this); + this.setMode(mode); +}; + + +(function() { + + oop.implement(this, EventEmitter); + + /** + * EditSession.setDocument(doc) + * - doc (Document): The new `Document` to use + * + * Sets the `EditSession` to point to a new `Document`. If a `BackgroundTokenizer` exists, it also points to `doc`. + * + **/ + this.setDocument = function(doc) { + if (this.doc) + throw new Error("Document is already set"); + + this.doc = doc; + doc.on("change", this.onChange.bind(this)); + this.on("changeFold", this.onChangeFold.bind(this)); + + if (this.bgTokenizer) { + this.bgTokenizer.setDocument(this.getDocument()); + this.bgTokenizer.start(0); + } + }; + + /** + * EditSession.getDocument() -> Document + * + * Returns the `Document` associated with this session. + * + **/ + this.getDocument = function() { + return this.doc; + }; + + /** internal, hide + * EditSession.$resetRowCache(row) + * - row (Number): The row to work with + * + * + * + **/ + this.$resetRowCache = function(row) { + if (row == 0) { + this.$rowCache = []; + return; + } + var rowCache = this.$rowCache; + for (var i = 0; i < rowCache.length; i++) { + if (rowCache[i].docRow >= row) { + rowCache.splice(i, rowCache.length); + return; + } + } + }; + + /** + * EditSession@onChangeFold(e) + * + * Emitted when a code fold changes its state. + * + **/ + this.onChangeFold = function(e) { + var fold = e.data; + this.$resetRowCache(fold.start.row); + }; + + /** + * EditSession@onChange(e) + * + * Emitted when the document changes. + **/ + this.onChange = function(e) { + var delta = e.data; + this.$modified = true; + + this.$resetRowCache(delta.range.start.row); + + var removedFolds = this.$updateInternalDataOnChange(e); + if (!this.$fromUndo && this.$undoManager && !delta.ignore) { + this.$deltasDoc.push(delta); + if (removedFolds && removedFolds.length != 0) { + this.$deltasFold.push({ + action: "removeFolds", + folds: removedFolds + }); + } + + this.$informUndoManager.schedule(); + } + + this.bgTokenizer.start(delta.range.start.row); + this._emit("change", e); + }; + + /** + * EditSession.setValue(text) + * - text (String): The new text to place + * + * Sets the session text. + * + **/ + this.setValue = function(text) { + this.doc.setValue(text); + this.selection.moveCursorTo(0, 0); + this.selection.clearSelection(); + + this.$resetRowCache(0); + this.$deltas = []; + this.$deltasDoc = []; + this.$deltasFold = []; + this.getUndoManager().reset(); + }; + + /** alias of: EditSession.toString + * EditSession.getValue() -> String + * + * Returns the current [[Document `Document`]] as a string. + * + **/ + /** alias of: EditSession.getValue + * EditSession.toString() -> String + * + * Returns the current [[Document `Document`]] as a string. + * + **/ + this.getValue = + this.toString = function() { + return this.doc.getValue(); + }; + + /** + * EditSession.getSelection() -> String + * + * Returns the string of the current selection. + **/ + this.getSelection = function() { + return this.selection; + }; + + /** related to: BackgroundTokenizer.getState + * EditSession.getState(row) -> Array + * - row (Number): The row to start at + * + * {:BackgroundTokenizer.getState} + * + **/ + this.getState = function(row) { + return this.bgTokenizer.getState(row); + }; + + /** related to: BackgroundTokenizer.getTokens + * EditSession.getTokens(firstRow, lastRow) -> Array + * - firstRow (Number): The row to start at + * - lastRow (Number): The row to finish at + * + * Starts tokenizing at the row indicated. Returns a list of objects of the tokenized rows. + * + **/ + this.getTokens = function(firstRow, lastRow) { + return this.bgTokenizer.getTokens(firstRow, lastRow); + }; + + /** + * EditSession.getTokenAt(row, column) -> Array + * - row (Number): The row number to retrieve from + * - column (Number): The column number to retrieve from + * + * Returns an array of tokens at the indicated row and column. + **/ + this.getTokenAt = function(row, column) { + var tokens = this.bgTokenizer.getTokens(row, row)[0].tokens; + var token, c = 0; + if (column == null) { + i = tokens.length - 1; + c = this.getLine(row).length; + } else { + for (var i = 0; i < tokens.length; i++) { + c += tokens[i].value.length; + if (c >= column) + break; + } + } + token = tokens[i]; + if (!token) + return null; + token.index = i; + token.start = c - token.value.length; + return token; + }; + + /** + * EditSession.setUndoManager(undoManager) + * - undoManager (UndoManager): The new undo manager + * + * Sets the undo manager. + **/ + this.setUndoManager = function(undoManager) { + this.$undoManager = undoManager; + this.$resetRowCache(0); + this.$deltas = []; + this.$deltasDoc = []; + this.$deltasFold = []; + + if (this.$informUndoManager) + this.$informUndoManager.cancel(); + + if (undoManager) { + var self = this; + /** internal, hide + * EditSession.$syncInformUndoManager() + * + * + **/ + this.$syncInformUndoManager = function() { + self.$informUndoManager.cancel(); + + if (self.$deltasFold.length) { + self.$deltas.push({ + group: "fold", + deltas: self.$deltasFold + }); + self.$deltasFold = []; + } + + if (self.$deltasDoc.length) { + self.$deltas.push({ + group: "doc", + deltas: self.$deltasDoc + }); + self.$deltasDoc = []; + } + + if (self.$deltas.length > 0) { + undoManager.execute({ + action: "aceupdate", + args: [self.$deltas, self] + }); + } + + self.$deltas = []; + } + this.$informUndoManager = + lang.deferredCall(this.$syncInformUndoManager); + } + }; + + this.$defaultUndoManager = { + undo: function() {}, + redo: function() {}, + reset: function() {} + }; + + /** + * EditSession.getUndoManager() -> UndoManager + * + * Returns the current undo manager. + **/ + this.getUndoManager = function() { + return this.$undoManager || this.$defaultUndoManager; + }, + + /** + * EditSession.getTabString() -> String + * + * Returns the current value for tabs. If the user is using soft tabs, this will be a series of spaces (defined by [[EditSession.getTabSize `getTabSize()`]]); otherwise it's simply `'\t'`. + **/ + this.getTabString = function() { + if (this.getUseSoftTabs()) { + return lang.stringRepeat(" ", this.getTabSize()); + } else { + return "\t"; + } + }; + + this.$useSoftTabs = true; + /** + * EditSession.setUseSoftTabs(useSoftTabs) + * - useSoftTabs (Boolean): Value indicating whether or not to use soft tabs + * + * Pass `true` to enable the use of soft tabs. Soft tabs means you're using spaces instead of the tab character (`'\t'`). + * + **/ + this.setUseSoftTabs = function(useSoftTabs) { + if (this.$useSoftTabs === useSoftTabs) return; + + this.$useSoftTabs = useSoftTabs; + }; + + /** + * EditSession.getUseSoftTabs() -> Boolean + * + * Returns `true` if soft tabs are being used, `false` otherwise. + * + **/ + this.getUseSoftTabs = function() { + return this.$useSoftTabs; + }; + + this.$tabSize = 4; + /** + * EditSession.setTabSize(tabSize) + * - tabSize (Number): The new tab size + * + * Set the number of spaces that define a soft tab; for example, passing in `4` transforms the soft tabs to be equivalent to four spaces. This function also emits the `changeTabSize` event. + **/ + this.setTabSize = function(tabSize) { + if (isNaN(tabSize) || this.$tabSize === tabSize) return; + + this.$modified = true; + this.$tabSize = tabSize; + this._emit("changeTabSize"); + }; + + /** + * EditSession.getTabSize() -> Number + * + * Returns the current tab size. + **/ + this.getTabSize = function() { + return this.$tabSize; + }; + + /** + * EditSession.isTabStop(position) -> Boolean + * - position (Object): The position to check + * + * Returns `true` if the character at the position is a soft tab. + **/ + this.isTabStop = function(position) { + return this.$useSoftTabs && (position.column % this.$tabSize == 0); + }; + + this.$overwrite = false; + /** + * EditSession.setOverwrite(overwrite) + * - overwrite (Boolean): Defines wheter or not to set overwrites + * + * Pass in `true` to enable overwrites in your session, or `false` to disable. If overwrites is enabled, any text you enter will type over any text after it. If the value of `overwrite` changes, this function also emites the `changeOverwrite` event. + * + **/ + this.setOverwrite = function(overwrite) { + if (this.$overwrite == overwrite) return; + + this.$overwrite = overwrite; + this._emit("changeOverwrite"); + }; + + /** + * EditSession.getOverwrite() -> Boolean + * + * Returns `true` if overwrites are enabled; `false` otherwise. + **/ + this.getOverwrite = function() { + return this.$overwrite; + }; + + /** + * EditSession.toggleOverwrite() + * + * Sets the value of overwrite to the opposite of whatever it currently is. + **/ + this.toggleOverwrite = function() { + this.setOverwrite(!this.$overwrite); + }; + + /** + * EditSession.getBreakpoints() -> Array + * + * Returns an array of numbers, indicating which rows have breakpoints. + **/ + this.getBreakpoints = function() { + return this.$breakpoints; + }; + + /** + * EditSession.setBreakpoints(rows) + * - rows (Array): An array of row indicies + * + * Sets a breakpoint on every row number given by `rows`. This function also emites the `'changeBreakpoint'` event. + * + **/ + this.setBreakpoints = function(rows) { + this.$breakpoints = []; + for (var i=0; i Number + * - range (Range): Define the range of the marker + * - clazz (String): Set the CSS class for the marker + * - type (Function | String): Identify the type of the marker + * - inFront (Boolean): Set to `true` to establish a front marker + * + * Adds a new marker to the given `Range`. If `inFront` is `true`, a front marker is defined, and the `'changeFrontMarker'` event fires; otherwise, the `'changeBackMarker'` event fires. + * + **/ + this.addMarker = function(range, clazz, type, inFront) { + var id = this.$markerId++; + + var marker = { + range : range, + type : type || "line", + renderer: typeof type == "function" ? type : null, + clazz : clazz, + inFront: !!inFront + } + + if (inFront) { + this.$frontMarkers[id] = marker; + this._emit("changeFrontMarker") + } else { + this.$backMarkers[id] = marker; + this._emit("changeBackMarker") + } + + return id; + }; + + /** + * EditSession.removeMarker(markerId) + * - markerId (Number): A number representing a marker + * + * Removes the marker with the specified ID. If this marker was in front, the `'changeFrontMarker'` event is emitted. If the marker was in the back, the `'changeBackMarker'` event is emitted. + * + **/ + this.removeMarker = function(markerId) { + var marker = this.$frontMarkers[markerId] || this.$backMarkers[markerId]; + if (!marker) + return; + + var markers = marker.inFront ? this.$frontMarkers : this.$backMarkers; + if (marker) { + delete (markers[markerId]); + this._emit(marker.inFront ? "changeFrontMarker" : "changeBackMarker"); + } + }; + + /** + * EditSession.getMarkers(inFront) -> Array + * - inFront (Boolean): If `true`, indicates you only want front markers; `false` indicates only back markers + * + * Returns an array containing the IDs of all the markers, either front or back. + * + **/ + this.getMarkers = function(inFront) { + return inFront ? this.$frontMarkers : this.$backMarkers; + }; + + /* + * Error: + * { + * row: 12, + * column: 2, //can be undefined + * text: "Missing argument", + * type: "error" // or "warning" or "info" + * } + */ + /** + * EditSession.setAnnotations(annotations) + * - annotations (Array): A list of annotations + * + * Sets annotations for the `EditSession`. This functions emits the `'changeAnnotation'` event. + **/ + this.setAnnotations = function(annotations) { + this.$annotations = {}; + for (var i=0; i Object + * + * Returns the annotations for the `EditSession`. + **/ + this.getAnnotations = function() { + return this.$annotations || {}; + }; + + /** + * EditSession.clearAnnotations() + * + * Clears all the annotations for this session. This function also triggers the `'changeAnnotation'` event. + **/ + this.clearAnnotations = function() { + this.$annotations = {}; + this._emit("changeAnnotation", {}); + }; + + /** internal, hide + * EditSession.$detectNewLine(text) + * - text (String): A block of text + * + * If `text` contains either the newline (`\n`) or carriage-return ('\r') characters, `$autoNewLine` stores that value. + * + **/ + this.$detectNewLine = function(text) { + var match = text.match(/^.*?(\r?\n)/m); + if (match) { + this.$autoNewLine = match[1]; + } else { + this.$autoNewLine = "\n"; + } + }; + + /** + * EditSession.getWordRange(row, column) -> Range + * - row (Number): The row to start at + * - column (Number): The column to start at + * + * Given a starting row and column, this method returns the `Range` of the first word boundary it finds. + * + **/ + this.getWordRange = function(row, column) { + var line = this.getLine(row); + + var inToken = false; + if (column > 0) { + inToken = !!line.charAt(column - 1).match(this.tokenRe); + } + + if (!inToken) { + inToken = !!line.charAt(column).match(this.tokenRe); + } + + var re = inToken ? this.tokenRe : this.nonTokenRe; + + var start = column; + if (start > 0) { + do { + start--; + } + while (start >= 0 && line.charAt(start).match(re)); + start++; + } + + var end = column; + while (end < line.length && line.charAt(end).match(re)) { + end++; + } + + return new Range(row, start, row, end); + }; + + /** + * EditSession.getAWordRange(row, column) -> Range + * - row (Number): The row number to start from + * - column (Number): The column number to start from + * + * Gets the range of a word, including its right whitespace. + **/ + this.getAWordRange = function(row, column) { + var wordRange = this.getWordRange(row, column); + var line = this.getLine(wordRange.end.row); + + while (line.charAt(wordRange.end.column).match(/[ \t]/)) { + wordRange.end.column += 1; + } + return wordRange; + }; + + /** related to: Document.setNewLineMode + * EditSession.setNewLineMode(newLineMode) + * - newLineMode (String): {:Document.setNewLineMode.param} + * + * {:Document.setNewLineMode.desc} + **/ + this.setNewLineMode = function(newLineMode) { + this.doc.setNewLineMode(newLineMode); + }; + + /** related to: Document.getNewLineMode + * EditSession.getNewLineMode() -> String + * + * Returns the current new line mode. + **/ + this.getNewLineMode = function() { + return this.doc.getNewLineMode(); + }; + + this.$useWorker = true; + + /** + * EditSession.setUseWorker(useWorker) + * - useWorker (Boolean): Set to `true` to use a worker + * + * Identifies if you want to use a worker for the `EditSession`. + * + **/ + this.setUseWorker = function(useWorker) { + if (this.$useWorker == useWorker) + return; + + this.$useWorker = useWorker; + + this.$stopWorker(); + if (useWorker) + this.$startWorker(); + }; + + /** + * EditSession.getUseWorker() -> Boolean + * + * Returns `true` if workers are being used. + **/ + this.getUseWorker = function() { + return this.$useWorker; + }; + + /** + * EditSession@onReloadTokenizer(e) + * + * Reloads all the tokens on the current session. This function calls [[BackgroundTokenizer.start `BackgroundTokenizer.start ()`]] to all the rows; it also emits the `'tokenizerUpdate'` event. + **/ + this.onReloadTokenizer = function(e) { + var rows = e.data; + this.bgTokenizer.start(rows.first); + this._emit("tokenizerUpdate", e); + }; + + this.$modes = {}; + this._loadMode = function(mode, callback) { + if (this.$modes[mode]) + return callback(this.$modes[mode]); + + var _self = this; + var module; + try { + module = require(mode); + } catch (e) {}; + if (module) + return done(module); + + fetch(function() { + require([mode], done); + }); + + function done(module) { + if (_self.$modes[mode]) + return callback(_self.$modes[mode]); + + _self.$modes[mode] = new module.Mode(); + _self.$modes[mode].$id = mode; + _self._emit("loadmode", { + name: mode, + mode: _self.$modes[mode] + }); + callback(_self.$modes[mode]); + } + + function fetch(callback) { + if (!config.get("packaged")) + return callback(); + + var base = mode.split("/").pop(); + var filename = config.get("modePath") + "/mode-" + base + config.get("suffix"); + net.loadScript(filename, callback); + } + }; + + /** + * EditSession.setMode(mode) + * - mode (TextMode): Set a new text mode + * + * Sets a new text mode for the `EditSession`. This method also emits the `'changeMode'` event. If a [[BackgroundTokenizer `BackgroundTokenizer`]] is set, the `'tokenizerUpdate'` event is also emitted. + * + **/ + this.$mode = null; + this.$modeId = null; + this.setMode = function(mode) { + // load on demand + if (typeof mode === "string") { + if (this.$modeId == mode) + return; + + this.$modeId = mode; + var _self = this; + this._loadMode(mode, function(module) { + if (_self.$modeId !== mode) + return; + + _self.setMode(module); + }); + return; + } else if (mode == null) { + mode = "ace/mode/text" + this.$modeId = mode; + this.$modes[mode] = this.$modes[mode] || (new TextMode()); + this.setMode(this.$modes[mode]); + return; + } + + if (this.$mode === mode) return; + this.$mode = mode; + this.$modeId = mode.$id; + + this.$stopWorker(); + + if (this.$useWorker) + this.$startWorker(); + + var tokenizer = mode.getTokenizer(); + + if(tokenizer.addEventListener !== undefined) { + var onReloadTokenizer = this.onReloadTokenizer.bind(this); + tokenizer.addEventListener("update", onReloadTokenizer); + } + + if (!this.bgTokenizer) { + this.bgTokenizer = new BackgroundTokenizer(tokenizer); + var _self = this; + this.bgTokenizer.addEventListener("update", function(e) { + _self._emit("tokenizerUpdate", e); + }); + } else { + this.bgTokenizer.setTokenizer(tokenizer); + } + + this.bgTokenizer.setDocument(this.getDocument()); + this.bgTokenizer.start(0); + + this.tokenRe = mode.tokenRe; + this.nonTokenRe = mode.nonTokenRe; + + this.$setFolding(mode.foldingRules); + + this._emit("changeMode"); + }; + + /** internal, hide + * EditSession.stopWorker() + * + * + **/ + this.$stopWorker = function() { + if (this.$worker) + this.$worker.terminate(); + + this.$worker = null; + }; + + /** internal, hide + * EditSession.$startWorker() + * + * + **/ + this.$startWorker = function() { + if (typeof Worker !== "undefined" && !require.noWorker) { + try { + this.$worker = this.$mode.createWorker(this); + } catch (e) { + console.log("Could not load worker"); + console.log(e); + this.$worker = null; + } + } + else + this.$worker = null; + }; + + /** + * EditSession.getMode() -> TextMode + * + * Returns the current text mode. + **/ + this.getMode = function() { + return this.$mode; + }; + + this.$scrollTop = 0; + /** + * EditSession.setScrollTop(scrollTop) + * - scrollTop (Number): The new scroll top value + * + * This function sets the scroll top value. It also emits the `'changeScrollTop'` event. + **/ + this.setScrollTop = function(scrollTop) { + scrollTop = Math.round(Math.max(0, scrollTop)); + if (this.$scrollTop === scrollTop) + return; + + this.$scrollTop = scrollTop; + this._emit("changeScrollTop", scrollTop); + }; + + /** + * EditSession.getScrollTop() -> Number + * + * [Returns the value of the distance between the top of the editor and the topmost part of the visible content.]{: #EditSession.getScrollTop} + **/ + this.getScrollTop = function() { + return this.$scrollTop; + }; + + this.$scrollLeft = 0; + /** + * EditSession.setScrollLeft(scrollLeft) + * + * [Sets the value of the distance between the left of the editor and the leftmost part of the visible content.]{: #EditSession.setScrollLeft} + **/ + this.setScrollLeft = function(scrollLeft) { + scrollLeft = Math.round(Math.max(0, scrollLeft)); + if (this.$scrollLeft === scrollLeft) + return; + + this.$scrollLeft = scrollLeft; + this._emit("changeScrollLeft", scrollLeft); + }; + + /** + * EditSession.getScrollLeft() -> Number + * + * [Returns the value of the distance between the left of the editor and the leftmost part of the visible content.]{: #EditSession.getScrollLeft} + **/ + this.getScrollLeft = function() { + return this.$scrollLeft; + }; + + /** + * EditSession.getWidth() -> Number + * + * Returns the width of the document. + **/ + this.getWidth = function() { + this.$computeWidth(); + return this.width; + }; + + /** + * EditSession.getScreenWidth() -> Number + * + * Returns the width of the screen. + **/ + this.getScreenWidth = function() { + this.$computeWidth(); + return this.screenWidth; + }; + + this.$computeWidth = function(force) { + if (this.$modified || force) { + this.$modified = false; + + var lines = this.doc.getAllLines(); + var longestLine = 0; + var longestScreenLine = 0; + + for ( var i = 0; i < lines.length; i++) { + var foldLine = this.getFoldLine(i), + line, len; + + line = lines[i]; + if (foldLine) { + var end = foldLine.range.end; + line = this.getFoldDisplayLine(foldLine); + // Continue after the foldLine.end.row. All the lines in + // between are folded. + i = end.row; + } + len = line.length; + longestLine = Math.max(longestLine, len); + if (!this.$useWrapMode) { + longestScreenLine = Math.max( + longestScreenLine, + this.$getStringScreenWidth(line)[0] + ); + } + } + this.width = longestLine; + + if (this.$useWrapMode) { + this.screenWidth = this.$wrapLimit; + } else { + this.screenWidth = longestScreenLine; + } + } + }; + + /** related to: Document.getLine + * EditSession.getLine(row) -> String + * - row (Number): The row to retrieve from + * + * Returns a verbatim copy of the given line as it is in the document + * + **/ + this.getLine = function(row) { + return this.doc.getLine(row); + }; + + /** related to: Document.getLines + * EditSession.getLines(firstRow, lastRow) -> Array + * - firstRow (Number): The first row index to retrieve + * - lastRow (Number): The final row index to retrieve + * + * Returns an array of strings of the rows between `firstRow` and `lastRow`. This function is inclusive of `lastRow`. + * + **/ + this.getLines = function(firstRow, lastRow) { + return this.doc.getLines(firstRow, lastRow); + }; + + /** related to: Document.getLength + * EditSession.getLength()-> Number + * + * Returns the number of rows in the document. + **/ + this.getLength = function() { + return this.doc.getLength(); + }; + + /** related to: Document.getTextRange + * EditSession.getTextRange(range) -> Array + * - range (String): The range to work with + * + * {:Document.getTextRange.desc} + **/ + this.getTextRange = function(range) { + return this.doc.getTextRange(range); + }; + + /** related to: Document.insert + * EditSession.insert(position, text) -> Number + * - position (Number): The position to start inserting at + * - text (String): A chunk of text to insert + * + (Number): The position of the last line of `text`. If the length of `text` is 0, this function simply returns `position`. + * + * Inserts a block of `text` and the indicated `position`. + * + * + **/ + this.insert = function(position, text) { + return this.doc.insert(position, text); + }; + + /** related to: Document.remove + * EditSession.remove(range) -> Object + * - range (Range): A specified Range to remove + * + (Object): The new `start` property of the range, which contains `startRow` and `startColumn`. If `range` is empty, this function returns the unmodified value of `range.start`. + * + * Removes the `range` from the document. + * + * + **/ + this.remove = function(range) { + return this.doc.remove(range); + }; + + /** + * EditSession.undoChanges(deltas, dontSelect) -> Range + * - deltas (Array): An array of previous changes + * - dontSelect (Boolean): [If `true`, doesn't select the range of where the change occured]{: #dontSelect} + * + * Reverts previous changes to your document. + **/ + this.undoChanges = function(deltas, dontSelect) { + if (!deltas.length) + return; + + this.$fromUndo = true; + var lastUndoRange = null; + for (var i = deltas.length - 1; i != -1; i--) { + var delta = deltas[i]; + if (delta.group == "doc") { + this.doc.revertDeltas(delta.deltas); + lastUndoRange = + this.$getUndoSelection(delta.deltas, true, lastUndoRange); + } else { + delta.deltas.forEach(function(foldDelta) { + this.addFolds(foldDelta.folds); + }, this); + } + } + this.$fromUndo = false; + lastUndoRange && + this.$undoSelect && + !dontSelect && + this.selection.setSelectionRange(lastUndoRange); + return lastUndoRange; + }; + + /** + * EditSession.redoChanges(deltas, dontSelect) -> Range + * - deltas (Array): An array of previous changes + * - dontSelect (Boolean): {:dontSelect} + * + * Re-implements a previously undone change to your document. + **/ + this.redoChanges = function(deltas, dontSelect) { + if (!deltas.length) + return; + + this.$fromUndo = true; + var lastUndoRange = null; + for (var i = 0; i < deltas.length; i++) { + var delta = deltas[i]; + if (delta.group == "doc") { + this.doc.applyDeltas(delta.deltas); + lastUndoRange = + this.$getUndoSelection(delta.deltas, false, lastUndoRange); + } + } + this.$fromUndo = false; + lastUndoRange && + this.$undoSelect && + !dontSelect && + this.selection.setSelectionRange(lastUndoRange); + return lastUndoRange; + }; + + /** + * EditSession.setUndoSelect(enable) + * - enable (Boolean): If `true`, selects the range of the reinserted change + * + * ENables or disables highlighting of the range where an undo occured. + **/ + this.setUndoSelect = function(enable) { + this.$undoSelect = enable; + }; + + /** internal, hide + * EditSession.$getUndoSelection(deltas, isUndo, lastUndoRange) -> Range + * + * + **/ + this.$getUndoSelection = function(deltas, isUndo, lastUndoRange) { + function isInsert(delta) { + var insert = + delta.action == "insertText" || delta.action == "insertLines"; + return isUndo ? !insert : insert; + } + + var delta = deltas[0]; + var range, point; + var lastDeltaIsInsert = false; + if (isInsert(delta)) { + range = delta.range.clone(); + lastDeltaIsInsert = true; + } else { + range = Range.fromPoints(delta.range.start, delta.range.start); + lastDeltaIsInsert = false; + } + + for (var i = 1; i < deltas.length; i++) { + delta = deltas[i]; + if (isInsert(delta)) { + point = delta.range.start; + if (range.compare(point.row, point.column) == -1) { + range.setStart(delta.range.start); + } + point = delta.range.end; + if (range.compare(point.row, point.column) == 1) { + range.setEnd(delta.range.end); + } + lastDeltaIsInsert = true; + } else { + point = delta.range.start; + if (range.compare(point.row, point.column) == -1) { + range = + Range.fromPoints(delta.range.start, delta.range.start); + } + lastDeltaIsInsert = false; + } + } + + // Check if this range and the last undo range has something in common. + // If true, merge the ranges. + if (lastUndoRange != null) { + var cmp = lastUndoRange.compareRange(range); + if (cmp == 1) { + range.setStart(lastUndoRange.start); + } else if (cmp == -1) { + range.setEnd(lastUndoRange.end); + } + } + + return range; + }, + + /** related to: Document.replace + * EditSession.replace(range, text) -> Object + * - range (Range): A specified Range to replace + * - text (String): The new text to use as a replacement + * + (Object): Returns an object containing the final row and column, like this:
+ * ```{row: endRow, column: 0}```
+ * If the text and range are empty, this function returns an object containing the current `range.start` value.
+ * If the text is the exact same as what currently exists, this function returns an object containing the current `range.end` value. + * + * Replaces a range in the document with the new `text`. + * + * + * + **/ + this.replace = function(range, text) { + return this.doc.replace(range, text); + }; + + /** + * EditSession.moveText(fromRange, toPosition) -> Range + * - fromRange (Range): The range of text you want moved within the document + * - toPosition (Object): The location (row and column) where you want to move the text to + * + (Range): The new range where the text was moved to. + * Moves a range of text from the given range to the given position. `toPosition` is an object that looks like this: + * + * { row: newRowLocation, column: newColumnLocation } + * + * + * + **/ + this.moveText = function(fromRange, toPosition) { + var text = this.getTextRange(fromRange); + this.remove(fromRange); + + var toRow = toPosition.row; + var toColumn = toPosition.column; + + // Make sure to update the insert location, when text is removed in + // front of the chosen point of insertion. + if (!fromRange.isMultiLine() && fromRange.start.row == toRow && + fromRange.end.column < toColumn) + toColumn -= text.length; + + if (fromRange.isMultiLine() && fromRange.end.row < toRow) { + var lines = this.doc.$split(text); + toRow -= lines.length - 1; + } + + var endRow = toRow + fromRange.end.row - fromRange.start.row; + var endColumn = fromRange.isMultiLine() ? + fromRange.end.column : + toColumn + fromRange.end.column - fromRange.start.column; + + var toRange = new Range(toRow, toColumn, endRow, endColumn); + + this.insert(toRange.start, text); + + return toRange; + }; + + /** + * EditSession.indentRows(startRow, endRow, indentString) + * - startRow (Number): Starting row + * - endRow (Number): Ending row + * - indentString (String): The indent token + * + * Indents all the rows, from `startRow` to `endRow` (inclusive), by prefixing each row with the token in `indentString`. + * + * If `indentString` contains the `'\t'` character, it's replaced by whatever is defined by [[EditSession.getTabString `getTabString()`]]. + * + **/ + this.indentRows = function(startRow, endRow, indentString) { + indentString = indentString.replace(/\t/g, this.getTabString()); + for (var row=startRow; row<=endRow; row++) + this.insert({row: row, column:0}, indentString); + }; + + /** + * EditSession.outdentRows(range) + * - range (Range): A range of rows + * + * Outdents all the rows defined by the `start` and `end` properties of `range`. + * + **/ + this.outdentRows = function (range) { + var rowRange = range.collapseRows(); + var deleteRange = new Range(0, 0, 0, 0); + var size = this.getTabSize(); + + for (var i = rowRange.start.row; i <= rowRange.end.row; ++i) { + var line = this.getLine(i); + + deleteRange.start.row = i; + deleteRange.end.row = i; + for (var j = 0; j < size; ++j) + if (line.charAt(j) != ' ') + break; + if (j < size && line.charAt(j) == '\t') { + deleteRange.start.column = j; + deleteRange.end.column = j + 1; + } else { + deleteRange.start.column = 0; + deleteRange.end.column = j; + } + this.remove(deleteRange); + } + }; + + /** related to: Document.insertLines + * EditSession.moveLinesUp(firstRow, lastRow) -> Number + * - firstRow (Number): The starting row to move up + * - lastRow (Number): The final row to move up + * + (Number): If `firstRow` is less-than or equal to 0, this function returns 0. Otherwise, on success, it returns -1. + * + * Shifts all the lines in the document up one, starting from `firstRow` and ending at `lastRow`. + * + * + **/ + this.moveLinesUp = function(firstRow, lastRow) { + if (firstRow <= 0) return 0; + + var removed = this.doc.removeLines(firstRow, lastRow); + this.doc.insertLines(firstRow - 1, removed); + return -1; + }; + + /** related to: Document.insertLines + * EditSession.moveLinesDown(firstRow, lastRow) -> Number + * - firstRow (Number): The starting row to move down + * - lastRow (Number): The final row to move down + * + (Number): If `firstRow` is less-than or equal to 0, this function returns 0. Otherwise, on success, it returns -1. + * + * + * + **/ + this.moveLinesDown = function(firstRow, lastRow) { + if (lastRow >= this.doc.getLength()-1) return 0; + + var removed = this.doc.removeLines(firstRow, lastRow); + this.doc.insertLines(firstRow+1, removed); + return 1; + }; + + /** + * EditSession.duplicateLines(firstRow, lastRow) -> Number + * - firstRow (Number): The starting row to duplicate + * - lastRow (Number): The final row to duplicate + * + (Number): Returns the number of new rows added; in other words, `lastRow - firstRow + 1`. + * + * Duplicates all the text between `firstRow` and `lastRow`. + * + * + * + **/ + this.duplicateLines = function(firstRow, lastRow) { + var firstRow = this.$clipRowToDocument(firstRow); + var lastRow = this.$clipRowToDocument(lastRow); + + var lines = this.getLines(firstRow, lastRow); + this.doc.insertLines(firstRow, lines); + + var addedRows = lastRow - firstRow + 1; + return addedRows; + }; + + + this.$clipRowToDocument = function(row) { + return Math.max(0, Math.min(row, this.doc.getLength()-1)); + }; + + this.$clipColumnToRow = function(row, column) { + if (column < 0) + return 0; + return Math.min(this.doc.getLine(row).length, column); + }; + + + this.$clipPositionToDocument = function(row, column) { + column = Math.max(0, column); + + if (row < 0) { + row = 0; + column = 0; + } else { + var len = this.doc.getLength(); + if (row >= len) { + row = len - 1; + column = this.doc.getLine(len-1).length; + } else { + column = Math.min(this.doc.getLine(row).length, column); + } + } + + return { + row: row, + column: column + }; + }; + + this.$clipRangeToDocument = function(range) { + if (range.start.row < 0) { + range.start.row = 0; + range.start.column = 0 + } else { + range.start.column = this.$clipColumnToRow( + range.start.row, + range.start.column + ); + } + + var len = this.doc.getLength() - 1; + if (range.end.row > len) { + range.end.row = len; + range.end.column = this.doc.getLine(len).length; + } else { + range.end.column = this.$clipColumnToRow( + range.end.row, + range.end.column + ); + } + return range; + }; + + // WRAPMODE + this.$wrapLimit = 80; + this.$useWrapMode = false; + this.$wrapLimitRange = { + min : null, + max : null + }; + + /** + * EditSession.setUseWrapMode(useWrapMode) + * - useWrapMode (Boolean): Enable (or disable) wrap mode + * + * Sets whether or not line wrapping is enabled. If `useWrapMode` is different than the current value, the `'changeWrapMode'` event is emitted. + **/ + this.setUseWrapMode = function(useWrapMode) { + if (useWrapMode != this.$useWrapMode) { + this.$useWrapMode = useWrapMode; + this.$modified = true; + this.$resetRowCache(0); + + // If wrapMode is activaed, the wrapData array has to be initialized. + if (useWrapMode) { + var len = this.getLength(); + this.$wrapData = []; + for (var i = 0; i < len; i++) { + this.$wrapData.push([]); + } + this.$updateWrapData(0, len - 1); + } + + this._emit("changeWrapMode"); + } + }; + + /** + * EditSession.getUseWrapMode() -> Boolean + * + * Returns `true` if wrap mode is being used; `false` otherwise. + **/ + this.getUseWrapMode = function() { + return this.$useWrapMode; + }; + + // Allow the wrap limit to move freely between min and max. Either + // parameter can be null to allow the wrap limit to be unconstrained + // in that direction. Or set both parameters to the same number to pin + // the limit to that value. + /** + * EditSession.setWrapLimitRange(min, max) + * - min (Number): The minimum wrap value (the left side wrap) + * - max (Number): The maximum wrap value (the right side wrap) + * + * Sets the boundaries of wrap. Either value can be `null` to have an unconstrained wrap, or, they can be the same number to pin the limit. If the wrap limits for `min` or `max` are different, this method also emits the `'changeWrapMode'` event. + **/ + this.setWrapLimitRange = function(min, max) { + if (this.$wrapLimitRange.min !== min || this.$wrapLimitRange.max !== max) { + this.$wrapLimitRange.min = min; + this.$wrapLimitRange.max = max; + this.$modified = true; + // This will force a recalculation of the wrap limit + this._emit("changeWrapMode"); + } + }; + + /** internal, hide + * EditSession.adjustWrapLimit(desiredLimit) -> Boolean + * - desiredLimit (Number): The new wrap limit + * + * This should generally only be called by the renderer when a resize is detected. + **/ + this.adjustWrapLimit = function(desiredLimit) { + var wrapLimit = this.$constrainWrapLimit(desiredLimit); + if (wrapLimit != this.$wrapLimit && wrapLimit > 0) { + this.$wrapLimit = wrapLimit; + this.$modified = true; + if (this.$useWrapMode) { + this.$updateWrapData(0, this.getLength() - 1); + this.$resetRowCache(0) + this._emit("changeWrapLimit"); + } + return true; + } + return false; + }; + + /** internal, hide + * EditSession.$constrainWrapLimit(wrapLimit) + * + * + **/ + this.$constrainWrapLimit = function(wrapLimit) { + var min = this.$wrapLimitRange.min; + if (min) + wrapLimit = Math.max(min, wrapLimit); + + var max = this.$wrapLimitRange.max; + if (max) + wrapLimit = Math.min(max, wrapLimit); + + // What would a limit of 0 even mean? + return Math.max(1, wrapLimit); + }; + + /** + * EditSession.getWrapLimit() -> Number + * + * Returns the value of wrap limit. + **/ + this.getWrapLimit = function() { + return this.$wrapLimit; + }; + + /** + * EditSession.getWrapLimitRange() -> Object + * + * Returns an object that defines the minimum and maximum of the wrap limit; it looks something like this: + * + * { min: wrapLimitRange_min, max: wrapLimitRange_max } + * + **/ + this.getWrapLimitRange = function() { + // Avoid unexpected mutation by returning a copy + return { + min : this.$wrapLimitRange.min, + max : this.$wrapLimitRange.max + }; + }; + + /** internal, hide + * EditSession.$updateInternalDataOnChange() + * + * + **/ + this.$updateInternalDataOnChange = function(e) { + var useWrapMode = this.$useWrapMode; + var len; + var action = e.data.action; + var firstRow = e.data.range.start.row; + var lastRow = e.data.range.end.row; + var start = e.data.range.start; + var end = e.data.range.end; + var removedFolds = null; + + if (action.indexOf("Lines") != -1) { + if (action == "insertLines") { + lastRow = firstRow + (e.data.lines.length); + } else { + lastRow = firstRow; + } + len = e.data.lines ? e.data.lines.length : lastRow - firstRow; + } else { + len = lastRow - firstRow; + } + + if (len != 0) { + if (action.indexOf("remove") != -1) { + useWrapMode && this.$wrapData.splice(firstRow, len); + + var foldLines = this.$foldData; + removedFolds = this.getFoldsInRange(e.data.range); + this.removeFolds(removedFolds); + + var foldLine = this.getFoldLine(end.row); + var idx = 0; + if (foldLine) { + foldLine.addRemoveChars(end.row, end.column, start.column - end.column); + foldLine.shiftRow(-len); + + var foldLineBefore = this.getFoldLine(firstRow); + if (foldLineBefore && foldLineBefore !== foldLine) { + foldLineBefore.merge(foldLine); + foldLine = foldLineBefore; + } + idx = foldLines.indexOf(foldLine) + 1; + } + + for (idx; idx < foldLines.length; idx++) { + var foldLine = foldLines[idx]; + if (foldLine.start.row >= end.row) { + foldLine.shiftRow(-len); + } + } + + lastRow = firstRow; + } else { + var args; + if (useWrapMode) { + args = [firstRow, 0]; + for (var i = 0; i < len; i++) args.push([]); + this.$wrapData.splice.apply(this.$wrapData, args); + } + + // If some new line is added inside of a foldLine, then split + // the fold line up. + var foldLines = this.$foldData; + var foldLine = this.getFoldLine(firstRow); + var idx = 0; + if (foldLine) { + var cmp = foldLine.range.compareInside(start.row, start.column) + // Inside of the foldLine range. Need to split stuff up. + if (cmp == 0) { + foldLine = foldLine.split(start.row, start.column); + foldLine.shiftRow(len); + foldLine.addRemoveChars( + lastRow, 0, end.column - start.column); + } else + // Infront of the foldLine but same row. Need to shift column. + if (cmp == -1) { + foldLine.addRemoveChars(firstRow, 0, end.column - start.column); + foldLine.shiftRow(len); + } + // Nothing to do if the insert is after the foldLine. + idx = foldLines.indexOf(foldLine) + 1; + } + + for (idx; idx < foldLines.length; idx++) { + var foldLine = foldLines[idx]; + if (foldLine.start.row >= firstRow) { + foldLine.shiftRow(len); + } + } + } + } else { + // Realign folds. E.g. if you add some new chars before a fold, the + // fold should "move" to the right. + len = Math.abs(e.data.range.start.column - e.data.range.end.column); + if (action.indexOf("remove") != -1) { + // Get all the folds in the change range and remove them. + removedFolds = this.getFoldsInRange(e.data.range); + this.removeFolds(removedFolds); + + len = -len; + } + var foldLine = this.getFoldLine(firstRow); + if (foldLine) { + foldLine.addRemoveChars(firstRow, start.column, len); + } + } + + if (useWrapMode && this.$wrapData.length != this.doc.getLength()) { + console.error("doc.getLength() and $wrapData.length have to be the same!"); + } + + useWrapMode && this.$updateWrapData(firstRow, lastRow); + + return removedFolds; + }; + + /** internal, hide + * EditSession.$updateWrapData(firstRow, lastRow) + * + * + **/ + this.$updateWrapData = function(firstRow, lastRow) { + var lines = this.doc.getAllLines(); + var tabSize = this.getTabSize(); + var wrapData = this.$wrapData; + var wrapLimit = this.$wrapLimit; + var tokens; + var foldLine; + + var row = firstRow; + lastRow = Math.min(lastRow, lines.length - 1); + while (row <= lastRow) { + foldLine = this.getFoldLine(row, foldLine); + if (!foldLine) { + tokens = this.$getDisplayTokens(lang.stringTrimRight(lines[row])); + wrapData[row] = this.$computeWrapSplits(tokens, wrapLimit, tabSize); + row ++; + } else { + tokens = []; + foldLine.walk( + function(placeholder, row, column, lastColumn) { + var walkTokens; + if (placeholder) { + walkTokens = this.$getDisplayTokens( + placeholder, tokens.length); + walkTokens[0] = PLACEHOLDER_START; + for (var i = 1; i < walkTokens.length; i++) { + walkTokens[i] = PLACEHOLDER_BODY; + } + } else { + walkTokens = this.$getDisplayTokens( + lines[row].substring(lastColumn, column), + tokens.length); + } + tokens = tokens.concat(walkTokens); + }.bind(this), + foldLine.end.row, + lines[foldLine.end.row].length + 1 + ); + // Remove spaces/tabs from the back of the token array. + while (tokens.length != 0 && tokens[tokens.length - 1] >= SPACE) + tokens.pop(); + + wrapData[foldLine.start.row] + = this.$computeWrapSplits(tokens, wrapLimit, tabSize); + row = foldLine.end.row + 1; + } + } + }; + + // "Tokens" + var CHAR = 1, + CHAR_EXT = 2, + PLACEHOLDER_START = 3, + PLACEHOLDER_BODY = 4, + PUNCTUATION = 9, + SPACE = 10, + TAB = 11, + TAB_SPACE = 12; + + /** internal, hide + * EditSession.$computeWrapSplits(tokens, wrapLimit) -> Array + * + * + **/ + this.$computeWrapSplits = function(tokens, wrapLimit) { + if (tokens.length == 0) { + return []; + } + + var splits = []; + var displayLength = tokens.length; + var lastSplit = 0, lastDocSplit = 0; + + function addSplit(screenPos) { + var displayed = tokens.slice(lastSplit, screenPos); + + // The document size is the current size - the extra width for tabs + // and multipleWidth characters. + var len = displayed.length; + displayed.join(""). + // Get all the TAB_SPACEs. + replace(/12/g, function() { + len -= 1; + }). + // Get all the CHAR_EXT/multipleWidth characters. + replace(/2/g, function() { + len -= 1; + }); + + lastDocSplit += len; + splits.push(lastDocSplit); + lastSplit = screenPos; + } + + while (displayLength - lastSplit > wrapLimit) { + // This is, where the split should be. + var split = lastSplit + wrapLimit; + + // If there is a space or tab at this split position, then making + // a split is simple. + if (tokens[split] >= SPACE) { + // Include all following spaces + tabs in this split as well. + while (tokens[split] >= SPACE) { + split ++; + } + addSplit(split); + continue; + } + + // === ELSE === + // Check if split is inside of a placeholder. Placeholder are + // not splitable. Therefore, seek the beginning of the placeholder + // and try to place the split beofre the placeholder's start. + if (tokens[split] == PLACEHOLDER_START + || tokens[split] == PLACEHOLDER_BODY) + { + // Seek the start of the placeholder and do the split + // before the placeholder. By definition there always + // a PLACEHOLDER_START between split and lastSplit. + for (split; split != lastSplit - 1; split--) { + if (tokens[split] == PLACEHOLDER_START) { + // split++; << No incremental here as we want to + // have the position before the Placeholder. + break; + } + } + + // If the PLACEHOLDER_START is not the index of the + // last split, then we can do the split + if (split > lastSplit) { + addSplit(split); + continue; + } + + // If the PLACEHOLDER_START IS the index of the last + // split, then we have to place the split after the + // placeholder. So, let's seek for the end of the placeholder. + split = lastSplit + wrapLimit; + for (split; split < tokens.length; split++) { + if (tokens[split] != PLACEHOLDER_BODY) + { + break; + } + } + + // If spilt == tokens.length, then the placeholder is the last + // thing in the line and adding a new split doesn't make sense. + if (split == tokens.length) { + break; // Breaks the while-loop. + } + + // Finally, add the split... + addSplit(split); + continue; + } + + // === ELSE === + // Search for the first non space/tab/placeholder/punctuation token backwards. + var minSplit = Math.max(split - 10, lastSplit - 1); + while (split > minSplit && tokens[split] < PLACEHOLDER_START) { + split --; + } + while (split > minSplit && tokens[split] == PUNCTUATION) { + split --; + } + // If we found one, then add the split. + if (split > minSplit) { + addSplit(++split); + continue; + } + + // === ELSE === + split = lastSplit + wrapLimit; + // The split is inside of a CHAR or CHAR_EXT token and no space + // around -> force a split. + addSplit(split); + } + return splits; + } + + /** internal, hide + * EditSession.$getDisplayTokens(str, offset) -> Array + * - str (String): The string to check + * - offset (Number): The value to start at + * + * Given a string, returns an array of the display characters, including tabs and spaces. + **/ + this.$getDisplayTokens = function(str, offset) { + var arr = []; + var tabSize; + offset = offset || 0; + + for (var i = 0; i < str.length; i++) { + var c = str.charCodeAt(i); + // Tab + if (c == 9) { + tabSize = this.getScreenTabSize(arr.length + offset); + arr.push(TAB); + for (var n = 1; n < tabSize; n++) { + arr.push(TAB_SPACE); + } + } + // Space + else if (c == 32) { + arr.push(SPACE); + } else if((c > 39 && c < 48) || (c > 57 && c < 64)) { + arr.push(PUNCTUATION); + } + // full width characters + else if (c >= 0x1100 && isFullWidth(c)) { + arr.push(CHAR, CHAR_EXT); + } else { + arr.push(CHAR); + } + } + return arr; + } + + /** internal, hide + * EditSession.$getStringScreenWidth(str, maxScreenColumn, screenColumn) -> [Number] + * - str (String): The string to calculate the screen width of + * - maxScreenColumn (Number): + * - screenColumn (Number): + * + ([Number]): Returns an `int[]` array with two elements:
+ * The first position indicates the number of columns for `str` on screen.
+ * The second value contains the position of the document column that this function read until. + * + * Calculates the width of the string `str` on the screen while assuming that the string starts at the first column on the screen. + * + * + **/ + this.$getStringScreenWidth = function(str, maxScreenColumn, screenColumn) { + if (maxScreenColumn == 0) { + return [0, 0]; + } + if (maxScreenColumn == null) { + maxScreenColumn = screenColumn + + str.length * Math.max(this.getTabSize(), 2); + } + screenColumn = screenColumn || 0; + + var c, column; + for (column = 0; column < str.length; column++) { + c = str.charCodeAt(column); + // tab + if (c == 9) { + screenColumn += this.getScreenTabSize(screenColumn); + } + // full width characters + else if (c >= 0x1100 && isFullWidth(c)) { + screenColumn += 2; + } else { + screenColumn += 1; + } + if (screenColumn > maxScreenColumn) { + break + } + } + + return [screenColumn, column]; + } + + /** + * EditSession.getRowLength(row) -> Number + * - row (Number): The row number to check + * + * + * Returns the length of the indicated row. + **/ + this.getRowLength = function(row) { + if (!this.$useWrapMode || !this.$wrapData[row]) { + return 1; + } else { + return this.$wrapData[row].length + 1; + } + } + + /** + * EditSession.getRowHeight(config, row) -> Number + * - config (Object): An object containing a parameter indicating the `lineHeight`. + * - row (Number): The row number to check + * + * Returns the height of the indicated row. This is mostly relevant for situations where wrapping occurs, and a single line spans across multiple rows. + * + **/ + this.getRowHeight = function(config, row) { + return this.getRowLength(row) * config.lineHeight; + } + + /** internal, hide, related to: EditSession.documentToScreenColumn + * EditSession.getScreenLastRowColumn(screenRow) -> Number + * - screenRow (Number): The screen row to check + * + * Returns the column position (on screen) for the last character in the provided row. + **/ + this.getScreenLastRowColumn = function(screenRow) { + var pos = this.screenToDocumentPosition(screenRow, Number.MAX_VALUE) + return this.documentToScreenColumn(pos.row, pos.column); + }; + + /** internal, hide + * EditSession.getDocumentLastRowColumn(docRow, docColumn) -> Number + * - docRow (Number): + * - docColumn (Number): + * + **/ + this.getDocumentLastRowColumn = function(docRow, docColumn) { + var screenRow = this.documentToScreenRow(docRow, docColumn); + return this.getScreenLastRowColumn(screenRow); + }; + + /** internal, hide + * EditSession.getDocumentLastRowColumnPosition(docRow, docColumn) -> Number + * + **/ + this.getDocumentLastRowColumnPosition = function(docRow, docColumn) { + var screenRow = this.documentToScreenRow(docRow, docColumn); + return this.screenToDocumentPosition(screenRow, Number.MAX_VALUE / 10); + }; + + /** internal, hide + * EditSession.getRowSplitData(row) -> undefined | String + * + **/ + this.getRowSplitData = function(row) { + if (!this.$useWrapMode) { + return undefined; + } else { + return this.$wrapData[row]; + } + }; + + /** + * EditSession.getScreenTabSize(screenColumn) -> Number + * - screenColumn (Number): The screen column to check + * + * The distance to the next tab stop at the specified screen column. + **/ + this.getScreenTabSize = function(screenColumn) { + return this.$tabSize - screenColumn % this.$tabSize; + }; + + /** internal, hide + * EditSession.screenToDocumentRow(screenRow, screenColumn) -> Number + * + * + **/ + this.screenToDocumentRow = function(screenRow, screenColumn) { + return this.screenToDocumentPosition(screenRow, screenColumn).row; + }; + + /** internal, hide + * EditSession.screenToDocumentColumn(screenRow, screenColumn) -> Number + * + * + **/ + this.screenToDocumentColumn = function(screenRow, screenColumn) { + return this.screenToDocumentPosition(screenRow, screenColumn).column; + }; + + /** related to: EditSession.documentToScreenPosition + * EditSession.screenToDocumentPosition(screenRow, screenColumn) -> Object + * - screenRow (Number): The screen row to check + * - screenColumn (Number): The screen column to check + * + (Object): The object returned has two properties: `row` and `column`. + * + * Converts characters coordinates on the screen to characters coordinates within the document. [This takes into account code folding, word wrap, tab size, and any other visual modifications.]{: #conversionConsiderations} + * + * + **/ + this.screenToDocumentPosition = function(screenRow, screenColumn) { + if (screenRow < 0) { + return { + row: 0, + column: 0 + } + } + + var line; + var docRow = 0; + var docColumn = 0; + var column; + var row = 0; + var rowLength = 0; + + var rowCache = this.$rowCache; + for (var i = 0; i < rowCache.length; i++) { + if (rowCache[i].screenRow < screenRow) { + row = rowCache[i].screenRow; + docRow = rowCache[i].docRow; + } + else { + break; + } + } + var doCache = !rowCache.length || i == rowCache.length; + + var maxRow = this.getLength() - 1; + var foldLine = this.getNextFoldLine(docRow); + var foldStart = foldLine ? foldLine.start.row : Infinity; + + while (row <= screenRow) { + rowLength = this.getRowLength(docRow); + if (row + rowLength - 1 >= screenRow || docRow >= maxRow) { + break; + } else { + row += rowLength; + docRow++; + if (docRow > foldStart) { + docRow = foldLine.end.row+1; + foldLine = this.getNextFoldLine(docRow, foldLine); + foldStart = foldLine ? foldLine.start.row : Infinity; + } + } + if (doCache) { + rowCache.push({ + docRow: docRow, + screenRow: row + }); + } + } + + if (foldLine && foldLine.start.row <= docRow) { + line = this.getFoldDisplayLine(foldLine); + docRow = foldLine.start.row; + } else if (row + rowLength <= screenRow || docRow > maxRow) { + // clip at the end of the document + return { + row: maxRow, + column: this.getLine(maxRow).length + } + } else { + line = this.getLine(docRow); + foldLine = null; + } + + if (this.$useWrapMode) { + var splits = this.$wrapData[docRow]; + if (splits) { + column = splits[screenRow - row]; + if(screenRow > row && splits.length) { + docColumn = splits[screenRow - row - 1] || splits[splits.length - 1]; + line = line.substring(docColumn); + } + } + } + + docColumn += this.$getStringScreenWidth(line, screenColumn)[1]; + + // We remove one character at the end so that the docColumn + // position returned is not associated to the next row on the screen. + if (this.$useWrapMode && docColumn >= column) { + docColumn = column - 1; + } + + if (foldLine) { + return foldLine.idxToPosition(docColumn); + } + + return { + row: docRow, + column: docColumn + } + }; + + /** related to: EditSession.screenToDocumentPosition + * EditSession.documentToScreenPosition(docRow, docColumn) -> Object + * - docRow (Number): The document row to check + * - docColumn (Number): The document column to check + * + (Object): The object returned by this method has two properties: `row` and `column`. + * + * Converts document coordinates to screen coordinates. {:conversionConsiderations} + * + * + * + **/ + this.documentToScreenPosition = function(docRow, docColumn) { + // Normalize the passed in arguments. + if (typeof docColumn === "undefined") + var pos = this.$clipPositionToDocument(docRow.row, docRow.column); + else + pos = this.$clipPositionToDocument(docRow, docColumn); + + docRow = pos.row; + docColumn = pos.column; + + var wrapData; + // Special case in wrapMode if the doc is at the end of the document. + if (this.$useWrapMode) { + wrapData = this.$wrapData; + if (docRow > wrapData.length - 1) { + return { + row: this.getScreenLength(), + column: wrapData.length == 0 + ? 0 + : (wrapData[wrapData.length - 1].length - 1) + }; + } + } + + var screenRow = 0; + var foldStartRow = null; + var fold = null; + + // Clamp the docRow position in case it's inside of a folded block. + fold = this.getFoldAt(docRow, docColumn, 1); + if (fold) { + docRow = fold.start.row; + docColumn = fold.start.column; + } + + var rowEnd, row = 0; + var rowCache = this.$rowCache; + + for (var i = 0; i < rowCache.length; i++) { + if (rowCache[i].docRow < docRow) { + screenRow = rowCache[i].screenRow; + row = rowCache[i].docRow; + } else { + break; + } + } + var doCache = !rowCache.length || i == rowCache.length; + + var foldLine = this.getNextFoldLine(row); + var foldStart = foldLine ?foldLine.start.row :Infinity; + + while (row < docRow) { + if (row >= foldStart) { + rowEnd = foldLine.end.row + 1; + if (rowEnd > docRow) + break; + foldLine = this.getNextFoldLine(rowEnd, foldLine); + foldStart = foldLine ?foldLine.start.row :Infinity; + } + else { + rowEnd = row + 1; + } + + screenRow += this.getRowLength(row); + row = rowEnd; + + if (doCache) { + rowCache.push({ + docRow: row, + screenRow: screenRow + }); + } + } + + // Calculate the text line that is displayed in docRow on the screen. + var textLine = ""; + // Check if the final row we want to reach is inside of a fold. + if (foldLine && row >= foldStart) { + textLine = this.getFoldDisplayLine(foldLine, docRow, docColumn); + foldStartRow = foldLine.start.row; + } else { + textLine = this.getLine(docRow).substring(0, docColumn); + foldStartRow = docRow; + } + // Clamp textLine if in wrapMode. + if (this.$useWrapMode) { + var wrapRow = wrapData[foldStartRow]; + var screenRowOffset = 0; + while (textLine.length >= wrapRow[screenRowOffset]) { + screenRow ++; + screenRowOffset++; + } + textLine = textLine.substring( + wrapRow[screenRowOffset - 1] || 0, textLine.length + ); + } + + return { + row: screenRow, + column: this.$getStringScreenWidth(textLine)[0] + }; + }; + + /** internal, hide + * EditSession.documentToScreenColumn(row, docColumn) -> Number + * + * + **/ + this.documentToScreenColumn = function(row, docColumn) { + return this.documentToScreenPosition(row, docColumn).column; + }; + + /** internal, hide + * EditSession.documentToScreenRow(docRow, docColumn) -> Number + * + * + **/ + this.documentToScreenRow = function(docRow, docColumn) { + return this.documentToScreenPosition(docRow, docColumn).row; + }; + + /** + * EditSession.getScreenLength() -> Number + * + * Returns the length of the screen. + **/ + this.getScreenLength = function() { + var screenRows = 0; + var fold = null; + if (!this.$useWrapMode) { + screenRows = this.getLength(); + + // Remove the folded lines again. + var foldData = this.$foldData; + for (var i = 0; i < foldData.length; i++) { + fold = foldData[i]; + screenRows -= fold.end.row - fold.start.row; + } + } else { + var lastRow = this.$wrapData.length; + var row = 0, i = 0; + var fold = this.$foldData[i++]; + var foldStart = fold ? fold.start.row :Infinity; + + while (row < lastRow) { + screenRows += this.$wrapData[row].length + 1; + row ++; + if (row > foldStart) { + row = fold.end.row+1; + fold = this.$foldData[i++]; + foldStart = fold ?fold.start.row :Infinity; + } + } + } + + return screenRows; + } + + // For every keystroke this gets called once per char in the whole doc!! + // Wouldn't hurt to make it a bit faster for c >= 0x1100 + function isFullWidth(c) { + if (c < 0x1100) + return false; + return c >= 0x1100 && c <= 0x115F || + c >= 0x11A3 && c <= 0x11A7 || + c >= 0x11FA && c <= 0x11FF || + c >= 0x2329 && c <= 0x232A || + c >= 0x2E80 && c <= 0x2E99 || + c >= 0x2E9B && c <= 0x2EF3 || + c >= 0x2F00 && c <= 0x2FD5 || + c >= 0x2FF0 && c <= 0x2FFB || + c >= 0x3000 && c <= 0x303E || + c >= 0x3041 && c <= 0x3096 || + c >= 0x3099 && c <= 0x30FF || + c >= 0x3105 && c <= 0x312D || + c >= 0x3131 && c <= 0x318E || + c >= 0x3190 && c <= 0x31BA || + c >= 0x31C0 && c <= 0x31E3 || + c >= 0x31F0 && c <= 0x321E || + c >= 0x3220 && c <= 0x3247 || + c >= 0x3250 && c <= 0x32FE || + c >= 0x3300 && c <= 0x4DBF || + c >= 0x4E00 && c <= 0xA48C || + c >= 0xA490 && c <= 0xA4C6 || + c >= 0xA960 && c <= 0xA97C || + c >= 0xAC00 && c <= 0xD7A3 || + c >= 0xD7B0 && c <= 0xD7C6 || + c >= 0xD7CB && c <= 0xD7FB || + c >= 0xF900 && c <= 0xFAFF || + c >= 0xFE10 && c <= 0xFE19 || + c >= 0xFE30 && c <= 0xFE52 || + c >= 0xFE54 && c <= 0xFE66 || + c >= 0xFE68 && c <= 0xFE6B || + c >= 0xFF01 && c <= 0xFF60 || + c >= 0xFFE0 && c <= 0xFFE6; + }; + +}).call(EditSession.prototype); + +require("./edit_session/folding").Folding.call(EditSession.prototype); +require("./edit_session/bracket_match").BracketMatch.call(EditSession.prototype); + +exports.EditSession = EditSession; +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/config', ['require', 'exports', 'module' , 'ace/lib/lang'], function(require, exports, module) { +"no use strict"; + +var lang = require("./lib/lang"); + +var global = (function() { + return this; +})(); + +var options = { + packaged: false, + workerPath: "", + modePath: "", + themePath: "", + suffix: ".js" +}; + +exports.get = function(key) { + if (!options.hasOwnProperty(key)) + throw new Error("Unknown confik key: " + key); + + return options[key]; +}; + +exports.set = function(key, value) { + if (!options.hasOwnProperty(key)) + throw new Error("Unknown confik key: " + key); + + options[key] = value; +}; + +exports.all = function() { + return lang.copyObject(options); +}; + +exports.init = function() { + options.packaged = require.packaged || module.packaged || (global.define && define.packaged); + + if (!global.document) + return ""; + + var scriptOptions = {}; + var scriptUrl = ""; + var suffix; + + var scripts = document.getElementsByTagName("script"); + for (var i=0; i + * Julian Viereck + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/selection', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/lib/lang', 'ace/lib/event_emitter', 'ace/range'], function(require, exports, module) { +"use strict"; + +var oop = require("./lib/oop"); +var lang = require("./lib/lang"); +var EventEmitter = require("./lib/event_emitter").EventEmitter; +var Range = require("./range").Range; + +/** + * class Selection + * + * Contains the cursor position and the text selection of an edit session. + * + * The row/columns used in the selection are in document coordinates representing ths coordinates as thez appear in the document before applying soft wrap and folding. + **/ + +/** + * new Selection(session) + * - session (EditSession): The session to use + * + * Creates a new `Selection` object. + * +**/ +var Selection = function(session) { + this.session = session; + this.doc = session.getDocument(); + + this.clearSelection(); + this.lead = this.selectionLead = this.doc.createAnchor(0, 0); + this.anchor = this.selectionAnchor = this.doc.createAnchor(0, 0); + + var self = this; + this.lead.on("change", function(e) { + self._emit("changeCursor"); + if (!self.$isEmpty) + self._emit("changeSelection"); + if (!self.$keepDesiredColumnOnChange && e.old.column != e.value.column) + self.$desiredColumn = null; + }); + + this.selectionAnchor.on("change", function() { + if (!self.$isEmpty) + self._emit("changeSelection"); + }); +}; + +(function() { + + oop.implement(this, EventEmitter); + + /** + * Selection.isEmpty() -> Boolean + * + * Returns `true` if the selection is empty. + **/ + this.isEmpty = function() { + return (this.$isEmpty || ( + this.anchor.row == this.lead.row && + this.anchor.column == this.lead.column + )); + }; + + /** + * Selection.isMultiLine() -> Boolean + * + * Returns `true` if the selection is a multi-line. + **/ + this.isMultiLine = function() { + if (this.isEmpty()) { + return false; + } + + return this.getRange().isMultiLine(); + }; + + /** + * Selection.getCursor() -> Number + * + * Gets the current position of the cursor. + **/ + this.getCursor = function() { + return this.lead.getPosition(); + }; + + /** + * Selection.setSelectionAnchor(row, column) + * - row (Number): The new row + * - column (Number): The new column + * + * Sets the row and column position of the anchor. This function also emits the `'changeSelection'` event. + **/ + this.setSelectionAnchor = function(row, column) { + this.anchor.setPosition(row, column); + + if (this.$isEmpty) { + this.$isEmpty = false; + this._emit("changeSelection"); + } + }; + + /** related to: Anchor.getPosition + * Selection.getSelectionAnchor() -> Object + * + * Returns an object containing the `row` and `column` of the calling selection anchor. + * + **/ + this.getSelectionAnchor = function() { + if (this.$isEmpty) + return this.getSelectionLead() + else + return this.anchor.getPosition(); + }; + + /** + * Selection.getSelectionLead() -> Object + * + * Returns an object containing the `row` and `column` of the calling selection lead. + **/ + this.getSelectionLead = function() { + return this.lead.getPosition(); + }; + + /** + * Selection.shiftSelection(columns) + * - columns (Number): The number of columns to shift by + * + * Shifts the selection up (or down, if [[Selection.isBackwards `isBackwards()`]] is true) the given number of columns. + * + **/ + this.shiftSelection = function(columns) { + if (this.$isEmpty) { + this.moveCursorTo(this.lead.row, this.lead.column + columns); + return; + }; + + var anchor = this.getSelectionAnchor(); + var lead = this.getSelectionLead(); + + var isBackwards = this.isBackwards(); + + if (!isBackwards || anchor.column !== 0) + this.setSelectionAnchor(anchor.row, anchor.column + columns); + + if (isBackwards || lead.column !== 0) { + this.$moveSelection(function() { + this.moveCursorTo(lead.row, lead.column + columns); + }); + } + }; + + /** + * Selection.isBackwards() -> Boolean + * + * Returns `true` if the selection is going backwards in the document. + **/ + this.isBackwards = function() { + var anchor = this.anchor; + var lead = this.lead; + return (anchor.row > lead.row || (anchor.row == lead.row && anchor.column > lead.column)); + }; + + /** + * Selection.getRange() -> Range + * + * [Returns the [[Range `Range`]] for the selected text.]{: #Selection.getRange} + **/ + this.getRange = function() { + var anchor = this.anchor; + var lead = this.lead; + + if (this.isEmpty()) + return Range.fromPoints(lead, lead); + + if (this.isBackwards()) { + return Range.fromPoints(lead, anchor); + } + else { + return Range.fromPoints(anchor, lead); + } + }; + + /** + * Selection.clearSelection() + * + * [Empties the selection (by de-selecting it). This function also emits the `'changeSelection'` event.]{: #Selection.clearSelection} + **/ + this.clearSelection = function() { + if (!this.$isEmpty) { + this.$isEmpty = true; + this._emit("changeSelection"); + } + }; + + /** + * Selection.selectAll() + * + * Selects all the text in the document. + **/ + this.selectAll = function() { + var lastRow = this.doc.getLength() - 1; + this.setSelectionAnchor(lastRow, this.doc.getLine(lastRow).length); + this.moveCursorTo(0, 0); + }; + + /** + * Selection.setSelectionRange(range, reverse) + * - range (Range): The range of text to select + * - reverse (Boolean): Indicates if the range should go backwards (`true`) or not + * + * Sets the selection to the provided range. + * + **/ + this.setSelectionRange = function(range, reverse) { + if (reverse) { + this.setSelectionAnchor(range.end.row, range.end.column); + this.selectTo(range.start.row, range.start.column); + } else { + this.setSelectionAnchor(range.start.row, range.start.column); + this.selectTo(range.end.row, range.end.column); + } + this.$desiredColumn = null; + }; + + this.$moveSelection = function(mover) { + var lead = this.lead; + if (this.$isEmpty) + this.setSelectionAnchor(lead.row, lead.column); + + mover.call(this); + }; + + /** + * Selection.selectTo(row, column) + * - row (Number): The row to select to + * - column (Number): The column to select to + * + * Moves the selection cursor to the indicated row and column. + * + **/ + this.selectTo = function(row, column) { + this.$moveSelection(function() { + this.moveCursorTo(row, column); + }); + }; + + /** + * Selection.selectToPosition(pos) + * - pos (Object): An object containing the row and column + * + * Moves the selection cursor to the row and column indicated by `pos`. + * + **/ + this.selectToPosition = function(pos) { + this.$moveSelection(function() { + this.moveCursorToPosition(pos); + }); + }; + + /** + * Selection.selectUp() + * + * Moves the selection up one row. + **/ + this.selectUp = function() { + this.$moveSelection(this.moveCursorUp); + }; + + /** + * Selection.selectDown() + * + * Moves the selection down one row. + **/ + this.selectDown = function() { + this.$moveSelection(this.moveCursorDown); + }; + + /** + * Selection.selectRight() + * + * Moves the selection right one column. + **/ + this.selectRight = function() { + this.$moveSelection(this.moveCursorRight); + }; + + /** + * Selection.selectLeft() + * + * Moves the selection left one column. + **/ + this.selectLeft = function() { + this.$moveSelection(this.moveCursorLeft); + }; + + /** + * Selection.selectLineStart() + * + * Moves the selection to the beginning of the current line. + **/ + this.selectLineStart = function() { + this.$moveSelection(this.moveCursorLineStart); + }; + + /** + * Selection.selectLineEnd() + * + * Moves the selection to the end of the current line. + **/ + this.selectLineEnd = function() { + this.$moveSelection(this.moveCursorLineEnd); + }; + + /** + * Selection.selectFileEnd() + * + * Moves the selection to the end of the file. + **/ + this.selectFileEnd = function() { + this.$moveSelection(this.moveCursorFileEnd); + }; + + /** + * Selection.selectFileStart() + * + * Moves the selection to the start of the file. + **/ + this.selectFileStart = function() { + this.$moveSelection(this.moveCursorFileStart); + }; + + /** + * Selection.selectWordRight() + * + * Moves the selection to the first word on the right. + **/ + this.selectWordRight = function() { + this.$moveSelection(this.moveCursorWordRight); + }; + + /** + * Selection.selectWordLeft() + * + * Moves the selection to the first word on the left. + **/ + this.selectWordLeft = function() { + this.$moveSelection(this.moveCursorWordLeft); + }; + + /** related to: EditSession.getWordRange + * Selection.selectWord() + * + * Moves the selection to highlight the entire word. + **/ + this.getWordRange = function(row, column) { + if (typeof column == "undefined") { + var cursor = row || this.lead; + row = cursor.row; + column = cursor.column; + } + return this.session.getWordRange(row, column); + }; + + this.selectWord = function() { + this.setSelectionRange(this.getWordRange()); + }; + + /** related to: EditSession.getAWordRange + * Selection.selectAWord() + * + * Selects a word, including its right whitespace. + **/ + this.selectAWord = function() { + var cursor = this.getCursor(); + var range = this.session.getAWordRange(cursor.row, cursor.column); + this.setSelectionRange(range); + }; + + this.getLineRange = function(row, excludeLastChar) { + var rowStart = typeof row == "number" ? row : this.lead.row; + var rowEnd; + + var foldLine = this.session.getFoldLine(rowStart); + if (foldLine) { + rowStart = foldLine.start.row; + rowEnd = foldLine.end.row; + } else { + rowEnd = rowStart; + } + if (excludeLastChar) + return new Range(rowStart, 0, rowEnd, this.session.getLine(rowEnd).length); + else + return new Range(rowStart, 0, rowEnd + 1, 0); + }; + + /** + * Selection.selectLine() + * + * Selects the entire line. + **/ + this.selectLine = function() { + this.setSelectionRange(this.getLineRange()); + }; + + /** + * Selection.moveCursorUp() + * + * Moves the cursor up one row. + **/ + this.moveCursorUp = function() { + this.moveCursorBy(-1, 0); + }; + + /** + * Selection.moveCursorDown() + * + * Moves the cursor down one row. + **/ + this.moveCursorDown = function() { + this.moveCursorBy(1, 0); + }; + + /** + * Selection.moveCursorLeft() + * + * Moves the cursor left one column. + **/ + this.moveCursorLeft = function() { + var cursor = this.lead.getPosition(), + fold; + + if (fold = this.session.getFoldAt(cursor.row, cursor.column, -1)) { + this.moveCursorTo(fold.start.row, fold.start.column); + } else if (cursor.column == 0) { + // cursor is a line (start + if (cursor.row > 0) { + this.moveCursorTo(cursor.row - 1, this.doc.getLine(cursor.row - 1).length); + } + } + else { + var tabSize = this.session.getTabSize(); + if (this.session.isTabStop(cursor) && this.doc.getLine(cursor.row).slice(cursor.column-tabSize, cursor.column).split(" ").length-1 == tabSize) + this.moveCursorBy(0, -tabSize); + else + this.moveCursorBy(0, -1); + } + }; + + /** + * Selection.moveCursorRight() + * + * Moves the cursor right one column. + **/ + this.moveCursorRight = function() { + var cursor = this.lead.getPosition(), + fold; + if (fold = this.session.getFoldAt(cursor.row, cursor.column, 1)) { + this.moveCursorTo(fold.end.row, fold.end.column); + } + else if (this.lead.column == this.doc.getLine(this.lead.row).length) { + if (this.lead.row < this.doc.getLength() - 1) { + this.moveCursorTo(this.lead.row + 1, 0); + } + } + else { + var tabSize = this.session.getTabSize(); + var cursor = this.lead; + if (this.session.isTabStop(cursor) && this.doc.getLine(cursor.row).slice(cursor.column, cursor.column+tabSize).split(" ").length-1 == tabSize) + this.moveCursorBy(0, tabSize); + else + this.moveCursorBy(0, 1); + } + }; + + /** + * Selection.moveCursorLineStart() + * + * Moves the cursor to the start of the line. + **/ + this.moveCursorLineStart = function() { + var row = this.lead.row; + var column = this.lead.column; + var screenRow = this.session.documentToScreenRow(row, column); + + // Determ the doc-position of the first character at the screen line. + var firstColumnPosition = this.session.screenToDocumentPosition(screenRow, 0); + + // Determ the line + var beforeCursor = this.session.getDisplayLine( + row, null, + firstColumnPosition.row, firstColumnPosition.column + ); + + var leadingSpace = beforeCursor.match(/^\s*/); + if (leadingSpace[0].length == column) { + this.moveCursorTo( + firstColumnPosition.row, firstColumnPosition.column + ); + } + else { + this.moveCursorTo( + firstColumnPosition.row, + firstColumnPosition.column + leadingSpace[0].length + ); + } + }; + + /** + * Selection.moveCursorLineEnd() + * + * Moves the cursor to the end of the line. + **/ + this.moveCursorLineEnd = function() { + var lead = this.lead; + var lastRowColumnPosition = + this.session.getDocumentLastRowColumnPosition(lead.row, lead.column); + this.moveCursorTo( + lastRowColumnPosition.row, + lastRowColumnPosition.column + ); + }; + + /** + * Selection.moveCursorFileEnd() + * + * Moves the cursor to the end of the file. + **/ + this.moveCursorFileEnd = function() { + var row = this.doc.getLength() - 1; + var column = this.doc.getLine(row).length; + this.moveCursorTo(row, column); + }; + + /** + * Selection.moveCursorFileStart() + * + * Moves the cursor to the start of the file. + **/ + this.moveCursorFileStart = function() { + this.moveCursorTo(0, 0); + }; + + /** + * Selection.moveCursorLongWordRight() + * + * Moves the cursor to the word on the right. + **/ + this.moveCursorLongWordRight = function() { + var row = this.lead.row; + var column = this.lead.column; + var line = this.doc.getLine(row); + var rightOfCursor = line.substring(column); + + var match; + this.session.nonTokenRe.lastIndex = 0; + this.session.tokenRe.lastIndex = 0; + + // skip folds + var fold = this.session.getFoldAt(row, column, 1); + if (fold) { + this.moveCursorTo(fold.end.row, fold.end.column); + return; + } + + // first skip space + if (match = this.session.nonTokenRe.exec(rightOfCursor)) { + column += this.session.nonTokenRe.lastIndex; + this.session.nonTokenRe.lastIndex = 0; + rightOfCursor = line.substring(column); + } + + // if at line end proceed with next line + if (column >= line.length) { + this.moveCursorTo(row, line.length); + this.moveCursorRight(); + if (row < this.doc.getLength() - 1) + this.moveCursorWordRight(); + return; + } + + // advance to the end of the next token + if (match = this.session.tokenRe.exec(rightOfCursor)) { + column += this.session.tokenRe.lastIndex; + this.session.tokenRe.lastIndex = 0; + } + + this.moveCursorTo(row, column); + }; + + /** + * Selection.moveCursorLongWordLeft() + * + * Moves the cursor to the word on the left. + **/ + this.moveCursorLongWordLeft = function() { + var row = this.lead.row; + var column = this.lead.column; + + // skip folds + var fold; + if (fold = this.session.getFoldAt(row, column, -1)) { + this.moveCursorTo(fold.start.row, fold.start.column); + return; + } + + var str = this.session.getFoldStringAt(row, column, -1); + if (str == null) { + str = this.doc.getLine(row).substring(0, column) + } + + var leftOfCursor = lang.stringReverse(str); + var match; + this.session.nonTokenRe.lastIndex = 0; + this.session.tokenRe.lastIndex = 0; + + // skip whitespace + if (match = this.session.nonTokenRe.exec(leftOfCursor)) { + column -= this.session.nonTokenRe.lastIndex; + leftOfCursor = leftOfCursor.slice(this.session.nonTokenRe.lastIndex); + this.session.nonTokenRe.lastIndex = 0; + } + + // if at begin of the line proceed in line above + if (column <= 0) { + this.moveCursorTo(row, 0); + this.moveCursorLeft(); + if (row > 0) + this.moveCursorWordLeft(); + return; + } + + // move to the begin of the word + if (match = this.session.tokenRe.exec(leftOfCursor)) { + column -= this.session.tokenRe.lastIndex; + this.session.tokenRe.lastIndex = 0; + } + + this.moveCursorTo(row, column); + }; + + this.$shortWordEndIndex = function(rightOfCursor) { + var match, index = 0, ch; + var whitespaceRe = /\s/; + var tokenRe = this.session.tokenRe; + + tokenRe.lastIndex = 0; + if (match = this.session.tokenRe.exec(rightOfCursor)) { + index = this.session.tokenRe.lastIndex; + } else { + while ((ch = rightOfCursor[index]) && whitespaceRe.test(ch)) + index ++; + + if (index <= 1) { + tokenRe.lastIndex = 0; + while ((ch = rightOfCursor[index]) && !tokenRe.test(ch)) { + tokenRe.lastIndex = 0; + index ++; + if (whitespaceRe.test(ch)) { + if (index > 2) { + index-- + break; + } else { + while ((ch = rightOfCursor[index]) && whitespaceRe.test(ch)) + index ++; + if (index > 2) + break + } + } + } + } + } + tokenRe.lastIndex = 0; + + return index; + }; + + this.moveCursorShortWordRight = function() { + var row = this.lead.row; + var column = this.lead.column; + var line = this.doc.getLine(row); + var rightOfCursor = line.substring(column); + + var fold = this.session.getFoldAt(row, column, 1); + if (fold) + return this.moveCursorTo(fold.end.row, fold.end.column); + + if (column == line.length) + return this.moveCursorRight(); + + var index = this.$shortWordEndIndex(rightOfCursor); + + this.moveCursorTo(row, column + index); + }; + + this.moveCursorShortWordLeft = function() { + var row = this.lead.row; + var column = this.lead.column; + + var fold; + if (fold = this.session.getFoldAt(row, column, -1)) + return this.moveCursorTo(fold.start.row, fold.start.column); + + if (column == 0) + return this.moveCursorLeft(); + + var str = this.session.getLine(row).substring(0, column); + var leftOfCursor = lang.stringReverse(str); + var index = this.$shortWordEndIndex(leftOfCursor); + + return this.moveCursorTo(row, column - index); + }; + + this.moveCursorWordRight = function() { + if (this.session.$selectLongWords) + this.moveCursorLongWordRight(); + else + this.moveCursorShortWordRight(); + }; + + this.moveCursorWordLeft = function() { + if (this.session.$selectLongWords) + this.moveCursorLongWordLeft(); + else + this.moveCursorShortWordLeft(); + }; + + /** related to: EditSession.documentToScreenPosition + * Selection.moveCursorBy(rows, chars) + * - rows (Number): The number of rows to move by + * - chars (Number): The number of characters to move by + * + * Moves the cursor to position indicated by the parameters. Negative numbers move the cursor backwards in the document. + **/ + this.moveCursorBy = function(rows, chars) { + var screenPos = this.session.documentToScreenPosition( + this.lead.row, + this.lead.column + ); + + if (chars === 0) { + if (this.$desiredColumn) + screenPos.column = this.$desiredColumn; + else + this.$desiredColumn = screenPos.column; + } + + var docPos = this.session.screenToDocumentPosition(screenPos.row + rows, screenPos.column); + + // move the cursor and update the desired column + this.moveCursorTo(docPos.row, docPos.column + chars, chars === 0); + }; + + /** + * Selection.moveCursorToPosition(position) + * - position (Object): The position to move to + * + * Moves the selection to the position indicated by its `row` and `column`. + **/ + this.moveCursorToPosition = function(position) { + this.moveCursorTo(position.row, position.column); + }; + + /** + * Selection.moveCursorTo(row, column, keepDesiredColumn) + * - row (Number): The row to move to + * - column (Number): The column to move to + * - keepDesiredColumn (Boolean): [If `true`, the cursor move does not respect the previous column]{: #preventUpdateBool} + * + * Moves the cursor to the row and column provided. [If `preventUpdateDesiredColumn` is `true`, then the cursor stays in the same column position as its original point.]{: #preventUpdateBoolDesc} + **/ + this.moveCursorTo = function(row, column, keepDesiredColumn) { + // Ensure the row/column is not inside of a fold. + var fold = this.session.getFoldAt(row, column, 1); + if (fold) { + row = fold.start.row; + column = fold.start.column; + } + + this.$keepDesiredColumnOnChange = true; + this.lead.setPosition(row, column); + this.$keepDesiredColumnOnChange = false; + + if (!keepDesiredColumn) + this.$desiredColumn = null; + }; + + /** + * Selection.moveCursorToScreen(row, column, keepDesiredColumn) + * - row (Number): The row to move to + * - column (Number): The column to move to + * - keepDesiredColumn (Boolean): {:preventUpdateBool} + * + * Moves the cursor to the screen position indicated by row and column. {:preventUpdateBoolDesc} + **/ + this.moveCursorToScreen = function(row, column, keepDesiredColumn) { + var pos = this.session.screenToDocumentPosition(row, column); + this.moveCursorTo(pos.row, pos.column, keepDesiredColumn); + }; + + // remove listeners from document + this.detach = function() { + this.lead.detach(); + this.anchor.detach(); + this.session = this.doc = null; + } + + this.fromOrientedRange = function(range) { + this.setSelectionRange(range, range.cursor == range.start); + this.$desiredColumn = range.desiredColumn || this.$desiredColumn; + } + + this.toOrientedRange = function(range) { + var r = this.getRange(); + if (range) { + range.start.column = r.start.column; + range.start.row = r.start.row; + range.end.column = r.end.column; + range.end.row = r.end.row; + } else { + range = r; + } + + range.cursor = this.isBackwards() ? range.start : range.end; + range.desiredColumn = this.$desiredColumn; + return range; + } + +}).call(Selection.prototype); + +exports.Selection = Selection; +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/range', ['require', 'exports', 'module' ], function(require, exports, module) { +"use strict"; + +/** + * class Range + * + * This object is used in various places to indicate a region within the editor. To better visualize how this works, imagine a rectangle. Each quadrant of the rectangle is analogus to a range, as ranges contain a starting row and starting column, and an ending row, and ending column. + * + **/ + +/** + * new Range(startRow, startColumn, endRow, endColumn) + * - startRow (Number): The starting row + * - startColumn (Number): The starting column + * - endRow (Number): The ending row + * - endColumn (Number): The ending column + * + * Creates a new `Range` object with the given starting and ending row and column points. + * + **/ +var Range = function(startRow, startColumn, endRow, endColumn) { + this.start = { + row: startRow, + column: startColumn + }; + + this.end = { + row: endRow, + column: endColumn + }; +}; + +(function() { + /** + * Range.isEqual(range) -> Boolean + * - range (Range): A range to check against + * + * Returns `true` if and only if the starting row and column, and ending tow and column, are equivalent to those given by `range`. + * + **/ + this.isEqual = function(range) { + return this.start.row == range.start.row && + this.end.row == range.end.row && + this.start.column == range.start.column && + this.end.column == range.end.column + }; + + /** + * Range.toString() -> String + * + * Returns a string containing the range's row and column information, given like this: + * + * [start.row/start.column] -> [end.row/end.column] + * + **/ + + this.toString = function() { + return ("Range: [" + this.start.row + "/" + this.start.column + + "] -> [" + this.end.row + "/" + this.end.column + "]"); + }; + + /** related to: Range.compare + * Range.contains(row, column) -> Boolean + * - row (Number): A row to check for + * - column (Number): A column to check for + * + * Returns `true` if the `row` and `column` provided are within the given range. This can better be expressed as returning `true` if: + * + * this.start.row <= row <= this.end.row && + * this.start.column <= column <= this.end.column + * + **/ + + this.contains = function(row, column) { + return this.compare(row, column) == 0; + }; + + /** related to: Range.compare + * Range.compareRange(range) -> Number + * - range (Range): A range to compare with + * + (Number): This method returns one of the following numbers:
+ *
+ * * `-2`: (B) is in front of (A), and doesn't intersect with (A)
+ * * `-1`: (B) begins before (A) but ends inside of (A)
+ * * `0`: (B) is completely inside of (A) OR (A) is completely inside of (B)
+ * * `+1`: (B) begins inside of (A) but ends outside of (A)
+ * * `+2`: (B) is after (A) and doesn't intersect with (A)
+ * * `42`: FTW state: (B) ends in (A) but starts outside of (A) + * + * Compares `this` range (A) with another range (B). + * + **/ + this.compareRange = function(range) { + var cmp, + end = range.end, + start = range.start; + + cmp = this.compare(end.row, end.column); + if (cmp == 1) { + cmp = this.compare(start.row, start.column); + if (cmp == 1) { + return 2; + } else if (cmp == 0) { + return 1; + } else { + return 0; + } + } else if (cmp == -1) { + return -2; + } else { + cmp = this.compare(start.row, start.column); + if (cmp == -1) { + return -1; + } else if (cmp == 1) { + return 42; + } else { + return 0; + } + } + } + + /** related to: Range.compare + * Range.comparePoint(p) -> Number + * - p (Range): A point to compare with + * + (Number): This method returns one of the following numbers:
+ * * `0` if the two points are exactly equal
+ * * `-1` if `p.row` is less then the calling range
+ * * `1` if `p.row` is greater than the calling range
+ *
+ * If the starting row of the calling range is equal to `p.row`, and:
+ * * `p.column` is greater than or equal to the calling range's starting column, this returns `0`
+ * * Otherwise, it returns -1
+ *
+ * If the ending row of the calling range is equal to `p.row`, and:
+ * * `p.column` is less than or equal to the calling range's ending column, this returns `0`
+ * * Otherwise, it returns 1
+ * + * Checks the row and column points of `p` with the row and column points of the calling range. + * + * + * + **/ + this.comparePoint = function(p) { + return this.compare(p.row, p.column); + } + + /** related to: Range.comparePoint + * Range.containsRange(range) -> Boolean + * - range (Range): A range to compare with + * + * Checks the start and end points of `range` and compares them to the calling range. Returns `true` if the `range` is contained within the caller's range. + * + **/ + this.containsRange = function(range) { + return this.comparePoint(range.start) == 0 && this.comparePoint(range.end) == 0; + } + + /** + * Range.intersects(range) -> Boolean + * - range (Range): A range to compare with + * + * Returns `true` if passed in `range` intersects with the one calling this method. + * + **/ + this.intersects = function(range) { + var cmp = this.compareRange(range); + return (cmp == -1 || cmp == 0 || cmp == 1); + } + + /** + * Range.isEnd(row, column) -> Boolean + * - row (Number): A row point to compare with + * - column (Number): A column point to compare with + * + * Returns `true` if the caller's ending row point is the same as `row`, and if the caller's ending column is the same as `column`. + * + **/ + this.isEnd = function(row, column) { + return this.end.row == row && this.end.column == column; + } + + /** + * Range.isStart(row, column) -> Boolean + * - row (Number): A row point to compare with + * - column (Number): A column point to compare with + * + * Returns `true` if the caller's starting row point is the same as `row`, and if the caller's starting column is the same as `column`. + * + **/ + this.isStart = function(row, column) { + return this.start.row == row && this.start.column == column; + } + + /** + * Range.setStart(row, column) + * - row (Number): A row point to set + * - column (Number): A column point to set + * + * Sets the starting row and column for the range. + * + **/ + this.setStart = function(row, column) { + if (typeof row == "object") { + this.start.column = row.column; + this.start.row = row.row; + } else { + this.start.row = row; + this.start.column = column; + } + } + + /** + * Range.setEnd(row, column) + * - row (Number): A row point to set + * - column (Number): A column point to set + * + * Sets the starting row and column for the range. + * + **/ + this.setEnd = function(row, column) { + if (typeof row == "object") { + this.end.column = row.column; + this.end.row = row.row; + } else { + this.end.row = row; + this.end.column = column; + } + } + + /** related to: Range.compare + * Range.inside(row, column) -> Boolean + * - row (Number): A row point to compare with + * - column (Number): A column point to compare with + * + * Returns `true` if the `row` and `column` are within the given range. + * + **/ + this.inside = function(row, column) { + if (this.compare(row, column) == 0) { + if (this.isEnd(row, column) || this.isStart(row, column)) { + return false; + } else { + return true; + } + } + return false; + } + + /** related to: Range.compare + * Range.insideStart(row, column) -> Boolean + * - row (Number): A row point to compare with + * - column (Number): A column point to compare with + * + * Returns `true` if the `row` and `column` are within the given range's starting points. + * + **/ + this.insideStart = function(row, column) { + if (this.compare(row, column) == 0) { + if (this.isEnd(row, column)) { + return false; + } else { + return true; + } + } + return false; + } + + /** related to: Range.compare + * Range.insideEnd(row, column) -> Boolean + * - row (Number): A row point to compare with + * - column (Number): A column point to compare with + * + * Returns `true` if the `row` and `column` are within the given range's ending points. + * + **/ + this.insideEnd = function(row, column) { + if (this.compare(row, column) == 0) { + if (this.isStart(row, column)) { + return false; + } else { + return true; + } + } + return false; + } + + /** + * Range.compare(row, column) -> Number + * - row (Number): A row point to compare with + * - column (Number): A column point to compare with + * + (Number): This method returns one of the following numbers:
+ * * `0` if the two points are exactly equal
+ * * `-1` if `p.row` is less then the calling range
+ * * `1` if `p.row` is greater than the calling range
+ *
+ * If the starting row of the calling range is equal to `p.row`, and:
+ * * `p.column` is greater than or equal to the calling range's starting column, this returns `0`
+ * * Otherwise, it returns -1
+ *
+ * If the ending row of the calling range is equal to `p.row`, and:
+ * * `p.column` is less than or equal to the calling range's ending column, this returns `0`
+ * * Otherwise, it returns 1 + * + * Checks the row and column points with the row and column points of the calling range. + * + * + **/ + this.compare = function(row, column) { + if (!this.isMultiLine()) { + if (row === this.start.row) { + return column < this.start.column ? -1 : (column > this.end.column ? 1 : 0); + }; + } + + if (row < this.start.row) + return -1; + + if (row > this.end.row) + return 1; + + if (this.start.row === row) + return column >= this.start.column ? 0 : -1; + + if (this.end.row === row) + return column <= this.end.column ? 0 : 1; + + return 0; + }; + + /** + * Range.compareStart(row, column) -> Number + * - row (Number): A row point to compare with + * - column (Number): A column point to compare with + * + (Number): This method returns one of the following numbers:
+ *
+ * * `0` if the two points are exactly equal
+ * * `-1` if `p.row` is less then the calling range
+ * * `1` if `p.row` is greater than the calling range, or if `isStart` is `true`.
+ *
+ * If the starting row of the calling range is equal to `p.row`, and:
+ * * `p.column` is greater than or equal to the calling range's starting column, this returns `0`
+ * * Otherwise, it returns -1
+ *
+ * If the ending row of the calling range is equal to `p.row`, and:
+ * * `p.column` is less than or equal to the calling range's ending column, this returns `0`
+ * * Otherwise, it returns 1 + * + * Checks the row and column points with the row and column points of the calling range. + * + * + * + **/ + this.compareStart = function(row, column) { + if (this.start.row == row && this.start.column == column) { + return -1; + } else { + return this.compare(row, column); + } + } + + /** + * Range.compareEnd(row, column) -> Number + * - row (Number): A row point to compare with + * - column (Number): A column point to compare with + * + (Number): This method returns one of the following numbers:
+ * * `0` if the two points are exactly equal
+ * * `-1` if `p.row` is less then the calling range
+ * * `1` if `p.row` is greater than the calling range, or if `isEnd` is `true.
+ *
+ * If the starting row of the calling range is equal to `p.row`, and:
+ * * `p.column` is greater than or equal to the calling range's starting column, this returns `0`
+ * * Otherwise, it returns -1
+ *
+ * If the ending row of the calling range is equal to `p.row`, and:
+ * * `p.column` is less than or equal to the calling range's ending column, this returns `0`
+ * * Otherwise, it returns 1 + * + * Checks the row and column points with the row and column points of the calling range. + * + * + **/ + this.compareEnd = function(row, column) { + if (this.end.row == row && this.end.column == column) { + return 1; + } else { + return this.compare(row, column); + } + } + + /** + * Range.compareInside(row, column) -> Number + * - row (Number): A row point to compare with + * - column (Number): A column point to compare with + * + (Number): This method returns one of the following numbers:
+ * * `1` if the ending row of the calling range is equal to `row`, and the ending column of the calling range is equal to `column`
+ * * `-1` if the starting row of the calling range is equal to `row`, and the starting column of the calling range is equal to `column`
+ *
+ * Otherwise, it returns the value after calling [[Range.compare `compare()`]]. + * + * Checks the row and column points with the row and column points of the calling range. + * + * + * + **/ + this.compareInside = function(row, column) { + if (this.end.row == row && this.end.column == column) { + return 1; + } else if (this.start.row == row && this.start.column == column) { + return -1; + } else { + return this.compare(row, column); + } + } + + /** + * Range.clipRows(firstRow, lastRow) -> Range + * - firstRow (Number): The starting row + * - lastRow (Number): The ending row + * + * Returns the part of the current `Range` that occurs within the boundaries of `firstRow` and `lastRow` as a new `Range` object. + * + **/ + this.clipRows = function(firstRow, lastRow) { + if (this.end.row > lastRow) { + var end = { + row: lastRow+1, + column: 0 + }; + } + + if (this.start.row > lastRow) { + var start = { + row: lastRow+1, + column: 0 + }; + } + + if (this.start.row < firstRow) { + var start = { + row: firstRow, + column: 0 + }; + } + + if (this.end.row < firstRow) { + var end = { + row: firstRow, + column: 0 + }; + } + return Range.fromPoints(start || this.start, end || this.end); + }; + + /** + * Range.extend(row, column) -> Range + * - row (Number): A new row to extend to + * - column (Number): A new column to extend to + * + * Changes the row and column points for the calling range for both the starting and ending points. This method returns that range with a new row. + * + **/ + this.extend = function(row, column) { + var cmp = this.compare(row, column); + + if (cmp == 0) + return this; + else if (cmp == -1) + var start = {row: row, column: column}; + else + var end = {row: row, column: column}; + + return Range.fromPoints(start || this.start, end || this.end); + }; + + this.isEmpty = function() { + return (this.start.row == this.end.row && this.start.column == this.end.column); + }; + + /** + * Range.isMultiLine() -> Boolean + * + * Returns true if the range spans across multiple lines. + * + **/ + this.isMultiLine = function() { + return (this.start.row !== this.end.row); + }; + + /** + * Range.clone() -> Range + * + * Returns a duplicate of the calling range. + * + **/ + this.clone = function() { + return Range.fromPoints(this.start, this.end); + }; + + /** + * Range.collapseRows() -> Range + * + * Returns a range containing the starting and ending rows of the original range, but with a column value of `0`. + * + **/ + this.collapseRows = function() { + if (this.end.column == 0) + return new Range(this.start.row, 0, Math.max(this.start.row, this.end.row-1), 0) + else + return new Range(this.start.row, 0, this.end.row, 0) + }; + + /** + * Range.toScreenRange(session) -> Range + * - session (EditSession): The `EditSession` to retrieve coordinates from + * + * Given the current `Range`, this function converts those starting and ending points into screen positions, and then returns a new `Range` object. + **/ + this.toScreenRange = function(session) { + var screenPosStart = + session.documentToScreenPosition(this.start); + var screenPosEnd = + session.documentToScreenPosition(this.end); + + return new Range( + screenPosStart.row, screenPosStart.column, + screenPosEnd.row, screenPosEnd.column + ); + }; + +}).call(Range.prototype); + +/** + * Range.fromPoints(start, end) -> Range + * - start (Range): A starting point to use + * - end (Range): An ending point to use + * + * Creates and returns a new `Range` based on the row and column of the given parameters. + * +**/ +Range.fromPoints = function(start, end) { + return new Range(start.row, start.column, end.row, end.column); +}; + +exports.Range = Range; +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * Mihai Sucan + * Chris Spencer + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/text', ['require', 'exports', 'module' , 'ace/tokenizer', 'ace/mode/text_highlight_rules', 'ace/mode/behaviour', 'ace/unicode'], function(require, exports, module) { +"use strict"; + +var Tokenizer = require("../tokenizer").Tokenizer; +var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; +var Behaviour = require("./behaviour").Behaviour; +var unicode = require("../unicode"); + +var Mode = function() { + this.$tokenizer = new Tokenizer(new TextHighlightRules().getRules()); + this.$behaviour = new Behaviour(); +}; + +(function() { + + this.tokenRe = new RegExp("^[" + + unicode.packages.L + + unicode.packages.Mn + unicode.packages.Mc + + unicode.packages.Nd + + unicode.packages.Pc + "\\$_]+", "g" + ); + + this.nonTokenRe = new RegExp("^(?:[^" + + unicode.packages.L + + unicode.packages.Mn + unicode.packages.Mc + + unicode.packages.Nd + + unicode.packages.Pc + "\\$_]|\s])+", "g" + ); + + this.getTokenizer = function() { + return this.$tokenizer; + }; + + this.toggleCommentLines = function(state, doc, startRow, endRow) { + }; + + this.getNextLineIndent = function(state, line, tab) { + return ""; + }; + + this.checkOutdent = function(state, line, input) { + return false; + }; + + this.autoOutdent = function(state, doc, row) { + }; + + this.$getIndent = function(line) { + var match = line.match(/^(\s+)/); + if (match) { + return match[1]; + } + + return ""; + }; + + this.createWorker = function(session) { + return null; + }; + + this.highlightSelection = function(editor) { + var session = editor.session; + if (!session.$selectionOccurrences) + session.$selectionOccurrences = []; + + if (session.$selectionOccurrences.length) + this.clearSelectionHighlight(editor); + + var selection = editor.getSelectionRange(); + if (selection.isEmpty() || selection.isMultiLine()) + return; + + var startOuter = selection.start.column - 1; + var endOuter = selection.end.column + 1; + var line = session.getLine(selection.start.row); + var lineCols = line.length; + var needle = line.substring(Math.max(startOuter, 0), + Math.min(endOuter, lineCols)); + + // Make sure the outer characters are not part of the word. + if ((startOuter >= 0 && /^[\w\d]/.test(needle)) || + (endOuter <= lineCols && /[\w\d]$/.test(needle))) + return; + + needle = line.substring(selection.start.column, selection.end.column); + if (!/^[\w\d]+$/.test(needle)) + return; + + var cursor = editor.getCursorPosition(); + + var newOptions = { + wrap: true, + wholeWord: true, + caseSensitive: true, + needle: needle + }; + + var currentOptions = editor.$search.getOptions(); + editor.$search.set(newOptions); + + var ranges = editor.$search.findAll(session); + ranges.forEach(function(range) { + if (!range.contains(cursor.row, cursor.column)) { + var marker = session.addMarker(range, "ace_selected_word", "text"); + session.$selectionOccurrences.push(marker); + } + }); + + editor.$search.set(currentOptions); + }; + + this.clearSelectionHighlight = function(editor) { + if (!editor.session.$selectionOccurrences) + return; + + editor.session.$selectionOccurrences.forEach(function(marker) { + editor.session.removeMarker(marker); + }); + + editor.session.$selectionOccurrences = []; + }; + + this.createModeDelegates = function (mapping) { + if (!this.$embeds) { + return; + } + this.$modes = {}; + for (var i = 0; i < this.$embeds.length; i++) { + if (mapping[this.$embeds[i]]) { + this.$modes[this.$embeds[i]] = new mapping[this.$embeds[i]](); + } + } + + var delegations = ['toggleCommentLines', 'getNextLineIndent', 'checkOutdent', 'autoOutdent', 'transformAction']; + + for (var i = 0; i < delegations.length; i++) { + (function(scope) { + var functionName = delegations[i]; + var defaultHandler = scope[functionName]; + scope[delegations[i]] = function() { + return this.$delegator(functionName, arguments, defaultHandler); + } + } (this)); + } + } + + this.$delegator = function(method, args, defaultHandler) { + var state = args[0]; + + for (var i = 0; i < this.$embeds.length; i++) { + if (!this.$modes[this.$embeds[i]]) continue; + + var split = state.split(this.$embeds[i]); + if (!split[0] && split[1]) { + args[0] = split[1]; + var mode = this.$modes[this.$embeds[i]]; + return mode[method].apply(mode, args); + } + } + var ret = defaultHandler.apply(this, args); + return defaultHandler ? ret : undefined; + }; + + this.transformAction = function(state, action, editor, session, param) { + if (this.$behaviour) { + var behaviours = this.$behaviour.getBehaviours(); + for (var key in behaviours) { + if (behaviours[key][action]) { + var ret = behaviours[key][action].apply(this, arguments); + if (ret) { + return ret; + } + } + } + } + } + +}).call(Mode.prototype); + +exports.Mode = Mode; +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/tokenizer', ['require', 'exports', 'module' ], function(require, exports, module) { +"use strict"; + +/** + * class Tokenizer + * + * This class takes a set of highlighting rules, and creates a tokenizer out of them. For more information, see [the wiki on extending highlighters](https://github.com/ajaxorg/ace/wiki/Creating-or-Extending-an-Edit-Mode#wiki-extendingTheHighlighter). + * + **/ + +/** + * new Tokenizer(rules, flag) + * - rules (Object): The highlighting rules + * - flag (String): Any additional regular expression flags to pass (like "i" for case insensitive) + * + * Constructs a new tokenizer based on the given rules and flags. + * + **/ +var Tokenizer = function(rules, flag) { + flag = flag ? "g" + flag : "g"; + this.rules = rules; + + this.regExps = {}; + this.matchMappings = {}; + for ( var key in this.rules) { + var rule = this.rules[key]; + var state = rule; + var ruleRegExps = []; + var matchTotal = 0; + var mapping = this.matchMappings[key] = {}; + + for ( var i = 0; i < state.length; i++) { + + if (state[i].regex instanceof RegExp) + state[i].regex = state[i].regex.toString().slice(1, -1); + + // Count number of matching groups. 2 extra groups from the full match + // And the catch-all on the end (used to force a match); + var matchcount = new RegExp("(?:(" + state[i].regex + ")|(.))").exec("a").length - 2; + + // Replace any backreferences and offset appropriately. + var adjustedregex = state[i].regex.replace(/\\([0-9]+)/g, function (match, digit) { + return "\\" + (parseInt(digit, 10) + matchTotal + 1); + }); + + if (matchcount > 1 && state[i].token.length !== matchcount-1) + throw new Error("Matching groups and length of the token array don't match in rule #" + i + " of state " + key); + + mapping[matchTotal] = { + rule: i, + len: matchcount + }; + matchTotal += matchcount; + + ruleRegExps.push(adjustedregex); + } + + this.regExps[key] = new RegExp("(?:(" + ruleRegExps.join(")|(") + ")|(.))", flag); + } +}; + +(function() { + + /** + * Tokenizer.getLineTokens() -> Object + * + * Returns an object containing two properties: `tokens`, which contains all the tokens; and `state`, the current state. + **/ + this.getLineTokens = function(line, startState) { + var currentState = startState; + var state = this.rules[currentState]; + var mapping = this.matchMappings[currentState]; + var re = this.regExps[currentState]; + re.lastIndex = 0; + + var match, tokens = []; + + var lastIndex = 0; + + var token = { + type: null, + value: "" + }; + + while (match = re.exec(line)) { + var type = "text"; + var rule = null; + var value = [match[0]]; + + for (var i = 0; i < match.length-2; i++) { + if (match[i + 1] === undefined) + continue; + + rule = state[mapping[i].rule]; + + if (mapping[i].len > 1) + value = match.slice(i+2, i+1+mapping[i].len); + + // compute token type + if (typeof rule.token == "function") + type = rule.token.apply(this, value); + else + type = rule.token; + + if (rule.next) { + currentState = rule.next; + state = this.rules[currentState]; + mapping = this.matchMappings[currentState]; + lastIndex = re.lastIndex; + + re = this.regExps[currentState]; + re.lastIndex = lastIndex; + } + break; + } + + if (value[0]) { + if (typeof type == "string") { + value = [value.join("")]; + type = [type]; + } + for (var i = 0; i < value.length; i++) { + if (!value[i]) + continue; + + if ((!rule || rule.merge || type[i] === "text") && token.type === type[i]) { + token.value += value[i]; + } else { + if (token.type) + tokens.push(token); + + token = { + type: type[i], + value: value[i] + }; + } + } + } + + if (lastIndex == line.length) + break; + + lastIndex = re.lastIndex; + } + + if (token.type) + tokens.push(token); + + return { + tokens : tokens, + state : currentState + }; + }; + +}).call(Tokenizer.prototype); + +exports.Tokenizer = Tokenizer; +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/text_highlight_rules', ['require', 'exports', 'module' , 'ace/lib/lang'], function(require, exports, module) { +"use strict"; + +var lang = require("../lib/lang"); + +var TextHighlightRules = function() { + + // regexp must not have capturing parentheses + // regexps are ordered -> the first match is used + + this.$rules = { + "start" : [{ + token : "empty_line", + regex : '^$' + }, { + token : "text", + regex : ".+" + }] + }; +}; + +(function() { + + this.addRules = function(rules, prefix) { + for (var key in rules) { + var state = rules[key]; + for (var i=0; i + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/behaviour', ['require', 'exports', 'module' ], function(require, exports, module) { +"use strict"; + +var Behaviour = function() { + this.$behaviours = {}; +}; + +(function () { + + this.add = function (name, action, callback) { + switch (undefined) { + case this.$behaviours: + this.$behaviours = {}; + case this.$behaviours[name]: + this.$behaviours[name] = {}; + } + this.$behaviours[name][action] = callback; + } + + this.addBehaviours = function (behaviours) { + for (var key in behaviours) { + for (var action in behaviours[key]) { + this.add(key, action, behaviours[key][action]); + } + } + } + + this.remove = function (name) { + if (this.$behaviours && this.$behaviours[name]) { + delete this.$behaviours[name]; + } + } + + this.inherit = function (mode, filter) { + if (typeof mode === "function") { + var behaviours = new mode().getBehaviours(filter); + } else { + var behaviours = mode.getBehaviours(filter); + } + this.addBehaviours(behaviours); + } + + this.getBehaviours = function (filter) { + if (!filter) { + return this.$behaviours; + } else { + var ret = {} + for (var i = 0; i < filter.length; i++) { + if (this.$behaviours[filter[i]]) { + ret[filter[i]] = this.$behaviours[filter[i]]; + } + } + return ret; + } + } + +}).call(Behaviour.prototype); + +exports.Behaviour = Behaviour; +}); +ace.define('ace/unicode', ['require', 'exports', 'module' ], function(require, exports, module) { +"use strict"; + +/* +XRegExp Unicode plugin pack: Categories 1.0 +(c) 2010 Steven Levithan +MIT License + +Uses the Unicode 5.2 character database + +This package for the XRegExp Unicode plugin enables the following Unicode categories (aka properties): + +L - Letter (the top-level Letter category is included in the Unicode plugin base script) + Ll - Lowercase letter + Lu - Uppercase letter + Lt - Titlecase letter + Lm - Modifier letter + Lo - Letter without case +M - Mark + Mn - Non-spacing mark + Mc - Spacing combining mark + Me - Enclosing mark +N - Number + Nd - Decimal digit + Nl - Letter number + No - Other number +P - Punctuation + Pd - Dash punctuation + Ps - Open punctuation + Pe - Close punctuation + Pi - Initial punctuation + Pf - Final punctuation + Pc - Connector punctuation + Po - Other punctuation +S - Symbol + Sm - Math symbol + Sc - Currency symbol + Sk - Modifier symbol + So - Other symbol +Z - Separator + Zs - Space separator + Zl - Line separator + Zp - Paragraph separator +C - Other + Cc - Control + Cf - Format + Co - Private use + Cs - Surrogate + Cn - Unassigned + +Example usage: + + \p{N} + \p{Cn} +*/ + + +// will be populated by addUnicodePackage +exports.packages = {}; + +addUnicodePackage({ + L: "0041-005A0061-007A00AA00B500BA00C0-00D600D8-00F600F8-02C102C6-02D102E0-02E402EC02EE0370-037403760377037A-037D03860388-038A038C038E-03A103A3-03F503F7-0481048A-05250531-055605590561-058705D0-05EA05F0-05F20621-064A066E066F0671-06D306D506E506E606EE06EF06FA-06FC06FF07100712-072F074D-07A507B107CA-07EA07F407F507FA0800-0815081A082408280904-0939093D09500958-0961097109720979-097F0985-098C098F09900993-09A809AA-09B009B209B6-09B909BD09CE09DC09DD09DF-09E109F009F10A05-0A0A0A0F0A100A13-0A280A2A-0A300A320A330A350A360A380A390A59-0A5C0A5E0A72-0A740A85-0A8D0A8F-0A910A93-0AA80AAA-0AB00AB20AB30AB5-0AB90ABD0AD00AE00AE10B05-0B0C0B0F0B100B13-0B280B2A-0B300B320B330B35-0B390B3D0B5C0B5D0B5F-0B610B710B830B85-0B8A0B8E-0B900B92-0B950B990B9A0B9C0B9E0B9F0BA30BA40BA8-0BAA0BAE-0BB90BD00C05-0C0C0C0E-0C100C12-0C280C2A-0C330C35-0C390C3D0C580C590C600C610C85-0C8C0C8E-0C900C92-0CA80CAA-0CB30CB5-0CB90CBD0CDE0CE00CE10D05-0D0C0D0E-0D100D12-0D280D2A-0D390D3D0D600D610D7A-0D7F0D85-0D960D9A-0DB10DB3-0DBB0DBD0DC0-0DC60E01-0E300E320E330E40-0E460E810E820E840E870E880E8A0E8D0E94-0E970E99-0E9F0EA1-0EA30EA50EA70EAA0EAB0EAD-0EB00EB20EB30EBD0EC0-0EC40EC60EDC0EDD0F000F40-0F470F49-0F6C0F88-0F8B1000-102A103F1050-1055105A-105D106110651066106E-10701075-1081108E10A0-10C510D0-10FA10FC1100-1248124A-124D1250-12561258125A-125D1260-1288128A-128D1290-12B012B2-12B512B8-12BE12C012C2-12C512C8-12D612D8-13101312-13151318-135A1380-138F13A0-13F41401-166C166F-167F1681-169A16A0-16EA1700-170C170E-17111720-17311740-17511760-176C176E-17701780-17B317D717DC1820-18771880-18A818AA18B0-18F51900-191C1950-196D1970-19741980-19AB19C1-19C71A00-1A161A20-1A541AA71B05-1B331B45-1B4B1B83-1BA01BAE1BAF1C00-1C231C4D-1C4F1C5A-1C7D1CE9-1CEC1CEE-1CF11D00-1DBF1E00-1F151F18-1F1D1F20-1F451F48-1F4D1F50-1F571F591F5B1F5D1F5F-1F7D1F80-1FB41FB6-1FBC1FBE1FC2-1FC41FC6-1FCC1FD0-1FD31FD6-1FDB1FE0-1FEC1FF2-1FF41FF6-1FFC2071207F2090-209421022107210A-211321152119-211D212421262128212A-212D212F-2139213C-213F2145-2149214E218321842C00-2C2E2C30-2C5E2C60-2CE42CEB-2CEE2D00-2D252D30-2D652D6F2D80-2D962DA0-2DA62DA8-2DAE2DB0-2DB62DB8-2DBE2DC0-2DC62DC8-2DCE2DD0-2DD62DD8-2DDE2E2F300530063031-3035303B303C3041-3096309D-309F30A1-30FA30FC-30FF3105-312D3131-318E31A0-31B731F0-31FF3400-4DB54E00-9FCBA000-A48CA4D0-A4FDA500-A60CA610-A61FA62AA62BA640-A65FA662-A66EA67F-A697A6A0-A6E5A717-A71FA722-A788A78BA78CA7FB-A801A803-A805A807-A80AA80C-A822A840-A873A882-A8B3A8F2-A8F7A8FBA90A-A925A930-A946A960-A97CA984-A9B2A9CFAA00-AA28AA40-AA42AA44-AA4BAA60-AA76AA7AAA80-AAAFAAB1AAB5AAB6AAB9-AABDAAC0AAC2AADB-AADDABC0-ABE2AC00-D7A3D7B0-D7C6D7CB-D7FBF900-FA2DFA30-FA6DFA70-FAD9FB00-FB06FB13-FB17FB1DFB1F-FB28FB2A-FB36FB38-FB3CFB3EFB40FB41FB43FB44FB46-FBB1FBD3-FD3DFD50-FD8FFD92-FDC7FDF0-FDFBFE70-FE74FE76-FEFCFF21-FF3AFF41-FF5AFF66-FFBEFFC2-FFC7FFCA-FFCFFFD2-FFD7FFDA-FFDC", + Ll: "0061-007A00AA00B500BA00DF-00F600F8-00FF01010103010501070109010B010D010F01110113011501170119011B011D011F01210123012501270129012B012D012F01310133013501370138013A013C013E014001420144014601480149014B014D014F01510153015501570159015B015D015F01610163016501670169016B016D016F0171017301750177017A017C017E-0180018301850188018C018D019201950199-019B019E01A101A301A501A801AA01AB01AD01B001B401B601B901BA01BD-01BF01C601C901CC01CE01D001D201D401D601D801DA01DC01DD01DF01E101E301E501E701E901EB01ED01EF01F001F301F501F901FB01FD01FF02010203020502070209020B020D020F02110213021502170219021B021D021F02210223022502270229022B022D022F02310233-0239023C023F0240024202470249024B024D024F-02930295-02AF037103730377037B-037D039003AC-03CE03D003D103D5-03D703D903DB03DD03DF03E103E303E503E703E903EB03ED03EF-03F303F503F803FB03FC0430-045F04610463046504670469046B046D046F04710473047504770479047B047D047F0481048B048D048F04910493049504970499049B049D049F04A104A304A504A704A904AB04AD04AF04B104B304B504B704B904BB04BD04BF04C204C404C604C804CA04CC04CE04CF04D104D304D504D704D904DB04DD04DF04E104E304E504E704E904EB04ED04EF04F104F304F504F704F904FB04FD04FF05010503050505070509050B050D050F05110513051505170519051B051D051F0521052305250561-05871D00-1D2B1D62-1D771D79-1D9A1E011E031E051E071E091E0B1E0D1E0F1E111E131E151E171E191E1B1E1D1E1F1E211E231E251E271E291E2B1E2D1E2F1E311E331E351E371E391E3B1E3D1E3F1E411E431E451E471E491E4B1E4D1E4F1E511E531E551E571E591E5B1E5D1E5F1E611E631E651E671E691E6B1E6D1E6F1E711E731E751E771E791E7B1E7D1E7F1E811E831E851E871E891E8B1E8D1E8F1E911E931E95-1E9D1E9F1EA11EA31EA51EA71EA91EAB1EAD1EAF1EB11EB31EB51EB71EB91EBB1EBD1EBF1EC11EC31EC51EC71EC91ECB1ECD1ECF1ED11ED31ED51ED71ED91EDB1EDD1EDF1EE11EE31EE51EE71EE91EEB1EED1EEF1EF11EF31EF51EF71EF91EFB1EFD1EFF-1F071F10-1F151F20-1F271F30-1F371F40-1F451F50-1F571F60-1F671F70-1F7D1F80-1F871F90-1F971FA0-1FA71FB0-1FB41FB61FB71FBE1FC2-1FC41FC61FC71FD0-1FD31FD61FD71FE0-1FE71FF2-1FF41FF61FF7210A210E210F2113212F21342139213C213D2146-2149214E21842C30-2C5E2C612C652C662C682C6A2C6C2C712C732C742C76-2C7C2C812C832C852C872C892C8B2C8D2C8F2C912C932C952C972C992C9B2C9D2C9F2CA12CA32CA52CA72CA92CAB2CAD2CAF2CB12CB32CB52CB72CB92CBB2CBD2CBF2CC12CC32CC52CC72CC92CCB2CCD2CCF2CD12CD32CD52CD72CD92CDB2CDD2CDF2CE12CE32CE42CEC2CEE2D00-2D25A641A643A645A647A649A64BA64DA64FA651A653A655A657A659A65BA65DA65FA663A665A667A669A66BA66DA681A683A685A687A689A68BA68DA68FA691A693A695A697A723A725A727A729A72BA72DA72F-A731A733A735A737A739A73BA73DA73FA741A743A745A747A749A74BA74DA74FA751A753A755A757A759A75BA75DA75FA761A763A765A767A769A76BA76DA76FA771-A778A77AA77CA77FA781A783A785A787A78CFB00-FB06FB13-FB17FF41-FF5A", + Lu: "0041-005A00C0-00D600D8-00DE01000102010401060108010A010C010E01100112011401160118011A011C011E01200122012401260128012A012C012E01300132013401360139013B013D013F0141014301450147014A014C014E01500152015401560158015A015C015E01600162016401660168016A016C016E017001720174017601780179017B017D018101820184018601870189-018B018E-0191019301940196-0198019C019D019F01A001A201A401A601A701A901AC01AE01AF01B1-01B301B501B701B801BC01C401C701CA01CD01CF01D101D301D501D701D901DB01DE01E001E201E401E601E801EA01EC01EE01F101F401F6-01F801FA01FC01FE02000202020402060208020A020C020E02100212021402160218021A021C021E02200222022402260228022A022C022E02300232023A023B023D023E02410243-02460248024A024C024E03700372037603860388-038A038C038E038F0391-03A103A3-03AB03CF03D2-03D403D803DA03DC03DE03E003E203E403E603E803EA03EC03EE03F403F703F903FA03FD-042F04600462046404660468046A046C046E04700472047404760478047A047C047E0480048A048C048E04900492049404960498049A049C049E04A004A204A404A604A804AA04AC04AE04B004B204B404B604B804BA04BC04BE04C004C104C304C504C704C904CB04CD04D004D204D404D604D804DA04DC04DE04E004E204E404E604E804EA04EC04EE04F004F204F404F604F804FA04FC04FE05000502050405060508050A050C050E05100512051405160518051A051C051E0520052205240531-055610A0-10C51E001E021E041E061E081E0A1E0C1E0E1E101E121E141E161E181E1A1E1C1E1E1E201E221E241E261E281E2A1E2C1E2E1E301E321E341E361E381E3A1E3C1E3E1E401E421E441E461E481E4A1E4C1E4E1E501E521E541E561E581E5A1E5C1E5E1E601E621E641E661E681E6A1E6C1E6E1E701E721E741E761E781E7A1E7C1E7E1E801E821E841E861E881E8A1E8C1E8E1E901E921E941E9E1EA01EA21EA41EA61EA81EAA1EAC1EAE1EB01EB21EB41EB61EB81EBA1EBC1EBE1EC01EC21EC41EC61EC81ECA1ECC1ECE1ED01ED21ED41ED61ED81EDA1EDC1EDE1EE01EE21EE41EE61EE81EEA1EEC1EEE1EF01EF21EF41EF61EF81EFA1EFC1EFE1F08-1F0F1F18-1F1D1F28-1F2F1F38-1F3F1F48-1F4D1F591F5B1F5D1F5F1F68-1F6F1FB8-1FBB1FC8-1FCB1FD8-1FDB1FE8-1FEC1FF8-1FFB21022107210B-210D2110-211221152119-211D212421262128212A-212D2130-2133213E213F214521832C00-2C2E2C602C62-2C642C672C692C6B2C6D-2C702C722C752C7E-2C802C822C842C862C882C8A2C8C2C8E2C902C922C942C962C982C9A2C9C2C9E2CA02CA22CA42CA62CA82CAA2CAC2CAE2CB02CB22CB42CB62CB82CBA2CBC2CBE2CC02CC22CC42CC62CC82CCA2CCC2CCE2CD02CD22CD42CD62CD82CDA2CDC2CDE2CE02CE22CEB2CEDA640A642A644A646A648A64AA64CA64EA650A652A654A656A658A65AA65CA65EA662A664A666A668A66AA66CA680A682A684A686A688A68AA68CA68EA690A692A694A696A722A724A726A728A72AA72CA72EA732A734A736A738A73AA73CA73EA740A742A744A746A748A74AA74CA74EA750A752A754A756A758A75AA75CA75EA760A762A764A766A768A76AA76CA76EA779A77BA77DA77EA780A782A784A786A78BFF21-FF3A", + Lt: "01C501C801CB01F21F88-1F8F1F98-1F9F1FA8-1FAF1FBC1FCC1FFC", + Lm: "02B0-02C102C6-02D102E0-02E402EC02EE0374037A0559064006E506E607F407F507FA081A0824082809710E460EC610FC17D718431AA71C78-1C7D1D2C-1D611D781D9B-1DBF2071207F2090-20942C7D2D6F2E2F30053031-3035303B309D309E30FC-30FEA015A4F8-A4FDA60CA67FA717-A71FA770A788A9CFAA70AADDFF70FF9EFF9F", + Lo: "01BB01C0-01C3029405D0-05EA05F0-05F20621-063F0641-064A066E066F0671-06D306D506EE06EF06FA-06FC06FF07100712-072F074D-07A507B107CA-07EA0800-08150904-0939093D09500958-096109720979-097F0985-098C098F09900993-09A809AA-09B009B209B6-09B909BD09CE09DC09DD09DF-09E109F009F10A05-0A0A0A0F0A100A13-0A280A2A-0A300A320A330A350A360A380A390A59-0A5C0A5E0A72-0A740A85-0A8D0A8F-0A910A93-0AA80AAA-0AB00AB20AB30AB5-0AB90ABD0AD00AE00AE10B05-0B0C0B0F0B100B13-0B280B2A-0B300B320B330B35-0B390B3D0B5C0B5D0B5F-0B610B710B830B85-0B8A0B8E-0B900B92-0B950B990B9A0B9C0B9E0B9F0BA30BA40BA8-0BAA0BAE-0BB90BD00C05-0C0C0C0E-0C100C12-0C280C2A-0C330C35-0C390C3D0C580C590C600C610C85-0C8C0C8E-0C900C92-0CA80CAA-0CB30CB5-0CB90CBD0CDE0CE00CE10D05-0D0C0D0E-0D100D12-0D280D2A-0D390D3D0D600D610D7A-0D7F0D85-0D960D9A-0DB10DB3-0DBB0DBD0DC0-0DC60E01-0E300E320E330E40-0E450E810E820E840E870E880E8A0E8D0E94-0E970E99-0E9F0EA1-0EA30EA50EA70EAA0EAB0EAD-0EB00EB20EB30EBD0EC0-0EC40EDC0EDD0F000F40-0F470F49-0F6C0F88-0F8B1000-102A103F1050-1055105A-105D106110651066106E-10701075-1081108E10D0-10FA1100-1248124A-124D1250-12561258125A-125D1260-1288128A-128D1290-12B012B2-12B512B8-12BE12C012C2-12C512C8-12D612D8-13101312-13151318-135A1380-138F13A0-13F41401-166C166F-167F1681-169A16A0-16EA1700-170C170E-17111720-17311740-17511760-176C176E-17701780-17B317DC1820-18421844-18771880-18A818AA18B0-18F51900-191C1950-196D1970-19741980-19AB19C1-19C71A00-1A161A20-1A541B05-1B331B45-1B4B1B83-1BA01BAE1BAF1C00-1C231C4D-1C4F1C5A-1C771CE9-1CEC1CEE-1CF12135-21382D30-2D652D80-2D962DA0-2DA62DA8-2DAE2DB0-2DB62DB8-2DBE2DC0-2DC62DC8-2DCE2DD0-2DD62DD8-2DDE3006303C3041-3096309F30A1-30FA30FF3105-312D3131-318E31A0-31B731F0-31FF3400-4DB54E00-9FCBA000-A014A016-A48CA4D0-A4F7A500-A60BA610-A61FA62AA62BA66EA6A0-A6E5A7FB-A801A803-A805A807-A80AA80C-A822A840-A873A882-A8B3A8F2-A8F7A8FBA90A-A925A930-A946A960-A97CA984-A9B2AA00-AA28AA40-AA42AA44-AA4BAA60-AA6FAA71-AA76AA7AAA80-AAAFAAB1AAB5AAB6AAB9-AABDAAC0AAC2AADBAADCABC0-ABE2AC00-D7A3D7B0-D7C6D7CB-D7FBF900-FA2DFA30-FA6DFA70-FAD9FB1DFB1F-FB28FB2A-FB36FB38-FB3CFB3EFB40FB41FB43FB44FB46-FBB1FBD3-FD3DFD50-FD8FFD92-FDC7FDF0-FDFBFE70-FE74FE76-FEFCFF66-FF6FFF71-FF9DFFA0-FFBEFFC2-FFC7FFCA-FFCFFFD2-FFD7FFDA-FFDC", + M: "0300-036F0483-04890591-05BD05BF05C105C205C405C505C70610-061A064B-065E067006D6-06DC06DE-06E406E706E806EA-06ED07110730-074A07A6-07B007EB-07F30816-0819081B-08230825-08270829-082D0900-0903093C093E-094E0951-0955096209630981-098309BC09BE-09C409C709C809CB-09CD09D709E209E30A01-0A030A3C0A3E-0A420A470A480A4B-0A4D0A510A700A710A750A81-0A830ABC0ABE-0AC50AC7-0AC90ACB-0ACD0AE20AE30B01-0B030B3C0B3E-0B440B470B480B4B-0B4D0B560B570B620B630B820BBE-0BC20BC6-0BC80BCA-0BCD0BD70C01-0C030C3E-0C440C46-0C480C4A-0C4D0C550C560C620C630C820C830CBC0CBE-0CC40CC6-0CC80CCA-0CCD0CD50CD60CE20CE30D020D030D3E-0D440D46-0D480D4A-0D4D0D570D620D630D820D830DCA0DCF-0DD40DD60DD8-0DDF0DF20DF30E310E34-0E3A0E47-0E4E0EB10EB4-0EB90EBB0EBC0EC8-0ECD0F180F190F350F370F390F3E0F3F0F71-0F840F860F870F90-0F970F99-0FBC0FC6102B-103E1056-1059105E-10601062-10641067-106D1071-10741082-108D108F109A-109D135F1712-17141732-1734175217531772177317B6-17D317DD180B-180D18A91920-192B1930-193B19B0-19C019C819C91A17-1A1B1A55-1A5E1A60-1A7C1A7F1B00-1B041B34-1B441B6B-1B731B80-1B821BA1-1BAA1C24-1C371CD0-1CD21CD4-1CE81CED1CF21DC0-1DE61DFD-1DFF20D0-20F02CEF-2CF12DE0-2DFF302A-302F3099309AA66F-A672A67CA67DA6F0A6F1A802A806A80BA823-A827A880A881A8B4-A8C4A8E0-A8F1A926-A92DA947-A953A980-A983A9B3-A9C0AA29-AA36AA43AA4CAA4DAA7BAAB0AAB2-AAB4AAB7AAB8AABEAABFAAC1ABE3-ABEAABECABEDFB1EFE00-FE0FFE20-FE26", + Mn: "0300-036F0483-04870591-05BD05BF05C105C205C405C505C70610-061A064B-065E067006D6-06DC06DF-06E406E706E806EA-06ED07110730-074A07A6-07B007EB-07F30816-0819081B-08230825-08270829-082D0900-0902093C0941-0948094D0951-095509620963098109BC09C1-09C409CD09E209E30A010A020A3C0A410A420A470A480A4B-0A4D0A510A700A710A750A810A820ABC0AC1-0AC50AC70AC80ACD0AE20AE30B010B3C0B3F0B41-0B440B4D0B560B620B630B820BC00BCD0C3E-0C400C46-0C480C4A-0C4D0C550C560C620C630CBC0CBF0CC60CCC0CCD0CE20CE30D41-0D440D4D0D620D630DCA0DD2-0DD40DD60E310E34-0E3A0E47-0E4E0EB10EB4-0EB90EBB0EBC0EC8-0ECD0F180F190F350F370F390F71-0F7E0F80-0F840F860F870F90-0F970F99-0FBC0FC6102D-10301032-10371039103A103D103E10581059105E-10601071-1074108210851086108D109D135F1712-17141732-1734175217531772177317B7-17BD17C617C9-17D317DD180B-180D18A91920-19221927192819321939-193B1A171A181A561A58-1A5E1A601A621A65-1A6C1A73-1A7C1A7F1B00-1B031B341B36-1B3A1B3C1B421B6B-1B731B801B811BA2-1BA51BA81BA91C2C-1C331C361C371CD0-1CD21CD4-1CE01CE2-1CE81CED1DC0-1DE61DFD-1DFF20D0-20DC20E120E5-20F02CEF-2CF12DE0-2DFF302A-302F3099309AA66FA67CA67DA6F0A6F1A802A806A80BA825A826A8C4A8E0-A8F1A926-A92DA947-A951A980-A982A9B3A9B6-A9B9A9BCAA29-AA2EAA31AA32AA35AA36AA43AA4CAAB0AAB2-AAB4AAB7AAB8AABEAABFAAC1ABE5ABE8ABEDFB1EFE00-FE0FFE20-FE26", + Mc: "0903093E-09400949-094C094E0982098309BE-09C009C709C809CB09CC09D70A030A3E-0A400A830ABE-0AC00AC90ACB0ACC0B020B030B3E0B400B470B480B4B0B4C0B570BBE0BBF0BC10BC20BC6-0BC80BCA-0BCC0BD70C01-0C030C41-0C440C820C830CBE0CC0-0CC40CC70CC80CCA0CCB0CD50CD60D020D030D3E-0D400D46-0D480D4A-0D4C0D570D820D830DCF-0DD10DD8-0DDF0DF20DF30F3E0F3F0F7F102B102C10311038103B103C105610571062-10641067-106D108310841087-108C108F109A-109C17B617BE-17C517C717C81923-19261929-192B193019311933-193819B0-19C019C819C91A19-1A1B1A551A571A611A631A641A6D-1A721B041B351B3B1B3D-1B411B431B441B821BA11BA61BA71BAA1C24-1C2B1C341C351CE11CF2A823A824A827A880A881A8B4-A8C3A952A953A983A9B4A9B5A9BAA9BBA9BD-A9C0AA2FAA30AA33AA34AA4DAA7BABE3ABE4ABE6ABE7ABE9ABEAABEC", + Me: "0488048906DE20DD-20E020E2-20E4A670-A672", + N: "0030-003900B200B300B900BC-00BE0660-066906F0-06F907C0-07C90966-096F09E6-09EF09F4-09F90A66-0A6F0AE6-0AEF0B66-0B6F0BE6-0BF20C66-0C6F0C78-0C7E0CE6-0CEF0D66-0D750E50-0E590ED0-0ED90F20-0F331040-10491090-10991369-137C16EE-16F017E0-17E917F0-17F91810-18191946-194F19D0-19DA1A80-1A891A90-1A991B50-1B591BB0-1BB91C40-1C491C50-1C5920702074-20792080-20892150-21822185-21892460-249B24EA-24FF2776-27932CFD30073021-30293038-303A3192-31953220-32293251-325F3280-328932B1-32BFA620-A629A6E6-A6EFA830-A835A8D0-A8D9A900-A909A9D0-A9D9AA50-AA59ABF0-ABF9FF10-FF19", + Nd: "0030-00390660-066906F0-06F907C0-07C90966-096F09E6-09EF0A66-0A6F0AE6-0AEF0B66-0B6F0BE6-0BEF0C66-0C6F0CE6-0CEF0D66-0D6F0E50-0E590ED0-0ED90F20-0F291040-10491090-109917E0-17E91810-18191946-194F19D0-19DA1A80-1A891A90-1A991B50-1B591BB0-1BB91C40-1C491C50-1C59A620-A629A8D0-A8D9A900-A909A9D0-A9D9AA50-AA59ABF0-ABF9FF10-FF19", + Nl: "16EE-16F02160-21822185-218830073021-30293038-303AA6E6-A6EF", + No: "00B200B300B900BC-00BE09F4-09F90BF0-0BF20C78-0C7E0D70-0D750F2A-0F331369-137C17F0-17F920702074-20792080-20892150-215F21892460-249B24EA-24FF2776-27932CFD3192-31953220-32293251-325F3280-328932B1-32BFA830-A835", + P: "0021-00230025-002A002C-002F003A003B003F0040005B-005D005F007B007D00A100AB00B700BB00BF037E0387055A-055F0589058A05BE05C005C305C605F305F40609060A060C060D061B061E061F066A-066D06D40700-070D07F7-07F90830-083E0964096509700DF40E4F0E5A0E5B0F04-0F120F3A-0F3D0F850FD0-0FD4104A-104F10FB1361-13681400166D166E169B169C16EB-16ED1735173617D4-17D617D8-17DA1800-180A1944194519DE19DF1A1E1A1F1AA0-1AA61AA8-1AAD1B5A-1B601C3B-1C3F1C7E1C7F1CD32010-20272030-20432045-20512053-205E207D207E208D208E2329232A2768-277527C527C627E6-27EF2983-299829D8-29DB29FC29FD2CF9-2CFC2CFE2CFF2E00-2E2E2E302E313001-30033008-30113014-301F3030303D30A030FBA4FEA4FFA60D-A60FA673A67EA6F2-A6F7A874-A877A8CEA8CFA8F8-A8FAA92EA92FA95FA9C1-A9CDA9DEA9DFAA5C-AA5FAADEAADFABEBFD3EFD3FFE10-FE19FE30-FE52FE54-FE61FE63FE68FE6AFE6BFF01-FF03FF05-FF0AFF0C-FF0FFF1AFF1BFF1FFF20FF3B-FF3DFF3FFF5BFF5DFF5F-FF65", + Pd: "002D058A05BE140018062010-20152E172E1A301C303030A0FE31FE32FE58FE63FF0D", + Ps: "0028005B007B0F3A0F3C169B201A201E2045207D208D23292768276A276C276E27702772277427C527E627E827EA27EC27EE2983298529872989298B298D298F299129932995299729D829DA29FC2E222E242E262E283008300A300C300E3010301430163018301A301DFD3EFE17FE35FE37FE39FE3BFE3DFE3FFE41FE43FE47FE59FE5BFE5DFF08FF3BFF5BFF5FFF62", + Pe: "0029005D007D0F3B0F3D169C2046207E208E232A2769276B276D276F27712773277527C627E727E927EB27ED27EF298429862988298A298C298E2990299229942996299829D929DB29FD2E232E252E272E293009300B300D300F3011301530173019301B301E301FFD3FFE18FE36FE38FE3AFE3CFE3EFE40FE42FE44FE48FE5AFE5CFE5EFF09FF3DFF5DFF60FF63", + Pi: "00AB2018201B201C201F20392E022E042E092E0C2E1C2E20", + Pf: "00BB2019201D203A2E032E052E0A2E0D2E1D2E21", + Pc: "005F203F20402054FE33FE34FE4D-FE4FFF3F", + Po: "0021-00230025-0027002A002C002E002F003A003B003F0040005C00A100B700BF037E0387055A-055F058905C005C305C605F305F40609060A060C060D061B061E061F066A-066D06D40700-070D07F7-07F90830-083E0964096509700DF40E4F0E5A0E5B0F04-0F120F850FD0-0FD4104A-104F10FB1361-1368166D166E16EB-16ED1735173617D4-17D617D8-17DA1800-18051807-180A1944194519DE19DF1A1E1A1F1AA0-1AA61AA8-1AAD1B5A-1B601C3B-1C3F1C7E1C7F1CD3201620172020-20272030-2038203B-203E2041-20432047-205120532055-205E2CF9-2CFC2CFE2CFF2E002E012E06-2E082E0B2E0E-2E162E182E192E1B2E1E2E1F2E2A-2E2E2E302E313001-3003303D30FBA4FEA4FFA60D-A60FA673A67EA6F2-A6F7A874-A877A8CEA8CFA8F8-A8FAA92EA92FA95FA9C1-A9CDA9DEA9DFAA5C-AA5FAADEAADFABEBFE10-FE16FE19FE30FE45FE46FE49-FE4CFE50-FE52FE54-FE57FE5F-FE61FE68FE6AFE6BFF01-FF03FF05-FF07FF0AFF0CFF0EFF0FFF1AFF1BFF1FFF20FF3CFF61FF64FF65", + S: "0024002B003C-003E005E0060007C007E00A2-00A900AC00AE-00B100B400B600B800D700F702C2-02C502D2-02DF02E5-02EB02ED02EF-02FF03750384038503F604820606-0608060B060E060F06E906FD06FE07F609F209F309FA09FB0AF10B700BF3-0BFA0C7F0CF10CF20D790E3F0F01-0F030F13-0F170F1A-0F1F0F340F360F380FBE-0FC50FC7-0FCC0FCE0FCF0FD5-0FD8109E109F13601390-139917DB194019E0-19FF1B61-1B6A1B74-1B7C1FBD1FBF-1FC11FCD-1FCF1FDD-1FDF1FED-1FEF1FFD1FFE20442052207A-207C208A-208C20A0-20B8210021012103-21062108210921142116-2118211E-2123212521272129212E213A213B2140-2144214A-214D214F2190-2328232B-23E82400-24262440-244A249C-24E92500-26CD26CF-26E126E326E8-26FF2701-27042706-2709270C-27272729-274B274D274F-27522756-275E2761-276727942798-27AF27B1-27BE27C0-27C427C7-27CA27CC27D0-27E527F0-29822999-29D729DC-29FB29FE-2B4C2B50-2B592CE5-2CEA2E80-2E992E9B-2EF32F00-2FD52FF0-2FFB300430123013302030363037303E303F309B309C319031913196-319F31C0-31E33200-321E322A-32503260-327F328A-32B032C0-32FE3300-33FF4DC0-4DFFA490-A4C6A700-A716A720A721A789A78AA828-A82BA836-A839AA77-AA79FB29FDFCFDFDFE62FE64-FE66FE69FF04FF0BFF1C-FF1EFF3EFF40FF5CFF5EFFE0-FFE6FFE8-FFEEFFFCFFFD", + Sm: "002B003C-003E007C007E00AC00B100D700F703F60606-060820442052207A-207C208A-208C2140-2144214B2190-2194219A219B21A021A321A621AE21CE21CF21D221D421F4-22FF2308-230B23202321237C239B-23B323DC-23E125B725C125F8-25FF266F27C0-27C427C7-27CA27CC27D0-27E527F0-27FF2900-29822999-29D729DC-29FB29FE-2AFF2B30-2B442B47-2B4CFB29FE62FE64-FE66FF0BFF1C-FF1EFF5CFF5EFFE2FFE9-FFEC", + Sc: "002400A2-00A5060B09F209F309FB0AF10BF90E3F17DB20A0-20B8A838FDFCFE69FF04FFE0FFE1FFE5FFE6", + Sk: "005E006000A800AF00B400B802C2-02C502D2-02DF02E5-02EB02ED02EF-02FF0375038403851FBD1FBF-1FC11FCD-1FCF1FDD-1FDF1FED-1FEF1FFD1FFE309B309CA700-A716A720A721A789A78AFF3EFF40FFE3", + So: "00A600A700A900AE00B000B60482060E060F06E906FD06FE07F609FA0B700BF3-0BF80BFA0C7F0CF10CF20D790F01-0F030F13-0F170F1A-0F1F0F340F360F380FBE-0FC50FC7-0FCC0FCE0FCF0FD5-0FD8109E109F13601390-1399194019E0-19FF1B61-1B6A1B74-1B7C210021012103-21062108210921142116-2118211E-2123212521272129212E213A213B214A214C214D214F2195-2199219C-219F21A121A221A421A521A7-21AD21AF-21CD21D021D121D321D5-21F32300-2307230C-231F2322-2328232B-237B237D-239A23B4-23DB23E2-23E82400-24262440-244A249C-24E92500-25B625B8-25C025C2-25F72600-266E2670-26CD26CF-26E126E326E8-26FF2701-27042706-2709270C-27272729-274B274D274F-27522756-275E2761-276727942798-27AF27B1-27BE2800-28FF2B00-2B2F2B452B462B50-2B592CE5-2CEA2E80-2E992E9B-2EF32F00-2FD52FF0-2FFB300430123013302030363037303E303F319031913196-319F31C0-31E33200-321E322A-32503260-327F328A-32B032C0-32FE3300-33FF4DC0-4DFFA490-A4C6A828-A82BA836A837A839AA77-AA79FDFDFFE4FFE8FFEDFFEEFFFCFFFD", + Z: "002000A01680180E2000-200A20282029202F205F3000", + Zs: "002000A01680180E2000-200A202F205F3000", + Zl: "2028", + Zp: "2029", + C: "0000-001F007F-009F00AD03780379037F-0383038B038D03A20526-05300557055805600588058B-059005C8-05CF05EB-05EF05F5-0605061C061D0620065F06DD070E070F074B074C07B2-07BF07FB-07FF082E082F083F-08FF093A093B094F095609570973-097809800984098D098E0991099209A909B109B3-09B509BA09BB09C509C609C909CA09CF-09D609D8-09DB09DE09E409E509FC-0A000A040A0B-0A0E0A110A120A290A310A340A370A3A0A3B0A3D0A43-0A460A490A4A0A4E-0A500A52-0A580A5D0A5F-0A650A76-0A800A840A8E0A920AA90AB10AB40ABA0ABB0AC60ACA0ACE0ACF0AD1-0ADF0AE40AE50AF00AF2-0B000B040B0D0B0E0B110B120B290B310B340B3A0B3B0B450B460B490B4A0B4E-0B550B58-0B5B0B5E0B640B650B72-0B810B840B8B-0B8D0B910B96-0B980B9B0B9D0BA0-0BA20BA5-0BA70BAB-0BAD0BBA-0BBD0BC3-0BC50BC90BCE0BCF0BD1-0BD60BD8-0BE50BFB-0C000C040C0D0C110C290C340C3A-0C3C0C450C490C4E-0C540C570C5A-0C5F0C640C650C70-0C770C800C810C840C8D0C910CA90CB40CBA0CBB0CC50CC90CCE-0CD40CD7-0CDD0CDF0CE40CE50CF00CF3-0D010D040D0D0D110D290D3A-0D3C0D450D490D4E-0D560D58-0D5F0D640D650D76-0D780D800D810D840D97-0D990DB20DBC0DBE0DBF0DC7-0DC90DCB-0DCE0DD50DD70DE0-0DF10DF5-0E000E3B-0E3E0E5C-0E800E830E850E860E890E8B0E8C0E8E-0E930E980EA00EA40EA60EA80EA90EAC0EBA0EBE0EBF0EC50EC70ECE0ECF0EDA0EDB0EDE-0EFF0F480F6D-0F700F8C-0F8F0F980FBD0FCD0FD9-0FFF10C6-10CF10FD-10FF1249124E124F12571259125E125F1289128E128F12B112B612B712BF12C112C612C712D7131113161317135B-135E137D-137F139A-139F13F5-13FF169D-169F16F1-16FF170D1715-171F1737-173F1754-175F176D17711774-177F17B417B517DE17DF17EA-17EF17FA-17FF180F181A-181F1878-187F18AB-18AF18F6-18FF191D-191F192C-192F193C-193F1941-1943196E196F1975-197F19AC-19AF19CA-19CF19DB-19DD1A1C1A1D1A5F1A7D1A7E1A8A-1A8F1A9A-1A9F1AAE-1AFF1B4C-1B4F1B7D-1B7F1BAB-1BAD1BBA-1BFF1C38-1C3A1C4A-1C4C1C80-1CCF1CF3-1CFF1DE7-1DFC1F161F171F1E1F1F1F461F471F4E1F4F1F581F5A1F5C1F5E1F7E1F7F1FB51FC51FD41FD51FDC1FF01FF11FF51FFF200B-200F202A-202E2060-206F20722073208F2095-209F20B9-20CF20F1-20FF218A-218F23E9-23FF2427-243F244B-245F26CE26E226E4-26E727002705270A270B2728274C274E2753-2755275F27602795-279727B027BF27CB27CD-27CF2B4D-2B4F2B5A-2BFF2C2F2C5F2CF2-2CF82D26-2D2F2D66-2D6E2D70-2D7F2D97-2D9F2DA72DAF2DB72DBF2DC72DCF2DD72DDF2E32-2E7F2E9A2EF4-2EFF2FD6-2FEF2FFC-2FFF3040309730983100-3104312E-3130318F31B8-31BF31E4-31EF321F32FF4DB6-4DBF9FCC-9FFFA48D-A48FA4C7-A4CFA62C-A63FA660A661A674-A67BA698-A69FA6F8-A6FFA78D-A7FAA82C-A82FA83A-A83FA878-A87FA8C5-A8CDA8DA-A8DFA8FC-A8FFA954-A95EA97D-A97FA9CEA9DA-A9DDA9E0-A9FFAA37-AA3FAA4EAA4FAA5AAA5BAA7C-AA7FAAC3-AADAAAE0-ABBFABEEABEFABFA-ABFFD7A4-D7AFD7C7-D7CAD7FC-F8FFFA2EFA2FFA6EFA6FFADA-FAFFFB07-FB12FB18-FB1CFB37FB3DFB3FFB42FB45FBB2-FBD2FD40-FD4FFD90FD91FDC8-FDEFFDFEFDFFFE1A-FE1FFE27-FE2FFE53FE67FE6C-FE6FFE75FEFD-FF00FFBF-FFC1FFC8FFC9FFD0FFD1FFD8FFD9FFDD-FFDFFFE7FFEF-FFFBFFFEFFFF", + Cc: "0000-001F007F-009F", + Cf: "00AD0600-060306DD070F17B417B5200B-200F202A-202E2060-2064206A-206FFEFFFFF9-FFFB", + Co: "E000-F8FF", + Cs: "D800-DFFF", + Cn: "03780379037F-0383038B038D03A20526-05300557055805600588058B-059005C8-05CF05EB-05EF05F5-05FF06040605061C061D0620065F070E074B074C07B2-07BF07FB-07FF082E082F083F-08FF093A093B094F095609570973-097809800984098D098E0991099209A909B109B3-09B509BA09BB09C509C609C909CA09CF-09D609D8-09DB09DE09E409E509FC-0A000A040A0B-0A0E0A110A120A290A310A340A370A3A0A3B0A3D0A43-0A460A490A4A0A4E-0A500A52-0A580A5D0A5F-0A650A76-0A800A840A8E0A920AA90AB10AB40ABA0ABB0AC60ACA0ACE0ACF0AD1-0ADF0AE40AE50AF00AF2-0B000B040B0D0B0E0B110B120B290B310B340B3A0B3B0B450B460B490B4A0B4E-0B550B58-0B5B0B5E0B640B650B72-0B810B840B8B-0B8D0B910B96-0B980B9B0B9D0BA0-0BA20BA5-0BA70BAB-0BAD0BBA-0BBD0BC3-0BC50BC90BCE0BCF0BD1-0BD60BD8-0BE50BFB-0C000C040C0D0C110C290C340C3A-0C3C0C450C490C4E-0C540C570C5A-0C5F0C640C650C70-0C770C800C810C840C8D0C910CA90CB40CBA0CBB0CC50CC90CCE-0CD40CD7-0CDD0CDF0CE40CE50CF00CF3-0D010D040D0D0D110D290D3A-0D3C0D450D490D4E-0D560D58-0D5F0D640D650D76-0D780D800D810D840D97-0D990DB20DBC0DBE0DBF0DC7-0DC90DCB-0DCE0DD50DD70DE0-0DF10DF5-0E000E3B-0E3E0E5C-0E800E830E850E860E890E8B0E8C0E8E-0E930E980EA00EA40EA60EA80EA90EAC0EBA0EBE0EBF0EC50EC70ECE0ECF0EDA0EDB0EDE-0EFF0F480F6D-0F700F8C-0F8F0F980FBD0FCD0FD9-0FFF10C6-10CF10FD-10FF1249124E124F12571259125E125F1289128E128F12B112B612B712BF12C112C612C712D7131113161317135B-135E137D-137F139A-139F13F5-13FF169D-169F16F1-16FF170D1715-171F1737-173F1754-175F176D17711774-177F17DE17DF17EA-17EF17FA-17FF180F181A-181F1878-187F18AB-18AF18F6-18FF191D-191F192C-192F193C-193F1941-1943196E196F1975-197F19AC-19AF19CA-19CF19DB-19DD1A1C1A1D1A5F1A7D1A7E1A8A-1A8F1A9A-1A9F1AAE-1AFF1B4C-1B4F1B7D-1B7F1BAB-1BAD1BBA-1BFF1C38-1C3A1C4A-1C4C1C80-1CCF1CF3-1CFF1DE7-1DFC1F161F171F1E1F1F1F461F471F4E1F4F1F581F5A1F5C1F5E1F7E1F7F1FB51FC51FD41FD51FDC1FF01FF11FF51FFF2065-206920722073208F2095-209F20B9-20CF20F1-20FF218A-218F23E9-23FF2427-243F244B-245F26CE26E226E4-26E727002705270A270B2728274C274E2753-2755275F27602795-279727B027BF27CB27CD-27CF2B4D-2B4F2B5A-2BFF2C2F2C5F2CF2-2CF82D26-2D2F2D66-2D6E2D70-2D7F2D97-2D9F2DA72DAF2DB72DBF2DC72DCF2DD72DDF2E32-2E7F2E9A2EF4-2EFF2FD6-2FEF2FFC-2FFF3040309730983100-3104312E-3130318F31B8-31BF31E4-31EF321F32FF4DB6-4DBF9FCC-9FFFA48D-A48FA4C7-A4CFA62C-A63FA660A661A674-A67BA698-A69FA6F8-A6FFA78D-A7FAA82C-A82FA83A-A83FA878-A87FA8C5-A8CDA8DA-A8DFA8FC-A8FFA954-A95EA97D-A97FA9CEA9DA-A9DDA9E0-A9FFAA37-AA3FAA4EAA4FAA5AAA5BAA7C-AA7FAAC3-AADAAAE0-ABBFABEEABEFABFA-ABFFD7A4-D7AFD7C7-D7CAD7FC-D7FFFA2EFA2FFA6EFA6FFADA-FAFFFB07-FB12FB18-FB1CFB37FB3DFB3FFB42FB45FBB2-FBD2FD40-FD4FFD90FD91FDC8-FDEFFDFEFDFFFE1A-FE1FFE27-FE2FFE53FE67FE6C-FE6FFE75FEFDFEFEFF00FFBF-FFC1FFC8FFC9FFD0FFD1FFD8FFD9FFDD-FFDFFFE7FFEF-FFF8FFFEFFFF" +}); + +function addUnicodePackage (pack) { + var codePoint = /\w{4}/g; + for (var name in pack) + exports.packages[name] = pack[name].replace(codePoint, "\\u$&"); +}; + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/document', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/lib/event_emitter', 'ace/range', 'ace/anchor'], function(require, exports, module) { +"use strict"; + +var oop = require("./lib/oop"); +var EventEmitter = require("./lib/event_emitter").EventEmitter; +var Range = require("./range").Range; +var Anchor = require("./anchor").Anchor; + +/** + * class Document + * + * Contains the text of the document. Documents are controlled by a single [[EditSession `EditSession`]]. At its core, `Document`s are just an array of strings, with each row in the document matching up to the array index. + * + * + **/ + + /** + * new Document([text]) + * - text (String | Array): The starting text + * + * Creates a new `Document`. If `text` is included, the `Document` contains those strings; otherwise, it's empty. + * + **/ +var Document = function(text) { + this.$lines = []; + + if (Array.isArray(text)) { + this.insertLines(0, text); + } + // There has to be one line at least in the document. If you pass an empty + // string to the insert function, nothing will happen. Workaround. + else if (text.length == 0) { + this.$lines = [""]; + } else { + this.insert({row: 0, column:0}, text); + } +}; + +(function() { + + oop.implement(this, EventEmitter); + + /** + * Document.setValue(text) -> Void + * - text (String): The text to use + * + * Replaces all the lines in the current `Document` with the value of `text`. + **/ + this.setValue = function(text) { + var len = this.getLength(); + this.remove(new Range(0, 0, len, this.getLine(len-1).length)); + this.insert({row: 0, column:0}, text); + }; + + /** + * Document.getValue() -> String + * + * Returns all the lines in the document as a single string, split by the new line character. + **/ + this.getValue = function() { + return this.getAllLines().join(this.getNewLineCharacter()); + }; + + /** + * Document.createAnchor(row, column) -> Anchor + * - row (Number): The row number to use + * - column (Number): The column number to use + * + * Creates a new `Anchor` to define a floating point in the document. + **/ + this.createAnchor = function(row, column) { + return new Anchor(this, row, column); + }; + + /** internal, hide + * Document.$split(text) -> [String] + * - text (String): The text to work with + * + ([String]): A String array, with each index containing a piece of the original `text` string. + * + * Splits a string of text on any newline (`\n`) or carriage-return ('\r') characters. + * + * + **/ + + // check for IE split bug + if ("aaa".split(/a/).length == 0) + this.$split = function(text) { + return text.replace(/\r\n|\r/g, "\n").split("\n"); + } + else + this.$split = function(text) { + return text.split(/\r\n|\r|\n/); + }; + + + /** internal, hide + * Document.$detectNewLine(text) -> Void + * + * + **/ + this.$detectNewLine = function(text) { + var match = text.match(/^.*?(\r\n|\r|\n)/m); + if (match) { + this.$autoNewLine = match[1]; + } else { + this.$autoNewLine = "\n"; + } + }; + + /** + * Document.getNewLineCharacter() -> String + * + (String): If `newLineMode == windows`, `\r\n` is returned.
+ * If `newLineMode == unix`, `\n` is returned.
+ * If `newLineMode == auto`, the value of `autoNewLine` is returned. + * + * Returns the newline character that's being used, depending on the value of `newLineMode`. + * + * + * + **/ + this.getNewLineCharacter = function() { + switch (this.$newLineMode) { + case "windows": + return "\r\n"; + + case "unix": + return "\n"; + + case "auto": + return this.$autoNewLine; + } + }; + + this.$autoNewLine = "\n"; + this.$newLineMode = "auto"; + /** + * Document.setNewLineMode(newLineMode) -> Void + * - newLineMode(String): [The newline mode to use; can be either `windows`, `unix`, or `auto`]{: #Document.setNewLineMode.param} + * + * [Sets the new line mode.]{: #Document.setNewLineMode.desc} + **/ + this.setNewLineMode = function(newLineMode) { + if (this.$newLineMode === newLineMode) + return; + + this.$newLineMode = newLineMode; + }; + + /** + * Document.getNewLineMode() -> String + * + * [Returns the type of newlines being used; either `windows`, `unix`, or `auto`]{: #Document.getNewLineMode} + * + **/ + this.getNewLineMode = function() { + return this.$newLineMode; + }; + + /** + * Document.isNewLine(text) -> Boolean + * - text (String): The text to check + * + * Returns `true` if `text` is a newline character (either `\r\n`, `\r`, or `\n`). + * + **/ + this.isNewLine = function(text) { + return (text == "\r\n" || text == "\r" || text == "\n"); + }; + + /** + * Document.getLine(row) -> String + * - row (Number): The row index to retrieve + * + * Returns a verbatim copy of the given line as it is in the document + * + **/ + this.getLine = function(row) { + return this.$lines[row] || ""; + }; + + /** + * Document.getLines(firstRow, lastRow) -> [String] + * - firstRow (Number): The first row index to retrieve + * - lastRow (Number): The final row index to retrieve + * + * Returns an array of strings of the rows between `firstRow` and `lastRow`. This function is inclusive of `lastRow`. + * + **/ + this.getLines = function(firstRow, lastRow) { + return this.$lines.slice(firstRow, lastRow + 1); + }; + + /** + * Document.getAllLines() -> [String] + * + * Returns all lines in the document as string array. Warning: The caller should not modify this array! + **/ + this.getAllLines = function() { + return this.getLines(0, this.getLength()); + }; + + /** + * Document.getLength() -> Number + * + * Returns the number of rows in the document. + **/ + this.getLength = function() { + return this.$lines.length; + }; + + /** + * Document.getTextRange(range) -> String + * - range (Range): The range to work with + * + * [Given a range within the document, this function returns all the text within that range as a single string.]{: #Document.getTextRange.desc} + **/ + this.getTextRange = function(range) { + if (range.start.row == range.end.row) { + return this.$lines[range.start.row].substring(range.start.column, + range.end.column); + } + else { + var lines = this.getLines(range.start.row+1, range.end.row-1); + lines.unshift((this.$lines[range.start.row] || "").substring(range.start.column)); + lines.push((this.$lines[range.end.row] || "").substring(0, range.end.column)); + return lines.join(this.getNewLineCharacter()); + } + }; + + /** internal, hide + * Document.$clipPosition(position) -> Number + * + * + **/ + this.$clipPosition = function(position) { + var length = this.getLength(); + if (position.row >= length) { + position.row = Math.max(0, length - 1); + position.column = this.getLine(length-1).length; + } + return position; + }; + + /** + * Document.insert(position, text) -> Number + * - position (Number): The position to start inserting at + * - text (String): A chunk of text to insert + * + (Number): The position of the last line of `text`. If the length of `text` is 0, this function simply returns `position`. + * Inserts a block of `text` and the indicated `position`. + * + * + **/ + this.insert = function(position, text) { + if (!text || text.length === 0) + return position; + + position = this.$clipPosition(position); + + // only detect new lines if the document has no line break yet + if (this.getLength() <= 1) + this.$detectNewLine(text); + + var lines = this.$split(text); + var firstLine = lines.splice(0, 1)[0]; + var lastLine = lines.length == 0 ? null : lines.splice(lines.length - 1, 1)[0]; + + position = this.insertInLine(position, firstLine); + if (lastLine !== null) { + position = this.insertNewLine(position); // terminate first line + position = this.insertLines(position.row, lines); + position = this.insertInLine(position, lastLine || ""); + } + return position; + }; + + /** + * Document.insertLines(row, lines) -> Object + * - row (Number): The index of the row to insert at + * - lines (Array): An array of strings + * + (Object): Returns an object containing the final row and column, like this:
+ * ```{row: endRow, column: 0}```
+ * If `lines` is empty, this function returns an object containing the current row, and column, like this:
+ * ```{row: row, column: 0}``` + * + * Inserts the elements in `lines` into the document, starting at the row index given by `row`. This method also triggers the `'change'` event. + * + * + **/ + this.insertLines = function(row, lines) { + if (lines.length == 0) + return {row: row, column: 0}; + + var args = [row, 0]; + args.push.apply(args, lines); + this.$lines.splice.apply(this.$lines, args); + + var range = new Range(row, 0, row + lines.length, 0); + var delta = { + action: "insertLines", + range: range, + lines: lines + }; + this._emit("change", { data: delta }); + return range.end; + }; + + /** + * Document.insertNewLine(position) -> Object + * - position (String): The position to insert at + * + (Object): Returns an object containing the final row and column, like this:
+ * ```{row: endRow, column: 0}``` + * + * Inserts a new line into the document at the current row's `position`. This method also triggers the `'change'` event. + * + * + * + **/ + this.insertNewLine = function(position) { + position = this.$clipPosition(position); + var line = this.$lines[position.row] || ""; + + this.$lines[position.row] = line.substring(0, position.column); + this.$lines.splice(position.row + 1, 0, line.substring(position.column, line.length)); + + var end = { + row : position.row + 1, + column : 0 + }; + + var delta = { + action: "insertText", + range: Range.fromPoints(position, end), + text: this.getNewLineCharacter() + }; + this._emit("change", { data: delta }); + + return end; + }; + + /** + * Document.insertInLine(position, text) -> Object | Number + * - position (Number): The position to insert at + * - text (String): A chunk of text + * + (Object): Returns an object containing the final row and column, like this:
+ * ```{row: endRow, column: 0}``` + * + (Number): If `text` is empty, this function returns the value of `position` + * + * Inserts `text` into the `position` at the current row. This method also triggers the `'change'` event. + * + * + * + **/ + this.insertInLine = function(position, text) { + if (text.length == 0) + return position; + + var line = this.$lines[position.row] || ""; + + this.$lines[position.row] = line.substring(0, position.column) + text + + line.substring(position.column); + + var end = { + row : position.row, + column : position.column + text.length + }; + + var delta = { + action: "insertText", + range: Range.fromPoints(position, end), + text: text + }; + this._emit("change", { data: delta }); + + return end; + }; + + /** + * Document.remove(range) -> Object + * - range (Range): A specified Range to remove + * + (Object): Returns the new `start` property of the range, which contains `startRow` and `startColumn`. If `range` is empty, this function returns the unmodified value of `range.start`. + * + * Removes the `range` from the document. + * + * + **/ + this.remove = function(range) { + // clip to document + range.start = this.$clipPosition(range.start); + range.end = this.$clipPosition(range.end); + + if (range.isEmpty()) + return range.start; + + var firstRow = range.start.row; + var lastRow = range.end.row; + + if (range.isMultiLine()) { + var firstFullRow = range.start.column == 0 ? firstRow : firstRow + 1; + var lastFullRow = lastRow - 1; + + if (range.end.column > 0) + this.removeInLine(lastRow, 0, range.end.column); + + if (lastFullRow >= firstFullRow) + this.removeLines(firstFullRow, lastFullRow); + + if (firstFullRow != firstRow) { + this.removeInLine(firstRow, range.start.column, this.getLine(firstRow).length); + this.removeNewLine(range.start.row); + } + } + else { + this.removeInLine(firstRow, range.start.column, range.end.column); + } + return range.start; + }; + + /** + * Document.removeInLine(row, startColumn, endColumn) -> Object + * - row (Number): The row to remove from + * - startColumn (Number): The column to start removing at + * - endColumn (Number): The column to stop removing at + * + (Object): Returns an object containing `startRow` and `startColumn`, indicating the new row and column values.
If `startColumn` is equal to `endColumn`, this function returns nothing. + * + * Removes the specified columns from the `row`. This method also triggers the `'change'` event. + * + * + **/ + this.removeInLine = function(row, startColumn, endColumn) { + if (startColumn == endColumn) + return; + + var range = new Range(row, startColumn, row, endColumn); + var line = this.getLine(row); + var removed = line.substring(startColumn, endColumn); + var newLine = line.substring(0, startColumn) + line.substring(endColumn, line.length); + this.$lines.splice(row, 1, newLine); + + var delta = { + action: "removeText", + range: range, + text: removed + }; + this._emit("change", { data: delta }); + return range.start; + }; + + /** + * Document.removeLines(firstRow, lastRow) -> [String] + * - firstRow (Number): The first row to be removed + * - lastRow (Number): The last row to be removed + * + ([String]): Returns all the removed lines. + * + * Removes a range of full lines. This method also triggers the `'change'` event. + * + * + **/ + this.removeLines = function(firstRow, lastRow) { + var range = new Range(firstRow, 0, lastRow + 1, 0); + var removed = this.$lines.splice(firstRow, lastRow - firstRow + 1); + + var delta = { + action: "removeLines", + range: range, + nl: this.getNewLineCharacter(), + lines: removed + }; + this._emit("change", { data: delta }); + return removed; + }; + + /** + * Document.removeNewLine(row) -> Void + * - row (Number): The row to check + * + * Removes the new line between `row` and the row immediately following it. This method also triggers the `'change'` event. + * + **/ + this.removeNewLine = function(row) { + var firstLine = this.getLine(row); + var secondLine = this.getLine(row+1); + + var range = new Range(row, firstLine.length, row+1, 0); + var line = firstLine + secondLine; + + this.$lines.splice(row, 2, line); + + var delta = { + action: "removeText", + range: range, + text: this.getNewLineCharacter() + }; + this._emit("change", { data: delta }); + }; + + /** + * Document.replace(range, text) -> Object + * - range (Range): A specified Range to replace + * - text (String): The new text to use as a replacement + * + (Object): Returns an object containing the final row and column, like this: + * {row: endRow, column: 0} + * If the text and range are empty, this function returns an object containing the current `range.start` value. + * If the text is the exact same as what currently exists, this function returns an object containing the current `range.end` value. + * + * Replaces a range in the document with the new `text`. + * + **/ + this.replace = function(range, text) { + if (text.length == 0 && range.isEmpty()) + return range.start; + + // Shortcut: If the text we want to insert is the same as it is already + // in the document, we don't have to replace anything. + if (text == this.getTextRange(range)) + return range.end; + + this.remove(range); + if (text) { + var end = this.insert(range.start, text); + } + else { + end = range.start; + } + + return end; + }; + + /** + * Document.applyDeltas(deltas) -> Void + * + * Applies all the changes previously accumulated. These can be either `'includeText'`, `'insertLines'`, `'removeText'`, and `'removeLines'`. + **/ + this.applyDeltas = function(deltas) { + for (var i=0; i Void + * + * Reverts any changes previously applied. These can be either `'includeText'`, `'insertLines'`, `'removeText'`, and `'removeLines'`. + **/ + this.revertDeltas = function(deltas) { + for (var i=deltas.length-1; i>=0; i--) { + var delta = deltas[i]; + + var range = Range.fromPoints(delta.range.start, delta.range.end); + + if (delta.action == "insertLines") + this.removeLines(range.start.row, range.end.row - 1); + else if (delta.action == "insertText") + this.remove(range); + else if (delta.action == "removeLines") + this.insertLines(range.start.row, delta.lines); + else if (delta.action == "removeText") + this.insert(range.start, delta.text); + } + }; + +}).call(Document.prototype); + +exports.Document = Document; +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/anchor', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/lib/event_emitter'], function(require, exports, module) { +"use strict"; + +var oop = require("./lib/oop"); +var EventEmitter = require("./lib/event_emitter").EventEmitter; + +/** + * class Anchor + * + * Defines the floating pointer in the document. Whenever text is inserted or deleted before the cursor, the position of the cursor is updated + * + **/ + +/** + * new Anchor(doc, row, column) + * - doc (Document): The document to associate with the anchor + * - row (Number): The starting row position + * - column (Number): The starting column position + * + * Creates a new `Anchor` and associates it with a document. + * + **/ + +var Anchor = exports.Anchor = function(doc, row, column) { + this.document = doc; + + if (typeof column == "undefined") + this.setPosition(row.row, row.column); + else + this.setPosition(row, column); + + this.$onChange = this.onChange.bind(this); + doc.on("change", this.$onChange); +}; + +(function() { + + oop.implement(this, EventEmitter); + + /** + * Anchor.getPosition() -> Object + * + * Returns an object identifying the `row` and `column` position of the current anchor. + * + **/ + + this.getPosition = function() { + return this.$clipPositionToDocument(this.row, this.column); + }; + + /** + * Anchor.getDocument() -> Document + * + * Returns the current document. + * + **/ + + this.getDocument = function() { + return this.document; + }; + + /** + * Anchor@onChange(e) + * - e (Event): Contains data about the event + * + * Fires whenever the anchor position changes. Events that can trigger this function include `'includeText'`, `'insertLines'`, `'removeText'`, and `'removeLines'`. + * + **/ + + this.onChange = function(e) { + var delta = e.data; + var range = delta.range; + + if (range.start.row == range.end.row && range.start.row != this.row) + return; + + if (range.start.row > this.row) + return; + + if (range.start.row == this.row && range.start.column > this.column) + return; + + var row = this.row; + var column = this.column; + + if (delta.action === "insertText") { + if (range.start.row === row && range.start.column <= column) { + if (range.start.row === range.end.row) { + column += range.end.column - range.start.column; + } + else { + column -= range.start.column; + row += range.end.row - range.start.row; + } + } + else if (range.start.row !== range.end.row && range.start.row < row) { + row += range.end.row - range.start.row; + } + } else if (delta.action === "insertLines") { + if (range.start.row <= row) { + row += range.end.row - range.start.row; + } + } + else if (delta.action == "removeText") { + if (range.start.row == row && range.start.column < column) { + if (range.end.column >= column) + column = range.start.column; + else + column = Math.max(0, column - (range.end.column - range.start.column)); + + } else if (range.start.row !== range.end.row && range.start.row < row) { + if (range.end.row == row) { + column = Math.max(0, column - range.end.column) + range.start.column; + } + row -= (range.end.row - range.start.row); + } + else if (range.end.row == row) { + row -= range.end.row - range.start.row; + column = Math.max(0, column - range.end.column) + range.start.column; + } + } else if (delta.action == "removeLines") { + if (range.start.row <= row) { + if (range.end.row <= row) + row -= range.end.row - range.start.row; + else { + row = range.start.row; + column = 0; + } + } + } + + this.setPosition(row, column, true); + }; + + /** + * Anchor.setPosition(row, column, noClip) + * - row (Number): The row index to move the anchor to + * - column (Number): The column index to move the anchor to + * - noClip (Boolean): Identifies if you want the position to be clipped + * + * Sets the anchor position to the specified row and column. If `noClip` is `true`, the position is not clipped. + * + **/ + + this.setPosition = function(row, column, noClip) { + var pos; + if (noClip) { + pos = { + row: row, + column: column + }; + } + else { + pos = this.$clipPositionToDocument(row, column); + } + + if (this.row == pos.row && this.column == pos.column) + return; + + var old = { + row: this.row, + column: this.column + }; + + this.row = pos.row; + this.column = pos.column; + this._emit("change", { + old: old, + value: pos + }); + }; + + /** + * Anchor.detach() + * + * When called, the `'change'` event listener is removed. + * + **/ + + this.detach = function() { + this.document.removeEventListener("change", this.$onChange); + }; + + /** internal, hide + * Anchor.clipPositionToDocument(row, column) + * - row (Number): The row index to clip the anchor to + * - column (Number): The column index to clip the anchor to + * + * Clips the anchor position to the specified row and column. + * + **/ + + this.$clipPositionToDocument = function(row, column) { + var pos = {}; + + if (row >= this.document.getLength()) { + pos.row = Math.max(0, this.document.getLength() - 1); + pos.column = this.document.getLine(pos.row).length; + } + else if (row < 0) { + pos.row = 0; + pos.column = 0; + } + else { + pos.row = row; + pos.column = Math.min(this.document.getLine(pos.row).length, Math.max(0, column)); + } + + if (column < 0) + pos.column = 0; + + return pos; + }; + +}).call(Anchor.prototype); + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/background_tokenizer', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/lib/event_emitter'], function(require, exports, module) { +"use strict"; + +var oop = require("./lib/oop"); +var EventEmitter = require("./lib/event_emitter").EventEmitter; + +/** + * class BackgroundTokenizer + * + * Tokenizes the current [[Document `Document`]] in the background, and caches the tokenized rows for future use. If a certain row is changed, everything below that row is re-tokenized. + * + **/ + +/** + * new BackgroundTokenizer(tokenizer, editor) + * - tokenizer (Tokenizer): The tokenizer to use + * - editor (Editor): The editor to associate with + * + * Creates a new `BackgroundTokenizer` object. + * + * + **/ + +var BackgroundTokenizer = function(tokenizer, editor) { + this.running = false; + this.lines = []; + this.currentLine = 0; + this.tokenizer = tokenizer; + + var self = this; + + this.$worker = function() { + if (!self.running) { return; } + + var workerStart = new Date(); + var startLine = self.currentLine; + var doc = self.doc; + + var processedLines = 0; + + var len = doc.getLength(); + while (self.currentLine < len) { + self.lines[self.currentLine] = self.$tokenizeRows(self.currentLine, self.currentLine)[0]; + self.currentLine++; + + // only check every 5 lines + processedLines += 1; + if ((processedLines % 5 == 0) && (new Date() - workerStart) > 20) { + self.fireUpdateEvent(startLine, self.currentLine-1); + self.running = setTimeout(self.$worker, 20); + return; + } + } + + self.running = false; + + self.fireUpdateEvent(startLine, len - 1); + }; +}; + +(function(){ + + oop.implement(this, EventEmitter); + + /** + * BackgroundTokenizer.setTokenizer(tokenizer) + * - tokenizer (Tokenizer): The new tokenizer to use + * + * Sets a new tokenizer for this object. + * + **/ + + this.setTokenizer = function(tokenizer) { + this.tokenizer = tokenizer; + this.lines = []; + + this.start(0); + }; + + /** + * BackgroundTokenizer.setDocument(doc) + * - doc (Document): The new document to associate with + * + * Sets a new document to associate with this object. + * + **/ + + this.setDocument = function(doc) { + this.doc = doc; + this.lines = []; + + this.stop(); + }; + + /** + * BackgroundTokenizer.fireUpdateEvent(firstRow, lastRow) + * - firstRow (Number): The starting row region + * - lastRow (Number): The final row region + * + * Emits the `'update'` event. `firstRow` and `lastRow` are used to define the boundaries of the region to be updated. + * + **/ + + this.fireUpdateEvent = function(firstRow, lastRow) { + var data = { + first: firstRow, + last: lastRow + }; + this._emit("update", {data: data}); + }; + + /** + * BackgroundTokenizer.start(startRow) + * - startRow (Number): The row to start at + * + * Starts tokenizing at the row indicated. + * + **/ + + this.start = function(startRow) { + this.currentLine = Math.min(startRow || 0, this.currentLine, + this.doc.getLength()); + + // remove all cached items below this line + this.lines.splice(this.currentLine, this.lines.length); + + this.stop(); + // pretty long delay to prevent the tokenizer from interfering with the user + this.running = setTimeout(this.$worker, 700); + }; + + /** + * BackgroundTokenizer.stop() + * + * Stops tokenizing. + * + **/ + + this.stop = function() { + if (this.running) + clearTimeout(this.running); + this.running = false; + }; + + /** related to: BackgroundTokenizer.$tokenizeRows + * BackgroundTokenizer.getTokens(firstRow, lastRow) -> [Object] + * - firstRow (Number): The row to start at + * - lastRow (Number): The row to finish at + * + * Starts tokenizing at the row indicated. Returns a list of objects of the tokenized rows. + * + **/ + + this.getTokens = function(firstRow, lastRow) { + return this.$tokenizeRows(firstRow, lastRow); + }; + + /** + * BackgroundTokenizer.getState(row) -> String + * - row (Number): The row to start at + * + * [Returns the state of tokenization for a row.]{: #BackgroundTokenizer.getState} + * + **/ + + this.getState = function(row) { + return this.$tokenizeRows(row, row)[0].state; + }; + + /** + * BackgroundTokenizer.$tokenizeRows(firstRow, lastRow) -> [Object] + * - startRow (Number): The row to start at + * - lastRow (Number): The row to finish at + * + ([Object]): A list of the tokenized rows. Each item in the list is an object with two properties, `state` and `start`. + * + * Tokenizes all the rows within the specified region. + * + * + **/ + this.$tokenizeRows = function(firstRow, lastRow) { + if (!this.doc || isNaN(firstRow) || isNaN(lastRow)) + return [{'state':'start','tokens':[]}]; + + var rows = []; + + // determine start state + var state = "start"; + var doCache = false; + if (firstRow > 0 && this.lines[firstRow - 1]) { + state = this.lines[firstRow - 1].state; + doCache = true; + } else if (firstRow == 0) { + state = "start"; + doCache = true; + } else if (this.lines.length > 0) { + // Guess that we haven't changed state. + state = this.lines[this.lines.length-1].state; + } + + var lines = this.doc.getLines(firstRow, lastRow); + for (var row=firstRow; row<=lastRow; row++) { + if (!this.lines[row]) { + var tokens = this.tokenizer.getLineTokens(lines[row-firstRow] || "", state); + var state = tokens.state; + rows.push(tokens); + + if (doCache) { + this.lines[row] = tokens; + } + } + else { + var tokens = this.lines[row]; + state = tokens.state; + rows.push(tokens); + } + } + return rows; + }; + +}).call(BackgroundTokenizer.prototype); + +exports.BackgroundTokenizer = BackgroundTokenizer; +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Julian Viereck + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/edit_session/folding', ['require', 'exports', 'module' , 'ace/range', 'ace/edit_session/fold_line', 'ace/edit_session/fold', 'ace/token_iterator'], function(require, exports, module) { +"use strict"; + +var Range = require("../range").Range; +var FoldLine = require("./fold_line").FoldLine; +var Fold = require("./fold").Fold; +var TokenIterator = require("../token_iterator").TokenIterator; + +function Folding() { + /* + * Looks up a fold at a given row/column. Possible values for side: + * -1: ignore a fold if fold.start = row/column + * +1: ignore a fold if fold.end = row/column + */ + this.getFoldAt = function(row, column, side) { + var foldLine = this.getFoldLine(row); + if (!foldLine) + return null; + + var folds = foldLine.folds; + for (var i = 0; i < folds.length; i++) { + var fold = folds[i]; + if (fold.range.contains(row, column)) { + if (side == 1 && fold.range.isEnd(row, column)) { + continue; + } else if (side == -1 && fold.range.isStart(row, column)) { + continue; + } + return fold; + } + } + }; + + /* + * Returns all folds in the given range. Note, that this will return folds + * + */ + this.getFoldsInRange = function(range) { + range = range.clone(); + var start = range.start; + var end = range.end; + var foldLines = this.$foldData; + var foundFolds = []; + + start.column += 1; + end.column -= 1; + + for (var i = 0; i < foldLines.length; i++) { + var cmp = foldLines[i].range.compareRange(range); + if (cmp == 2) { + // Range is before foldLine. No intersection. This means, + // there might be other foldLines that intersect. + continue; + } + else if (cmp == -2) { + // Range is after foldLine. There can't be any other foldLines then, + // so let's give up. + break; + } + + var folds = foldLines[i].folds; + for (var j = 0; j < folds.length; j++) { + var fold = folds[j]; + cmp = fold.range.compareRange(range); + if (cmp == -2) { + break; + } else if (cmp == 2) { + continue; + } else + // WTF-state: Can happen due to -1/+1 to start/end column. + if (cmp == 42) { + break; + } + foundFolds.push(fold); + } + } + return foundFolds; + }; + + /* + * Returns all folds in the document + */ + this.getAllFolds = function() { + var folds = []; + var foldLines = this.$foldData; + + function addFold(fold) { + folds.push(fold); + if (!fold.subFolds) + return; + + for (var i = 0; i < fold.subFolds.length; i++) + addFold(fold.subFolds[i]); + } + + for (var i = 0; i < foldLines.length; i++) + for (var j = 0; j < foldLines[i].folds.length; j++) + addFold(foldLines[i].folds[j]); + + return folds; + }; + + /* + * Returns the string between folds at the given position. + * E.g. + * foob|arwolrd -> "bar" + * foobarwol|rd -> "world" + * foobarwolrd -> + * + * where | means the position of row/column + * + * The trim option determs if the return string should be trimed according + * to the "side" passed with the trim value: + * + * E.g. + * foob|arwolrd -trim=-1> "b" + * foobarwol|rd -trim=+1> "rld" + * fo|obarwolrd -trim=00> "foo" + */ + this.getFoldStringAt = function(row, column, trim, foldLine) { + foldLine = foldLine || this.getFoldLine(row); + if (!foldLine) + return null; + + var lastFold = { + end: { column: 0 } + }; + // TODO: Refactor to use getNextFoldTo function. + var str, fold; + for (var i = 0; i < foldLine.folds.length; i++) { + fold = foldLine.folds[i]; + var cmp = fold.range.compareEnd(row, column); + if (cmp == -1) { + str = this + .getLine(fold.start.row) + .substring(lastFold.end.column, fold.start.column); + break; + } + else if (cmp === 0) { + return null; + } + lastFold = fold; + } + if (!str) + str = this.getLine(fold.start.row).substring(lastFold.end.column); + + if (trim == -1) + return str.substring(0, column - lastFold.end.column); + else if (trim == 1) + return str.substring(column - lastFold.end.column); + else + return str; + }; + + this.getFoldLine = function(docRow, startFoldLine) { + var foldData = this.$foldData; + var i = 0; + if (startFoldLine) + i = foldData.indexOf(startFoldLine); + if (i == -1) + i = 0; + for (i; i < foldData.length; i++) { + var foldLine = foldData[i]; + if (foldLine.start.row <= docRow && foldLine.end.row >= docRow) { + return foldLine; + } else if (foldLine.end.row > docRow) { + return null; + } + } + return null; + }; + + // returns the fold which starts after or contains docRow + this.getNextFoldLine = function(docRow, startFoldLine) { + var foldData = this.$foldData; + var i = 0; + if (startFoldLine) + i = foldData.indexOf(startFoldLine); + if (i == -1) + i = 0; + for (i; i < foldData.length; i++) { + var foldLine = foldData[i]; + if (foldLine.end.row >= docRow) { + return foldLine; + } + } + return null; + }; + + this.getFoldedRowCount = function(first, last) { + var foldData = this.$foldData, rowCount = last-first+1; + for (var i = 0; i < foldData.length; i++) { + var foldLine = foldData[i], + end = foldLine.end.row, + start = foldLine.start.row; + if (end >= last) { + if(start < last) { + if(start >= first) + rowCount -= last-start; + else + rowCount = 0;//in one fold + } + break; + } else if(end >= first){ + if (start >= first) //fold inside range + rowCount -= end-start; + else + rowCount -= end-first+1; + } + } + return rowCount; + }; + + this.$addFoldLine = function(foldLine) { + this.$foldData.push(foldLine); + this.$foldData.sort(function(a, b) { + return a.start.row - b.start.row; + }); + return foldLine; + }; + + /* + * Adds a new fold. + * + * @returns + * The new created Fold object or an existing fold object in case the + * passed in range fits an existing fold exactly. + */ + this.addFold = function(placeholder, range) { + var foldData = this.$foldData; + var added = false; + var fold; + + if (placeholder instanceof Fold) + fold = placeholder; + else + fold = new Fold(range, placeholder); + + this.$clipRangeToDocument(fold.range); + + var startRow = fold.start.row; + var startColumn = fold.start.column; + var endRow = fold.end.row; + var endColumn = fold.end.column; + + // --- Some checking --- + if (fold.placeholder.length < 2) + throw "Placeholder has to be at least 2 characters"; + + if (startRow == endRow && endColumn - startColumn < 2) + throw "The range has to be at least 2 characters width"; + + var startFold = this.getFoldAt(startRow, startColumn, 1); + var endFold = this.getFoldAt(endRow, endColumn, -1); + if (startFold && endFold == startFold) + return startFold.addSubFold(fold); + + if ( + (startFold && !startFold.range.isStart(startRow, startColumn)) + || (endFold && !endFold.range.isEnd(endRow, endColumn)) + ) { + throw "A fold can't intersect already existing fold" + fold.range + startFold.range; + } + + // Check if there are folds in the range we create the new fold for. + var folds = this.getFoldsInRange(fold.range); + if (folds.length > 0) { + // Remove the folds from fold data. + this.removeFolds(folds); + // Add the removed folds as subfolds on the new fold. + fold.subFolds = folds; + } + + for (var i = 0; i < foldData.length; i++) { + var foldLine = foldData[i]; + if (endRow == foldLine.start.row) { + foldLine.addFold(fold); + added = true; + break; + } + else if (startRow == foldLine.end.row) { + foldLine.addFold(fold); + added = true; + if (!fold.sameRow) { + // Check if we might have to merge two FoldLines. + var foldLineNext = foldData[i + 1]; + if (foldLineNext && foldLineNext.start.row == endRow) { + // We need to merge! + foldLine.merge(foldLineNext); + break; + } + } + break; + } + else if (endRow <= foldLine.start.row) { + break; + } + } + + if (!added) + foldLine = this.$addFoldLine(new FoldLine(this.$foldData, fold)); + + if (this.$useWrapMode) + this.$updateWrapData(foldLine.start.row, foldLine.start.row); + + // Notify that fold data has changed. + this.$modified = true; + this._emit("changeFold", { data: fold }); + + return fold; + }; + + this.addFolds = function(folds) { + folds.forEach(function(fold) { + this.addFold(fold); + }, this); + }; + + this.removeFold = function(fold) { + var foldLine = fold.foldLine; + var startRow = foldLine.start.row; + var endRow = foldLine.end.row; + + var foldLines = this.$foldData; + var folds = foldLine.folds; + // Simple case where there is only one fold in the FoldLine such that + // the entire fold line can get removed directly. + if (folds.length == 1) { + foldLines.splice(foldLines.indexOf(foldLine), 1); + } else + // If the fold is the last fold of the foldLine, just remove it. + if (foldLine.range.isEnd(fold.end.row, fold.end.column)) { + folds.pop(); + foldLine.end.row = folds[folds.length - 1].end.row; + foldLine.end.column = folds[folds.length - 1].end.column; + } else + // If the fold is the first fold of the foldLine, just remove it. + if (foldLine.range.isStart(fold.start.row, fold.start.column)) { + folds.shift(); + foldLine.start.row = folds[0].start.row; + foldLine.start.column = folds[0].start.column; + } else + // We know there are more then 2 folds and the fold is not at the edge. + // This means, the fold is somewhere in between. + // + // If the fold is in one row, we just can remove it. + if (fold.sameRow) { + folds.splice(folds.indexOf(fold), 1); + } else + // The fold goes over more then one row. This means remvoing this fold + // will cause the fold line to get splitted up. newFoldLine is the second part + { + var newFoldLine = foldLine.split(fold.start.row, fold.start.column); + folds = newFoldLine.folds; + folds.shift(); + newFoldLine.start.row = folds[0].start.row; + newFoldLine.start.column = folds[0].start.column; + } + + if (this.$useWrapMode) { + this.$updateWrapData(startRow, endRow); + } + + // Notify that fold data has changed. + this.$modified = true; + this._emit("changeFold", { data: fold }); + }; + + this.removeFolds = function(folds) { + // We need to clone the folds array passed in as it might be the folds + // array of a fold line and as we call this.removeFold(fold), folds + // are removed from folds and changes the current index. + var cloneFolds = []; + for (var i = 0; i < folds.length; i++) { + cloneFolds.push(folds[i]); + } + + cloneFolds.forEach(function(fold) { + this.removeFold(fold); + }, this); + this.$modified = true; + }; + + this.expandFold = function(fold) { + this.removeFold(fold); + fold.subFolds.forEach(function(fold) { + this.addFold(fold); + }, this); + fold.subFolds = []; + }; + + this.expandFolds = function(folds) { + folds.forEach(function(fold) { + this.expandFold(fold); + }, this); + }; + + this.unfold = function(location, expandInner) { + var range, folds; + if (location == null) + range = new Range(0, 0, this.getLength(), 0); + else if (typeof location == "number") + range = new Range(location, 0, location, this.getLine(location).length); + else if ("row" in location) + range = Range.fromPoints(location, location); + else + range = location; + + folds = this.getFoldsInRange(range); + if (expandInner) { + this.removeFolds(folds); + } else { + // TODO: might need to remove and add folds in one go instead of using + // expandFolds several times. + while (folds.length) { + this.expandFolds(folds); + folds = this.getFoldsInRange(range); + } + } + }; + + /* + * Checks if a given documentRow is folded. This is true if there are some + * folded parts such that some parts of the line is still visible. + **/ + this.isRowFolded = function(docRow, startFoldRow) { + return !!this.getFoldLine(docRow, startFoldRow); + }; + + this.getRowFoldEnd = function(docRow, startFoldRow) { + var foldLine = this.getFoldLine(docRow, startFoldRow); + return (foldLine + ? foldLine.end.row + : docRow); + }; + + this.getFoldDisplayLine = function(foldLine, endRow, endColumn, startRow, startColumn) { + if (startRow == null) { + startRow = foldLine.start.row; + startColumn = 0; + } + + if (endRow == null) { + endRow = foldLine.end.row; + endColumn = this.getLine(endRow).length; + } + + // Build the textline using the FoldLine walker. + var doc = this.doc; + var textLine = ""; + + foldLine.walk(function(placeholder, row, column, lastColumn) { + if (row < startRow) { + return; + } else if (row == startRow) { + if (column < startColumn) { + return; + } + lastColumn = Math.max(startColumn, lastColumn); + } + if (placeholder) { + textLine += placeholder; + } else { + textLine += doc.getLine(row).substring(lastColumn, column); + } + }.bind(this), endRow, endColumn); + return textLine; + }; + + this.getDisplayLine = function(row, endColumn, startRow, startColumn) { + var foldLine = this.getFoldLine(row); + + if (!foldLine) { + var line; + line = this.doc.getLine(row); + return line.substring(startColumn || 0, endColumn || line.length); + } else { + return this.getFoldDisplayLine( + foldLine, row, endColumn, startRow, startColumn); + } + }; + + this.$cloneFoldData = function() { + var fd = []; + fd = this.$foldData.map(function(foldLine) { + var folds = foldLine.folds.map(function(fold) { + return fold.clone(); + }); + return new FoldLine(fd, folds); + }); + + return fd; + }; + + this.toggleFold = function(tryToUnfold) { + var selection = this.selection; + var range = selection.getRange(); + var fold; + var bracketPos; + + if (range.isEmpty()) { + var cursor = range.start; + fold = this.getFoldAt(cursor.row, cursor.column); + + if (fold) { + this.expandFold(fold); + return; + } + else if (bracketPos = this.findMatchingBracket(cursor)) { + if (range.comparePoint(bracketPos) == 1) { + range.end = bracketPos; + } + else { + range.start = bracketPos; + range.start.column++; + range.end.column--; + } + } + else if (bracketPos = this.findMatchingBracket({row: cursor.row, column: cursor.column + 1})) { + if (range.comparePoint(bracketPos) == 1) + range.end = bracketPos; + else + range.start = bracketPos; + + range.start.column++; + } + else { + range = this.getCommentFoldRange(cursor.row, cursor.column) || range; + } + } else { + var folds = this.getFoldsInRange(range); + if (tryToUnfold && folds.length) { + this.expandFolds(folds); + return; + } + else if (folds.length == 1 ) { + fold = folds[0]; + } + } + + if (!fold) + fold = this.getFoldAt(range.start.row, range.start.column); + + if (fold && fold.range.toString() == range.toString()) { + this.expandFold(fold); + return; + } + + var placeholder = "..."; + if (!range.isMultiLine()) { + placeholder = this.getTextRange(range); + if(placeholder.length < 4) + return; + placeholder = placeholder.trim().substring(0, 2) + ".."; + } + + this.addFold(placeholder, range); + }; + + this.getCommentFoldRange = function(row, column) { + var iterator = new TokenIterator(this, row, column); + var token = iterator.getCurrentToken(); + if (token && /^comment|string/.test(token.type)) { + var range = new Range(); + var re = new RegExp(token.type.replace(/\..*/, "\\.")); + do { + token = iterator.stepBackward(); + } while(token && re.test(token.type)); + + iterator.stepForward(); + range.start.row = iterator.getCurrentTokenRow(); + range.start.column = iterator.getCurrentTokenColumn() + 2; + + iterator = new TokenIterator(this, row, column); + + do { + token = iterator.stepForward(); + } while(token && re.test(token.type)); + + token = iterator.stepBackward(); + + range.end.row = iterator.getCurrentTokenRow(); + range.end.column = iterator.getCurrentTokenColumn() + token.value.length; + return range; + } + }; + + this.foldAll = function(startRow, endRow) { + var foldWidgets = this.foldWidgets; + endRow = endRow || this.getLength(); + for (var row = startRow || 0; row < endRow; row++) { + if (foldWidgets[row] == null) + foldWidgets[row] = this.getFoldWidget(row); + if (foldWidgets[row] != "start") + continue; + + var range = this.getFoldWidgetRange(row); + // sometimes range can be incompatible with existing fold + // wouldn't it be better for addFold to return null istead of throwing? + if (range && range.end.row < endRow) try { + this.addFold("...", range); + } catch(e) {} + } + }; + + this.$foldStyles = { + "manual": 1, + "markbegin": 1, + "markbeginend": 1 + }; + this.$foldStyle = "markbegin"; + this.setFoldStyle = function(style) { + if (!this.$foldStyles[style]) + throw new Error("invalid fold style: " + style + "[" + Object.keys(this.$foldStyles).join(", ") + "]"); + + if (this.$foldStyle == style) + return; + + this.$foldStyle = style; + + if (style == "manual") + this.unfold(); + + // reset folding + var mode = this.$foldMode; + this.$setFolding(null); + this.$setFolding(mode); + }; + + // structured folding + this.$setFolding = function(foldMode) { + if (this.$foldMode == foldMode) + return; + + this.$foldMode = foldMode; + + this.removeListener('change', this.$updateFoldWidgets); + this._emit("changeAnnotation"); + + if (!foldMode || this.$foldStyle == "manual") { + this.foldWidgets = null; + return; + } + + this.foldWidgets = []; + this.getFoldWidget = foldMode.getFoldWidget.bind(foldMode, this, this.$foldStyle); + this.getFoldWidgetRange = foldMode.getFoldWidgetRange.bind(foldMode, this, this.$foldStyle); + + this.$updateFoldWidgets = this.updateFoldWidgets.bind(this); + this.on('change', this.$updateFoldWidgets); + + }; + + this.onFoldWidgetClick = function(row, e) { + var type = this.getFoldWidget(row); + var line = this.getLine(row); + var onlySubfolds = e.shiftKey; + var addSubfolds = onlySubfolds || e.ctrlKey || e.altKey || e.metaKey; + var fold; + + if (type == "end") + fold = this.getFoldAt(row, 0, -1); + else + fold = this.getFoldAt(row, line.length, 1); + + if (fold) { + if (addSubfolds) + this.removeFold(fold); + else + this.expandFold(fold); + return; + } + + var range = this.getFoldWidgetRange(row); + if (range) { + // sometimes singleline folds can be missed by the code above + if (!range.isMultiLine()) { + fold = this.getFoldAt(range.start.row, range.start.column, 1); + if (fold && range.isEqual(fold.range)) { + this.removeFold(fold); + return; + } + } + + if (!onlySubfolds) + this.addFold("...", range); + + if (addSubfolds) + this.foldAll(range.start.row + 1, range.end.row); + } else { + if (addSubfolds) + this.foldAll(row + 1, this.getLength()); + e.target.className += " invalid" + } + }; + + this.updateFoldWidgets = function(e) { + var delta = e.data; + var range = delta.range; + var firstRow = range.start.row; + var len = range.end.row - firstRow; + + if (len === 0) { + this.foldWidgets[firstRow] = null; + } else if (delta.action == "removeText" || delta.action == "removeLines") { + this.foldWidgets.splice(firstRow, len + 1, null); + } else { + var args = Array(len + 1); + args.unshift(firstRow, 1); + this.foldWidgets.splice.apply(this.foldWidgets, args); + } + }; + +} + +exports.Folding = Folding; + +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Julian Viereck + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/edit_session/fold_line', ['require', 'exports', 'module' , 'ace/range'], function(require, exports, module) { +"use strict"; + +var Range = require("../range").Range; + +/* + * If an array is passed in, the folds are expected to be sorted already. + */ +function FoldLine(foldData, folds) { + this.foldData = foldData; + if (Array.isArray(folds)) { + this.folds = folds; + } else { + folds = this.folds = [ folds ]; + } + + var last = folds[folds.length - 1] + this.range = new Range(folds[0].start.row, folds[0].start.column, + last.end.row, last.end.column); + this.start = this.range.start; + this.end = this.range.end; + + this.folds.forEach(function(fold) { + fold.setFoldLine(this); + }, this); +} + +(function() { + /* + * Note: This doesn't update wrapData! + */ + this.shiftRow = function(shift) { + this.start.row += shift; + this.end.row += shift; + this.folds.forEach(function(fold) { + fold.start.row += shift; + fold.end.row += shift; + }); + } + + this.addFold = function(fold) { + if (fold.sameRow) { + if (fold.start.row < this.startRow || fold.endRow > this.endRow) { + throw "Can't add a fold to this FoldLine as it has no connection"; + } + this.folds.push(fold); + this.folds.sort(function(a, b) { + return -a.range.compareEnd(b.start.row, b.start.column); + }); + if (this.range.compareEnd(fold.start.row, fold.start.column) > 0) { + this.end.row = fold.end.row; + this.end.column = fold.end.column; + } else if (this.range.compareStart(fold.end.row, fold.end.column) < 0) { + this.start.row = fold.start.row; + this.start.column = fold.start.column; + } + } else if (fold.start.row == this.end.row) { + this.folds.push(fold); + this.end.row = fold.end.row; + this.end.column = fold.end.column; + } else if (fold.end.row == this.start.row) { + this.folds.unshift(fold); + this.start.row = fold.start.row; + this.start.column = fold.start.column; + } else { + throw "Trying to add fold to FoldRow that doesn't have a matching row"; + } + fold.foldLine = this; + } + + this.containsRow = function(row) { + return row >= this.start.row && row <= this.end.row; + } + + this.walk = function(callback, endRow, endColumn) { + var lastEnd = 0, + folds = this.folds, + fold, + comp, stop, isNewRow = true; + + if (endRow == null) { + endRow = this.end.row; + endColumn = this.end.column; + } + + for (var i = 0; i < folds.length; i++) { + fold = folds[i]; + + comp = fold.range.compareStart(endRow, endColumn); + // This fold is after the endRow/Column. + if (comp == -1) { + callback(null, endRow, endColumn, lastEnd, isNewRow); + return; + } + + stop = callback(null, fold.start.row, fold.start.column, lastEnd, isNewRow); + stop = !stop && callback(fold.placeholder, fold.start.row, fold.start.column, lastEnd); + + // If the user requested to stop the walk or endRow/endColumn is + // inside of this fold (comp == 0), then end here. + if (stop || comp == 0) { + return; + } + + // Note the new lastEnd might not be on the same line. However, + // it's the callback's job to recognize this. + isNewRow = !fold.sameRow; + lastEnd = fold.end.column; + } + callback(null, endRow, endColumn, lastEnd, isNewRow); + } + + this.getNextFoldTo = function(row, column) { + var fold, cmp; + for (var i = 0; i < this.folds.length; i++) { + fold = this.folds[i]; + cmp = fold.range.compareEnd(row, column); + if (cmp == -1) { + return { + fold: fold, + kind: "after" + }; + } else if (cmp == 0) { + return { + fold: fold, + kind: "inside" + } + } + } + return null; + } + + this.addRemoveChars = function(row, column, len) { + var ret = this.getNextFoldTo(row, column), + fold, folds; + if (ret) { + fold = ret.fold; + if (ret.kind == "inside" + && fold.start.column != column + && fold.start.row != row) + { + //throwing here breaks whole editor + //@todo properly handle this + window.console && window.console.log(row, column, fold); + } else if (fold.start.row == row) { + folds = this.folds; + var i = folds.indexOf(fold); + if (i == 0) { + this.start.column += len; + } + for (i; i < folds.length; i++) { + fold = folds[i]; + fold.start.column += len; + if (!fold.sameRow) { + return; + } + fold.end.column += len; + } + this.end.column += len; + } + } + } + + this.split = function(row, column) { + var fold = this.getNextFoldTo(row, column).fold, + folds = this.folds; + var foldData = this.foldData; + + if (!fold) { + return null; + } + var i = folds.indexOf(fold); + var foldBefore = folds[i - 1]; + this.end.row = foldBefore.end.row; + this.end.column = foldBefore.end.column; + + // Remove the folds after row/column and create a new FoldLine + // containing these removed folds. + folds = folds.splice(i, folds.length - i); + + var newFoldLine = new FoldLine(foldData, folds); + foldData.splice(foldData.indexOf(this) + 1, 0, newFoldLine); + return newFoldLine; + } + + this.merge = function(foldLineNext) { + var folds = foldLineNext.folds; + for (var i = 0; i < folds.length; i++) { + this.addFold(folds[i]); + } + // Remove the foldLineNext - no longer needed, as + // it's merged now with foldLineNext. + var foldData = this.foldData; + foldData.splice(foldData.indexOf(foldLineNext), 1); + } + + this.toString = function() { + var ret = [this.range.toString() + ": [" ]; + + this.folds.forEach(function(fold) { + ret.push(" " + fold.toString()); + }); + ret.push("]") + return ret.join("\n"); + } + + this.idxToPosition = function(idx) { + var lastFoldEndColumn = 0; + var fold; + + for (var i = 0; i < this.folds.length; i++) { + var fold = this.folds[i]; + + idx -= fold.start.column - lastFoldEndColumn; + if (idx < 0) { + return { + row: fold.start.row, + column: fold.start.column + idx + }; + } + + idx -= fold.placeholder.length; + if (idx < 0) { + return fold.start; + } + + lastFoldEndColumn = fold.end.column; + } + + return { + row: this.end.row, + column: this.end.column + idx + }; + } +}).call(FoldLine.prototype); + +exports.FoldLine = FoldLine; +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Julian Viereck + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/edit_session/fold', ['require', 'exports', 'module' ], function(require, exports, module) { +"use strict"; + +/* + * Simple fold-data struct. + **/ +var Fold = exports.Fold = function(range, placeholder) { + this.foldLine = null; + this.placeholder = placeholder; + this.range = range; + this.start = range.start; + this.end = range.end; + + this.sameRow = range.start.row == range.end.row; + this.subFolds = []; +}; + +(function() { + + this.toString = function() { + return '"' + this.placeholder + '" ' + this.range.toString(); + }; + + this.setFoldLine = function(foldLine) { + this.foldLine = foldLine; + this.subFolds.forEach(function(fold) { + fold.setFoldLine(foldLine); + }); + }; + + this.clone = function() { + var range = this.range.clone(); + var fold = new Fold(range, this.placeholder); + this.subFolds.forEach(function(subFold) { + fold.subFolds.push(subFold.clone()); + }); + return fold; + }; + + this.addSubFold = function(fold) { + if (this.range.isEqual(fold)) + return this; + + if (!this.range.containsRange(fold)) + throw "A fold can't intersect already existing fold" + fold.range + this.range; + + var row = fold.range.start.row, column = fold.range.start.column; + for (var i = 0, cmp = -1; i < this.subFolds.length; i++) { + cmp = this.subFolds[i].range.compare(row, column); + if (cmp != 1) + break; + } + var afterStart = this.subFolds[i]; + + if (cmp == 0) + return afterStart.addSubFold(fold) + + // cmp == -1 + var row = fold.range.end.row, column = fold.range.end.column; + for (var j = i, cmp = -1; j < this.subFolds.length; j++) { + cmp = this.subFolds[j].range.compare(row, column); + if (cmp != 1) + break; + } + var afterEnd = this.subFolds[j]; + + if (cmp == 0) + throw "A fold can't intersect already existing fold" + fold.range + this.range; + + var consumedFolds = this.subFolds.splice(i, j - i, fold) + fold.setFoldLine(this.foldLine); + + return fold; + } + +}).call(Fold.prototype); + +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/token_iterator', ['require', 'exports', 'module' ], function(require, exports, module) { +"use strict"; + +/** + * class TokenIterator + * + * This class provides an essay way to treat the document as a stream of tokens, and provides methods to iterate over these tokens. + * + **/ + +/** + * new TokenIterator(session, initialRow, initialColumn) + * - session (EditSession): The session to associate with + * - initialRow (Number): The row to start the tokenizing at + * - initialColumn (Number): The column to start the tokenizing at + * + * Creates a new token iterator object. The inital token index is set to the provided row and column coordinates. + * + **/ +var TokenIterator = function(session, initialRow, initialColumn) { + this.$session = session; + this.$row = initialRow; + this.$rowTokens = session.getTokens(initialRow, initialRow)[0].tokens; + + var token = session.getTokenAt(initialRow, initialColumn); + this.$tokenIndex = token ? token.index : -1; +}; + +(function() { + + /** + * TokenIterator.stepBackward() -> [String] + * + (String): If the current point is not at the top of the file, this function returns `null`. Otherwise, it returns an array of the tokenized strings. + * + * Tokenizes all the items from the current point to the row prior in the document. + **/ + this.stepBackward = function() { + this.$tokenIndex -= 1; + + while (this.$tokenIndex < 0) { + this.$row -= 1; + if (this.$row < 0) { + this.$row = 0; + return null; + } + + this.$rowTokens = this.$session.getTokens(this.$row, this.$row)[0].tokens; + this.$tokenIndex = this.$rowTokens.length - 1; + } + + return this.$rowTokens[this.$tokenIndex]; + }; + + /** + * TokenIterator.stepForward() -> String + * + * Tokenizes all the items from the current point until the next row in the document. If the current point is at the end of the file, this function returns `null`. Otherwise, it returns the tokenized string. + **/ + this.stepForward = function() { + var rowCount = this.$session.getLength(); + this.$tokenIndex += 1; + + while (this.$tokenIndex >= this.$rowTokens.length) { + this.$row += 1; + if (this.$row >= rowCount) { + this.$row = rowCount - 1; + return null; + } + + this.$rowTokens = this.$session.getTokens(this.$row, this.$row)[0].tokens; + this.$tokenIndex = 0; + } + + return this.$rowTokens[this.$tokenIndex]; + }; + + /** + * TokenIterator.getCurrentToken() -> String + * + * Returns the current tokenized string. + * + **/ + this.getCurrentToken = function () { + return this.$rowTokens[this.$tokenIndex]; + }; + + /** + * TokenIterator.getCurrentTokenRow() -> Number + * + * Returns the current row. + * + **/ + this.getCurrentTokenRow = function () { + return this.$row; + }; + + /** + * TokenIterator.getCurrentTokenColumn() -> Number + * + * Returns the current column. + * + **/ + this.getCurrentTokenColumn = function() { + var rowTokens = this.$rowTokens; + var tokenIndex = this.$tokenIndex; + + // If a column was cached by EditSession.getTokenAt, then use it + var column = rowTokens[tokenIndex].start; + if (column !== undefined) + return column; + + column = 0; + while (tokenIndex > 0) { + tokenIndex -= 1; + column += rowTokens[tokenIndex].value.length; + } + + return column; + }; + +}).call(TokenIterator.prototype); + +exports.TokenIterator = TokenIterator; +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/edit_session/bracket_match', ['require', 'exports', 'module' , 'ace/token_iterator'], function(require, exports, module) { +"use strict"; + +var TokenIterator = require("../token_iterator").TokenIterator; + +/** + * class BracketMatch + * + * + * + * + **/ + +/** + * new BracketMatch(position) + * - platform (String): Identifier for the platform; must be either `'mac'` or `'win'` + * - commands (Array): A list of commands + * + * TODO + * + * + **/ +function BracketMatch() { + + /** + * new findMatchingBracket(position) + * - position (Number): Identifier for the platform; must be either `'mac'` or `'win'` + * - commands (Array): A list of commands + * + * TODO + * + * + **/ + this.findMatchingBracket = function(position) { + if (position.column == 0) return null; + + var charBeforeCursor = this.getLine(position.row).charAt(position.column-1); + if (charBeforeCursor == "") return null; + + var match = charBeforeCursor.match(/([\(\[\{])|([\)\]\}])/); + if (!match) { + return null; + } + + if (match[1]) { + return this.$findClosingBracket(match[1], position); + } else { + return this.$findOpeningBracket(match[2], position); + } + }; + + this.$brackets = { + ")": "(", + "(": ")", + "]": "[", + "[": "]", + "{": "}", + "}": "{" + }; + + this.$findOpeningBracket = function(bracket, position) { + var openBracket = this.$brackets[bracket]; + var depth = 1; + + var iterator = new TokenIterator(this, position.row, position.column); + var token = iterator.getCurrentToken(); + if (!token) return null; + + // token.type contains a period-delimited list of token identifiers + // (e.g.: "constant.numeric" or "paren.lparen"). Create a pattern that + // matches any token containing the same identifiers or a subset. In + // addition, if token.type includes "rparen", then also match "lparen". + // So if type.token is "paren.rparen", then typeRe will match "lparen.paren". + var typeRe = new RegExp("(\\.?" + + token.type.replace(".", "|").replace("rparen", "lparen|rparen") + ")+"); + + // Start searching in token, just before the character at position.column + var valueIndex = position.column - iterator.getCurrentTokenColumn() - 2; + var value = token.value; + + while (true) { + + while (valueIndex >= 0) { + var chr = value.charAt(valueIndex); + if (chr == openBracket) { + depth -= 1; + if (depth == 0) { + return {row: iterator.getCurrentTokenRow(), + column: valueIndex + iterator.getCurrentTokenColumn()}; + } + } + else if (chr == bracket) { + depth += 1; + } + valueIndex -= 1; + } + + // Scan backward through the document, looking for the next token + // whose type matches typeRe + do { + token = iterator.stepBackward(); + } while (token && !typeRe.test(token.type)); + + if (token == null) + break; + + value = token.value; + valueIndex = value.length - 1; + } + + return null; + }; + + this.$findClosingBracket = function(bracket, position) { + var closingBracket = this.$brackets[bracket]; + var depth = 1; + + var iterator = new TokenIterator(this, position.row, position.column); + var token = iterator.getCurrentToken(); + if (!token) return null; + + // token.type contains a period-delimited list of token identifiers + // (e.g.: "constant.numeric" or "paren.lparen"). Create a pattern that + // matches any token containing the same identifiers or a subset. In + // addition, if token.type includes "lparen", then also match "rparen". + // So if type.token is "lparen.paren", then typeRe will match "paren.rparen". + var typeRe = new RegExp("(\\.?" + + token.type.replace(".", "|").replace("lparen", "lparen|rparen") + ")+"); + + // Start searching in token, after the character at position.column + var valueIndex = position.column - iterator.getCurrentTokenColumn(); + + while (true) { + + var value = token.value; + var valueLength = value.length; + while (valueIndex < valueLength) { + var chr = value.charAt(valueIndex); + if (chr == closingBracket) { + depth -= 1; + if (depth == 0) { + return {row: iterator.getCurrentTokenRow(), + column: valueIndex + iterator.getCurrentTokenColumn()}; + } + } + else if (chr == bracket) { + depth += 1; + } + valueIndex += 1; + } + + // Scan forward through the document, looking for the next token + // whose type matches typeRe + do { + token = iterator.stepForward(); + } while (token && !typeRe.test(token.type)); + + if (token == null) + break; + + valueIndex = 0; + } + + return null; + }; +} +exports.BracketMatch = BracketMatch; + +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * Mihai Sucan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/search', ['require', 'exports', 'module' , 'ace/lib/lang', 'ace/lib/oop', 'ace/range'], function(require, exports, module) { +"use strict"; + +var lang = require("./lib/lang"); +var oop = require("./lib/oop"); +var Range = require("./range").Range; + +/** + * class Search + * + * A class designed to handle all sorts of text searches within a [[Document `Document`]]. + * + **/ + +/** + * new Search() + * + * Creates a new `Search` object. The search options contain the following defaults: + * + * * `needle`: `""` + * * `backwards`: `false` + * * `wrap`: `false` + * * `caseSensitive`: `false` + * * `wholeWord`: `false` + * * `scope`: `ALL` + * * `regExp`: `false` + * +**/ + +var Search = function() { + this.$options = { + needle: "", + backwards: false, + wrap: false, + caseSensitive: false, + wholeWord: false, + scope: Search.ALL, + regExp: false + }; +}; + +Search.ALL = 1; +Search.SELECTION = 2; + +(function() { + + /** + * Search.set(options) -> Search + * - options (Object): An object containing all the new search properties + * + * Sets the search options via the `options` parameter. + * + **/ + this.set = function(options) { + oop.mixin(this.$options, options); + return this; + }; + + /** + * Search.getOptions() -> Object + * + * [Returns an object containing all the search options.]{: #Search.getOptions} + * + **/ + this.getOptions = function() { + return lang.copyObject(this.$options); + }; + + /** + * Search.find(session) -> Range + * - session (EditSession): The session to search with + * + * Searches for `options.needle`. If found, this method returns the [[Range `Range`]] where the text first occurs. If `options.backwards` is `true`, the search goes backwards in the session. + * + **/ + this.find = function(session) { + if (!this.$options.needle) + return null; + + if (this.$options.backwards) { + var iterator = this.$backwardMatchIterator(session); + } else { + iterator = this.$forwardMatchIterator(session); + } + + var firstRange = null; + iterator.forEach(function(range) { + firstRange = range; + return true; + }); + + return firstRange; + }; + + /** + * Search.findAll(session) -> [Range] + * - session (EditSession): The session to search with + * + * Searches for all occurances `options.needle`. If found, this method returns an array of [[Range `Range`s]] where the text first occurs. If `options.backwards` is `true`, the search goes backwards in the session. + * + **/ + this.findAll = function(session) { + var options = this.$options; + if (!options.needle) + return []; + + if (options.backwards) { + var iterator = this.$backwardMatchIterator(session); + } else { + iterator = this.$forwardMatchIterator(session); + } + + var ignoreCursor = !options.start && options.wrap && options.scope == Search.ALL; + if (ignoreCursor) + options.start = {row: 0, column: 0}; + + var ranges = []; + iterator.forEach(function(range) { + ranges.push(range); + }); + + if (ignoreCursor) + options.start = null; + + return ranges; + }; + + /** + * Search.replace(input, replacement) -> String + * - input (String): The text to search in + * - replacement (String): The replacing text + * + (String): If `options.regExp` is `true`, this function returns `input` with the replacement already made. Otherwise, this function just returns `replacement`.
+ * If `options.needle` was not found, this function returns `null`. + * + * Searches for `options.needle` in `input`, and, if found, replaces it with `replacement`. + * + * + * + **/ + this.replace = function(input, replacement) { + var re = this.$assembleRegExp(); + var match = re.exec(input); + if (match && match[0].length == input.length) { + if (this.$options.regExp) { + return input.replace(re, replacement); + } else { + return replacement; + } + } else { + return null; + } + }; + + /** internal, hide + * Search.$forwardMatchIterator(session) -> String | Boolean + * - session (EditSession): The session to search with + * + * + * + **/ + this.$forwardMatchIterator = function(session) { + var re = this.$assembleRegExp(); + var self = this; + + return { + forEach: function(callback) { + self.$forwardLineIterator(session).forEach(function(line, startIndex, row) { + if (startIndex) { + line = line.substring(startIndex); + } + + var matches = []; + + line.replace(re, function(str) { + var offset = arguments[arguments.length-2]; + matches.push({ + str: str, + offset: startIndex + offset + }); + return str; + }); + + for (var i=0; i String + * - session (EditSession): The session to search with + * + * + * + **/ + this.$backwardMatchIterator = function(session) { + var re = this.$assembleRegExp(); + var self = this; + + return { + forEach: function(callback) { + self.$backwardLineIterator(session).forEach(function(line, startIndex, row) { + if (startIndex) { + line = line.substring(startIndex); + } + + var matches = []; + + line.replace(re, function(str, offset) { + matches.push({ + str: str, + offset: startIndex + offset + }); + return str; + }); + + for (var i=matches.length-1; i>= 0; i--) { + var match = matches[i]; + var range = self.$rangeFromMatch(row, match.offset, match.str.length); + if (callback(range)) + return true; + } + }); + } + }; + }; + + this.$rangeFromMatch = function(row, column, length) { + return new Range(row, column, row, column+length); + }; + + this.$assembleRegExp = function() { + if (this.$options.regExp) { + var needle = this.$options.needle; + } else { + needle = lang.escapeRegExp(this.$options.needle); + } + + if (this.$options.wholeWord) { + needle = "\\b" + needle + "\\b"; + } + + var modifier = "g"; + if (!this.$options.caseSensitive) { + modifier += "i"; + } + + var re = new RegExp(needle, modifier); + return re; + }; + + this.$forwardLineIterator = function(session) { + var searchSelection = this.$options.scope == Search.SELECTION; + + var range = this.$options.range || session.getSelection().getRange(); + var start = this.$options.start || range[searchSelection ? "start" : "end"]; + + var firstRow = searchSelection ? range.start.row : 0; + var firstColumn = searchSelection ? range.start.column : 0; + var lastRow = searchSelection ? range.end.row : session.getLength() - 1; + + var wrap = this.$options.wrap; + var inWrap = false; + + function getLine(row) { + var line = session.getLine(row); + if (searchSelection && row == range.end.row) { + line = line.substring(0, range.end.column); + } + if (inWrap && row == start.row) { + line = line.substring(0, start.column); + } + return line; + } + + return { + forEach: function(callback) { + var row = start.row; + + var line = getLine(row); + var startIndex = start.column; + + var stop = false; + inWrap = false; + + while (!callback(line, startIndex, row)) { + + if (stop) { + return; + } + + row++; + startIndex = 0; + + if (row > lastRow) { + if (wrap) { + row = firstRow; + startIndex = firstColumn; + inWrap = true; + } else { + return; + } + } + + if (row == start.row) + stop = true; + + line = getLine(row); + } + } + }; + }; + + this.$backwardLineIterator = function(session) { + var searchSelection = this.$options.scope == Search.SELECTION; + + var range = this.$options.range || session.getSelection().getRange(); + var start = this.$options.start || range[searchSelection ? "end" : "start"]; + + var firstRow = searchSelection ? range.start.row : 0; + var firstColumn = searchSelection ? range.start.column : 0; + var lastRow = searchSelection ? range.end.row : session.getLength() - 1; + + var wrap = this.$options.wrap; + + return { + forEach : function(callback) { + var row = start.row; + + var line = session.getLine(row).substring(0, start.column); + var startIndex = 0; + var stop = false; + var inWrap = false; + + while (!callback(line, startIndex, row)) { + + if (stop) + return; + + row--; + startIndex = 0; + + if (row < firstRow) { + if (wrap) { + row = lastRow; + inWrap = true; + } else { + return; + } + } + + if (row == start.row) + stop = true; + + line = session.getLine(row); + if (searchSelection) { + if (row == firstRow) + startIndex = firstColumn; + else if (row == lastRow) + line = line.substring(0, range.end.column); + } + + if (inWrap && row == start.row) + startIndex = start.column; + } + } + }; + }; + +}).call(Search.prototype); + +exports.Search = Search; +}); +ace.define('ace/commands/command_manager', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/keyboard/hash_handler', 'ace/lib/event_emitter'], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var HashHandler = require("../keyboard/hash_handler").HashHandler; +var EventEmitter = require("../lib/event_emitter").EventEmitter; + +/** + * class CommandManager + * + * + * + * + **/ + +/** + * new CommandManager(platform, commands) + * - platform (String): Identifier for the platform; must be either `'mac'` or `'win'` + * - commands (Array): A list of commands + * + * TODO + * + * + **/ + +var CommandManager = function(platform, commands) { + this.platform = platform; + this.commands = {}; + this.commmandKeyBinding = {}; + + this.addCommands(commands); + + this.setDefaultHandler("exec", function(e) { + return e.command.exec(e.editor, e.args || {}); + }); +}; + +oop.inherits(CommandManager, HashHandler); + +(function() { + + oop.implement(this, EventEmitter); + + this.exec = function(command, editor, args) { + if (typeof command === 'string') + command = this.commands[command]; + + if (!command) + return false; + + if (editor && editor.$readOnly && !command.readOnly) + return false; + + try { + var retvalue = this._emit("exec", { + editor: editor, + command: command, + args: args + }); + } catch (e) { + window.console && window.console.log(e); + return true; + } + + return retvalue === false ? false : true; + }; + + this.toggleRecording = function() { + if (this.$inReplay) + return; + if (this.recording) { + this.macro.pop(); + this.removeEventListener("exec", this.$addCommandToMacro); + + if (!this.macro.length) + this.macro = this.oldMacro; + + return this.recording = false; + } + if (!this.$addCommandToMacro) { + this.$addCommandToMacro = function(e) { + this.macro.push([e.command, e.args]); + }.bind(this); + } + + this.oldMacro = this.macro; + this.macro = []; + this.on("exec", this.$addCommandToMacro); + return this.recording = true; + }; + + this.replay = function(editor) { + if (this.$inReplay || !this.macro) + return; + + if (this.recording) + return this.toggleRecording(); + + try { + this.$inReplay = true; + this.macro.forEach(function(x) { + if (typeof x == "string") + this.exec(x, editor); + else + this.exec(x[0], editor, x[1]); + }, this); + } finally { + this.$inReplay = false; + } + }; + + this.trimMacro = function(m) { + return m.map(function(x){ + if (typeof x[0] != "string") + x[0] = x[0].name; + if (!x[1]) + x = x[0]; + return x; + }); + }; + +}).call(CommandManager.prototype); + +exports.CommandManager = CommandManager; + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Skywriter. + * + * The Initial Developer of the Original Code is + * Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * Julian Viereck (julian.viereck@gmail.com) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/keyboard/hash_handler', ['require', 'exports', 'module' , 'ace/lib/keys'], function(require, exports, module) { +"use strict"; + +var keyUtil = require("../lib/keys"); + +function HashHandler(config, platform) { + this.platform = platform; + this.commands = {}; + this.commmandKeyBinding = {}; + + this.addCommands(config); +}; + +(function() { + + this.addCommand = function(command) { + if (this.commands[command.name]) + this.removeCommand(command); + + this.commands[command.name] = command; + + if (command.bindKey) { + this._buildKeyHash(command); + } + }; + + this.removeCommand = function(command) { + var name = (typeof command === 'string' ? command : command.name); + command = this.commands[name]; + delete this.commands[name]; + + // exhaustive search is brute force but since removeCommand is + // not a performance critical operation this should be OK + var ckb = this.commmandKeyBinding; + for (var hashId in ckb) { + for (var key in ckb[hashId]) { + if (ckb[hashId][key] == command) + delete ckb[hashId][key]; + } + } + }; + + this.bindKey = function(key, command) { + if(!key) + return; + + var ckb = this.commmandKeyBinding; + key.split("|").forEach(function(keyPart) { + var binding = this.parseKeys(keyPart, command); + var hashId = binding.hashId; + (ckb[hashId] || (ckb[hashId] = {}))[binding.key] = command; + }, this); + }; + + this.addCommands = function(commands) { + commands && Object.keys(commands).forEach(function(name) { + var command = commands[name]; + if (typeof command === "string") + return this.bindKey(command, name); + + if (typeof command === "function") + command = { exec: command }; + + if (!command.name) + command.name = name; + + this.addCommand(command); + }, this); + }; + + this.removeCommands = function(commands) { + Object.keys(commands).forEach(function(name) { + this.removeCommand(commands[name]); + }, this); + }; + + this.bindKeys = function(keyList) { + Object.keys(keyList).forEach(function(key) { + this.bindKey(key, keyList[key]); + }, this); + }; + + this._buildKeyHash = function(command) { + var binding = command.bindKey; + if (!binding) + return; + + var key = typeof binding == "string" ? binding: binding[this.platform]; + this.bindKey(key, command); + }; + + this.parseKeys = function(keys, val) { + var key; + var hashId = 0; + var parts = keys.toLowerCase().trim().split(/\s*\-\s*/); + + for (var i = 0, l = parts.length; i < l; i++) { + if (keyUtil.KEY_MODS[parts[i]]) + hashId = hashId | keyUtil.KEY_MODS[parts[i]]; + else + key = parts[i] || "-"; //when empty, the splitSafe removed a '-' + } + + return { + key: key, + hashId: hashId + }; + }; + + this.findKeyCommand = function findKeyCommand(hashId, keyString) { + var ckbr = this.commmandKeyBinding; + return ckbr[hashId] && ckbr[hashId][keyString.toLowerCase()]; + }; + + this.handleKeyboard = function(data, hashId, keyString, keyCode) { + return { + command: this.findKeyCommand(hashId, keyString) + }; + }; + +}).call(HashHandler.prototype) + +exports.HashHandler = HashHandler; +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * Mihai Sucan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/undomanager', ['require', 'exports', 'module' ], function(require, exports, module) { +"use strict"; + +/** + * class UndoManager + * + * This object maintains the undo stack for an [[EditSession `EditSession`]]. + * + **/ + +/** + * new UndoManager() + * + * Resets the current undo state and creates a new `UndoManager`. + **/ +var UndoManager = function() { + this.reset(); +}; + +(function() { + + /** + * UndoManager.execute(options) -> Void + * - options (Object): Contains additional properties + * + * Provides a means for implementing your own undo manager. `options` has one property, `args`, an [[Array `Array`]], with two elements: + * * `args[0]` is an array of deltas + * * `args[1]` is the document to associate with + * + **/ + this.execute = function(options) { + var deltas = options.args[0]; + this.$doc = options.args[1]; + this.$undoStack.push(deltas); + this.$redoStack = []; + }; + + /** + * UndoManager.undo(dontSelect) -> Range + * - dontSelect (Boolean): {:dontSelect} + * + * [Perform an undo operation on the document, reverting the last change. Returns the range of the undo.]{: #UndoManager.undo} + **/ + this.undo = function(dontSelect) { + var deltas = this.$undoStack.pop(); + var undoSelectionRange = null; + if (deltas) { + undoSelectionRange = + this.$doc.undoChanges(deltas, dontSelect); + this.$redoStack.push(deltas); + } + return undoSelectionRange; + }; + + /** + * UndoManager.redo(dontSelect) -> Void + * - dontSelect (Boolean): {:dontSelect} + * + * [Perform a redo operation on the document, reimplementing the last change.]{: #UndoManager.redo} + **/ + this.redo = function(dontSelect) { + var deltas = this.$redoStack.pop(); + var redoSelectionRange = null; + if (deltas) { + redoSelectionRange = + this.$doc.redoChanges(deltas, dontSelect); + this.$undoStack.push(deltas); + } + return redoSelectionRange; + }; + + /** + * UndoManager.reset() -> Void + * + * Destroys the stack of undo and redo redo operations. + **/ + this.reset = function() { + this.$undoStack = []; + this.$redoStack = []; + }; + + /** + * UndoManager.hasUndo() -> Boolean + * + * Returns `true` if there are undo operations left to perform. + **/ + this.hasUndo = function() { + return this.$undoStack.length > 0; + }; + + /** + * UndoManager.hasRedo() -> Boolean + * + * Returns `true` if there are redo operations left to perform. + **/ + this.hasRedo = function() { + return this.$redoStack.length > 0; + }; + +}).call(UndoManager.prototype); + +exports.UndoManager = UndoManager; +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * Irakli Gozalishvili (http://jeditoolkit.com) + * Julian Viereck + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/virtual_renderer', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/lib/dom', 'ace/lib/event', 'ace/lib/useragent', 'ace/config', 'ace/lib/net', 'ace/layer/gutter', 'ace/layer/marker', 'ace/layer/text', 'ace/layer/cursor', 'ace/scrollbar', 'ace/renderloop', 'ace/lib/event_emitter', 'text!ace/css/editor.css'], function(require, exports, module) { +"use strict"; + +var oop = require("./lib/oop"); +var dom = require("./lib/dom"); +var event = require("./lib/event"); +var useragent = require("./lib/useragent"); +var config = require("./config"); +var net = require("./lib/net"); +var GutterLayer = require("./layer/gutter").Gutter; +var MarkerLayer = require("./layer/marker").Marker; +var TextLayer = require("./layer/text").Text; +var CursorLayer = require("./layer/cursor").Cursor; +var ScrollBar = require("./scrollbar").ScrollBar; +var RenderLoop = require("./renderloop").RenderLoop; +var EventEmitter = require("./lib/event_emitter").EventEmitter; +var editorCss = require("text!./css/editor.css"); + +dom.importCssString(editorCss, "ace_editor"); + +/** + * class VirtualRenderer + * + * The class that is responsible for drawing everything you see on the screen! + * + **/ + +/** + * new VirtualRenderer(container, theme) + * - container (DOMElement): The root element of the editor + * - theme (String): The starting theme + * + * Constructs a new `VirtualRenderer` within the `container` specified, applying the given `theme`. + * + **/ + +var VirtualRenderer = function(container, theme) { + var _self = this; + + this.container = container; + + // TODO: this breaks rendering in Cloud9 with multiple ace instances +// // Imports CSS once per DOM document ('ace_editor' serves as an identifier). +// dom.importCssString(editorCss, "ace_editor", container.ownerDocument); + + // in IE <= 9 the native cursor always shines through + this.$keepTextAreaAtCursor = !useragent.isIE; + + dom.addCssClass(container, "ace_editor"); + + this.setTheme(theme); + + this.$gutter = dom.createElement("div"); + this.$gutter.className = "ace_gutter"; + this.container.appendChild(this.$gutter); + + this.scroller = dom.createElement("div"); + this.scroller.className = "ace_scroller"; + this.container.appendChild(this.scroller); + + this.content = dom.createElement("div"); + this.content.className = "ace_content"; + this.scroller.appendChild(this.content); + + this.setHighlightGutterLine(true); + this.$gutterLayer = new GutterLayer(this.$gutter); + this.$gutterLayer.on("changeGutterWidth", this.onResize.bind(this, true)); + this.setFadeFoldWidgets(true); + + this.$markerBack = new MarkerLayer(this.content); + + var textLayer = this.$textLayer = new TextLayer(this.content); + this.canvas = textLayer.element; + + this.$markerFront = new MarkerLayer(this.content); + + this.characterWidth = textLayer.getCharacterWidth(); + this.lineHeight = textLayer.getLineHeight(); + + this.$cursorLayer = new CursorLayer(this.content); + this.$cursorPadding = 8; + + // Indicates whether the horizontal scrollbar is visible + this.$horizScroll = false; + this.$horizScrollAlwaysVisible = false; + + this.$animatedScroll = false; + + this.scrollBar = new ScrollBar(container); + this.scrollBar.addEventListener("scroll", function(e) { + if (!_self.$inScrollAnimation) + _self.session.setScrollTop(e.data); + }); + + this.scrollTop = 0; + this.scrollLeft = 0; + + event.addListener(this.scroller, "scroll", function() { + var scrollLeft = _self.scroller.scrollLeft; + _self.scrollLeft = scrollLeft; + _self.session.setScrollLeft(scrollLeft); + + _self.scroller.className = scrollLeft == 0 + ? "ace_scroller" + : "ace_scroller horscroll"; + }); + + this.cursorPos = { + row : 0, + column : 0 + }; + + this.$textLayer.addEventListener("changeCharacterSize", function() { + _self.characterWidth = textLayer.getCharacterWidth(); + _self.lineHeight = textLayer.getLineHeight(); + _self.$updatePrintMargin(); + _self.onResize(true); + + _self.$loop.schedule(_self.CHANGE_FULL); + }); + + this.$size = { + width: 0, + height: 0, + scrollerHeight: 0, + scrollerWidth: 0 + }; + + this.layerConfig = { + width : 1, + padding : 0, + firstRow : 0, + firstRowScreen: 0, + lastRow : 0, + lineHeight : 1, + characterWidth : 1, + minHeight : 1, + maxHeight : 1, + offset : 0, + height : 1 + }; + + this.$loop = new RenderLoop( + this.$renderChanges.bind(this), + this.container.ownerDocument.defaultView + ); + this.$loop.schedule(this.CHANGE_FULL); + + this.setPadding(4); + this.$updatePrintMargin(); +}; + +(function() { + this.showGutter = true; + + this.CHANGE_CURSOR = 1; + this.CHANGE_MARKER = 2; + this.CHANGE_GUTTER = 4; + this.CHANGE_SCROLL = 8; + this.CHANGE_LINES = 16; + this.CHANGE_TEXT = 32; + this.CHANGE_SIZE = 64; + this.CHANGE_MARKER_BACK = 128; + this.CHANGE_MARKER_FRONT = 256; + this.CHANGE_FULL = 512; + this.CHANGE_H_SCROLL = 1024; + + oop.implement(this, EventEmitter); + + /** + * VirtualRenderer.setSession(session) -> Void + * + * Associates an [[EditSession `EditSession`]]. + **/ + this.setSession = function(session) { + this.session = session; + + this.scroller.className = "ace_scroller"; + + this.$cursorLayer.setSession(session); + this.$markerBack.setSession(session); + this.$markerFront.setSession(session); + this.$gutterLayer.setSession(session); + this.$textLayer.setSession(session); + this.$loop.schedule(this.CHANGE_FULL); + + }; + + /** + * VirtualRenderer.updateLines(firstRow, lastRow) -> Void + * - firstRow (Number): The first row to update + * - lastRow (Number): The last row to update + * + * Triggers a partial update of the text, from the range given by the two parameters. + **/ + this.updateLines = function(firstRow, lastRow) { + if (lastRow === undefined) + lastRow = Infinity; + + if (!this.$changedLines) { + this.$changedLines = { + firstRow: firstRow, + lastRow: lastRow + }; + } + else { + if (this.$changedLines.firstRow > firstRow) + this.$changedLines.firstRow = firstRow; + + if (this.$changedLines.lastRow < lastRow) + this.$changedLines.lastRow = lastRow; + } + + this.$loop.schedule(this.CHANGE_LINES); + }; + + /** + * VirtualRenderer.updateText() -> Void + * + * Triggers a full update of the text, for all the rows. + **/ + this.updateText = function() { + this.$loop.schedule(this.CHANGE_TEXT); + }; + + /** + * VirtualRenderer.updateFull() -> Void + * + * Triggers a full update of all the layers, for all the rows. + **/ + this.updateFull = function() { + this.$loop.schedule(this.CHANGE_FULL); + }; + + /** + * VirtualRenderer.updateFontSize() -> Void + * + * Updates the font size. + **/ + this.updateFontSize = function() { + this.$textLayer.checkForSizeChanges(); + }; + + /** + * VirtualRenderer.onResize(force) -> Void + * - force (Boolean): If `true`, recomputes the size, even if the height and width haven't changed + * + * [Triggers a resize of the editor.]{: #VirtualRenderer.onResize} + **/ + this.onResize = function(force) { + var changes = this.CHANGE_SIZE; + var size = this.$size; + + var height = dom.getInnerHeight(this.container); + if (force || size.height != height) { + size.height = height; + + this.scroller.style.height = height + "px"; + size.scrollerHeight = this.scroller.clientHeight; + this.scrollBar.setHeight(size.scrollerHeight); + + if (this.session) { + this.session.setScrollTop(this.getScrollTop()); + changes = changes | this.CHANGE_FULL; + } + } + + var width = dom.getInnerWidth(this.container); + if (force || size.width != width) { + size.width = width; + + var gutterWidth = this.showGutter ? this.$gutter.offsetWidth : 0; + this.scroller.style.left = gutterWidth + "px"; + size.scrollerWidth = Math.max(0, width - gutterWidth - this.scrollBar.getWidth()); + this.scroller.style.width = size.scrollerWidth + "px"; + + if (this.session.getUseWrapMode() && this.adjustWrapLimit() || force) + changes = changes | this.CHANGE_FULL; + } + + this.$loop.schedule(changes); + }; + + /** + * VirtualRenderer.adjustWrapLimit() -> Void + * + * Adjusts the wrap limit, which is the number of characters that can fit within the width of the edit area on screen. + **/ + this.adjustWrapLimit = function() { + var availableWidth = this.$size.scrollerWidth - this.$padding * 2; + var limit = Math.floor(availableWidth / this.characterWidth); + return this.session.adjustWrapLimit(limit); + }; + + /** + * VirtualRenderer.setAnimatedScroll(shouldAnimate) -> Void + * - shouldAnimate (Boolean): Set to `true` to show animated scrolls + * + * Identifies whether you want to have an animated scroll or not. + * + **/ + this.setAnimatedScroll = function(shouldAnimate){ + this.$animatedScroll = shouldAnimate; + }; + + /** + * VirtualRenderer.getAnimatedScroll() -> Boolean + * + * Returns whether an animated scroll happens or not. + **/ + this.getAnimatedScroll = function() { + return this.$animatedScroll; + }; + + /** + * VirtualRenderer.setShowInvisibles(showInvisibles) -> Void + * - showInvisibles (Boolean): Set to `true` to show invisibles + * + * Identifies whether you want to show invisible characters or not. + * + **/ + this.setShowInvisibles = function(showInvisibles) { + if (this.$textLayer.setShowInvisibles(showInvisibles)) + this.$loop.schedule(this.CHANGE_TEXT); + }; + + /** + * VirtualRenderer.getShowInvisibles() -> Boolean + * + * Returns whether invisible characters are being shown or not. + **/ + this.getShowInvisibles = function() { + return this.$textLayer.showInvisibles; + }; + + this.$showPrintMargin = true; + + /** + * VirtualRenderer.setShowPrintMargin(showPrintMargin) + * - showPrintMargin (Boolean): Set to `true` to show the print margin + * + * Identifies whether you want to show the print margin or not. + * + **/ + this.setShowPrintMargin = function(showPrintMargin) { + this.$showPrintMargin = showPrintMargin; + this.$updatePrintMargin(); + }; + + /** + * VirtualRenderer.getShowPrintMargin() -> Boolean + * + * Returns whetherthe print margin is being shown or not. + **/ + this.getShowPrintMargin = function() { + return this.$showPrintMargin; + }; + + this.$printMarginColumn = 80; + + /** + * VirtualRenderer.setPrintMarginColumn(showPrintMargin) + * - showPrintMargin (Boolean): Set to `true` to show the print margin column + * + * Identifies whether you want to show the print margin column or not. + * + **/ + this.setPrintMarginColumn = function(showPrintMargin) { + this.$printMarginColumn = showPrintMargin; + this.$updatePrintMargin(); + }; + + /** + * VirtualRenderer.getPrintMarginColumn() -> Boolean + * + * Returns whether the print margin column is being shown or not. + **/ + this.getPrintMarginColumn = function() { + return this.$printMarginColumn; + }; + + /** + * VirtualRenderer.getShowGutter() -> Boolean + * + * Returns `true` if the gutter is being shown. + **/ + this.getShowGutter = function(){ + return this.showGutter; + }; + + /** + * VirtualRenderer.setShowGutter(show) -> Void + * - show (Boolean): Set to `true` to show the gutter + * + * Identifies whether you want to show the gutter or not. + **/ + this.setShowGutter = function(show){ + if(this.showGutter === show) + return; + this.$gutter.style.display = show ? "block" : "none"; + this.showGutter = show; + this.onResize(true); + }; + + this.getFadeFoldWidgets = function(){ + return dom.hasCssClass(this.$gutter, "ace_fade-fold-widgets"); + }; + + this.setFadeFoldWidgets = function(show) { + if (show) + dom.addCssClass(this.$gutter, "ace_fade-fold-widgets"); + else + dom.removeCssClass(this.$gutter, "ace_fade-fold-widgets"); + }; + + this.$highlightGutterLine = false; + this.setHighlightGutterLine = function(shouldHighlight) { + if (this.$highlightGutterLine == shouldHighlight) + return; + this.$highlightGutterLine = shouldHighlight; + + + if (!this.$gutterLineHighlight) { + this.$gutterLineHighlight = dom.createElement("div"); + this.$gutterLineHighlight.className = "ace_gutter_active_line"; + this.$gutter.appendChild(this.$gutterLineHighlight); + return; + } + + this.$gutterLineHighlight.style.display = shouldHighlight ? "" : "none"; + this.$updateGutterLineHighlight(); + }; + + this.getHighlightGutterLine = function() { + return this.$highlightGutterLine; + }; + + this.$updateGutterLineHighlight = function() { + this.$gutterLineHighlight.style.top = this.$cursorLayer.$pixelPos.top + "px"; + this.$gutterLineHighlight.style.height = this.layerConfig.lineHeight + "px"; + }; + + this.$updatePrintMargin = function() { + var containerEl; + + if (!this.$showPrintMargin && !this.$printMarginEl) + return; + + if (!this.$printMarginEl) { + containerEl = dom.createElement("div"); + containerEl.className = "ace_print_margin_layer"; + this.$printMarginEl = dom.createElement("div"); + this.$printMarginEl.className = "ace_print_margin"; + containerEl.appendChild(this.$printMarginEl); + this.content.insertBefore(containerEl, this.$textLayer.element); + } + + var style = this.$printMarginEl.style; + style.left = ((this.characterWidth * this.$printMarginColumn) + this.$padding) + "px"; + style.visibility = this.$showPrintMargin ? "visible" : "hidden"; + }; + + /** + * VirtualRenderer.getContainerElement() -> DOMElement + * + * Returns the root element containing this renderer. + **/ + this.getContainerElement = function() { + return this.container; + }; + + /** + * VirtualRenderer.getMouseEventTarget() -> DOMElement + * + * Returns the element that the mouse events are attached to + **/ + this.getMouseEventTarget = function() { + return this.content; + }; + + /** + * VirtualRenderer.getTextAreaContainer() -> DOMElement + * + * Returns the element to which the hidden text area is added. + **/ + this.getTextAreaContainer = function() { + return this.container; + }; + + // move text input over the cursor + // this is required for iOS and IME + this.$moveTextAreaToCursor = function() { + if (!this.$keepTextAreaAtCursor) + return; + + var posTop = this.$cursorLayer.$pixelPos.top; + var posLeft = this.$cursorLayer.$pixelPos.left; + posTop -= this.layerConfig.offset; + + if (posTop < 0 || posTop > this.layerConfig.height) + return; + + posLeft += (this.showGutter ? this.$gutterLayer.gutterWidth : 0) - this.scrollLeft; + var bounds = this.container.getBoundingClientRect(); + this.textarea.style.left = (bounds.left + posLeft) + "px"; + this.textarea.style.top = (bounds.top + posTop) + "px"; + }; + + /** + * VirtualRenderer.getFirstVisibleRow() -> Number + * + * [Returns the index of the first visible row.]{: #VirtualRenderer.getFirstVisibleRow} + **/ + this.getFirstVisibleRow = function() { + return this.layerConfig.firstRow; + }; + + /** + * VirtualRenderer.getFirstFullyVisibleRow() -> Number + * + * Returns the index of the first fully visible row. "Fully" here means that the characters in the row are not truncated; that the top and the bottom of the row are on the screen. + **/ + this.getFirstFullyVisibleRow = function() { + return this.layerConfig.firstRow + (this.layerConfig.offset === 0 ? 0 : 1); + }; + + /** + * VirtualRenderer.getLastFullyVisibleRow() -> Number + * + * Returns the index of the last fully visible row. "Fully" here means that the characters in the row are not truncated; that the top and the bottom of the row are on the screen. + **/ + this.getLastFullyVisibleRow = function() { + var flint = Math.floor((this.layerConfig.height + this.layerConfig.offset) / this.layerConfig.lineHeight); + return this.layerConfig.firstRow - 1 + flint; + }; + + /** + * VirtualRenderer.getLastVisibleRow() -> Number + * + * [Returns the index of the last visible row.]{: #VirtualRenderer.getLastVisibleRow} + **/ + this.getLastVisibleRow = function() { + return this.layerConfig.lastRow; + }; + + this.$padding = null; + + /** + * VirtualRenderer.setPadding(padding) -> Void + * - padding (Number): A new padding value (in pixels) + * + * Sets the padding for all the layers. + * + **/ + this.setPadding = function(padding) { + this.$padding = padding; + this.$textLayer.setPadding(padding); + this.$cursorLayer.setPadding(padding); + this.$markerFront.setPadding(padding); + this.$markerBack.setPadding(padding); + this.$loop.schedule(this.CHANGE_FULL); + this.$updatePrintMargin(); + }; + + /** + * VirtualRenderer.getHScrollBarAlwaysVisible() -> Boolean + * + * Returns whether the horizontal scrollbar is set to be always visible. + **/ + this.getHScrollBarAlwaysVisible = function() { + return this.$horizScrollAlwaysVisible; + }; + + /** + * VirtualRenderer.setHScrollBarAlwaysVisible(alwaysVisible) -> Void + * - alwaysVisible (Boolean): Set to `true` to make the horizontal scroll bar visible + * + * Identifies whether you want to show the horizontal scrollbar or not. + **/ + this.setHScrollBarAlwaysVisible = function(alwaysVisible) { + if (this.$horizScrollAlwaysVisible != alwaysVisible) { + this.$horizScrollAlwaysVisible = alwaysVisible; + if (!this.$horizScrollAlwaysVisible || !this.$horizScroll) + this.$loop.schedule(this.CHANGE_SCROLL); + } + }; + + this.$updateScrollBar = function() { + this.scrollBar.setInnerHeight(this.layerConfig.maxHeight); + this.scrollBar.setScrollTop(this.scrollTop); + }; + + this.$renderChanges = function(changes) { + if (!changes || !this.session || !this.container.offsetWidth) + return; + + // text, scrolling and resize changes can cause the view port size to change + if (changes & this.CHANGE_FULL || + changes & this.CHANGE_SIZE || + changes & this.CHANGE_TEXT || + changes & this.CHANGE_LINES || + changes & this.CHANGE_SCROLL + ) + this.$computeLayerConfig(); + + // horizontal scrolling + if (changes & this.CHANGE_H_SCROLL) { + this.scroller.scrollLeft = this.scrollLeft; + + // read the value after writing it since the value might get clipped + var scrollLeft = this.scroller.scrollLeft; + this.scrollLeft = scrollLeft; + this.session.setScrollLeft(scrollLeft); + } + + // full + if (changes & this.CHANGE_FULL) { + this.$textLayer.checkForSizeChanges(); + // update scrollbar first to not lose scroll position when gutter calls resize + this.$updateScrollBar(); + this.$textLayer.update(this.layerConfig); + if (this.showGutter) + this.$gutterLayer.update(this.layerConfig); + this.$markerBack.update(this.layerConfig); + this.$markerFront.update(this.layerConfig); + this.$cursorLayer.update(this.layerConfig); + this.$moveTextAreaToCursor(); + this.$highlightGutterLine && this.$updateGutterLineHighlight(); + return; + } + + // scrolling + if (changes & this.CHANGE_SCROLL) { + this.$updateScrollBar(); + if (changes & this.CHANGE_TEXT || changes & this.CHANGE_LINES) + this.$textLayer.update(this.layerConfig); + else + this.$textLayer.scrollLines(this.layerConfig); + + if (this.showGutter) + this.$gutterLayer.update(this.layerConfig); + this.$markerBack.update(this.layerConfig); + this.$markerFront.update(this.layerConfig); + this.$cursorLayer.update(this.layerConfig); + this.$moveTextAreaToCursor(); + this.$highlightGutterLine && this.$updateGutterLineHighlight(); + return; + } + + if (changes & this.CHANGE_TEXT) { + this.$textLayer.update(this.layerConfig); + if (this.showGutter) + this.$gutterLayer.update(this.layerConfig); + } + else if (changes & this.CHANGE_LINES) { + if (this.$updateLines()) { + this.$updateScrollBar(); + if (this.showGutter) + this.$gutterLayer.update(this.layerConfig); + } + } else if (changes & this.CHANGE_GUTTER) { + if (this.showGutter) + this.$gutterLayer.update(this.layerConfig); + } + + if (changes & this.CHANGE_CURSOR) { + this.$cursorLayer.update(this.layerConfig); + this.$moveTextAreaToCursor(); + this.$highlightGutterLine && this.$updateGutterLineHighlight(); + } + + if (changes & (this.CHANGE_MARKER | this.CHANGE_MARKER_FRONT)) { + this.$markerFront.update(this.layerConfig); + } + + if (changes & (this.CHANGE_MARKER | this.CHANGE_MARKER_BACK)) { + this.$markerBack.update(this.layerConfig); + } + + if (changes & this.CHANGE_SIZE) + this.$updateScrollBar(); + }; + + this.$computeLayerConfig = function() { + var session = this.session; + + var offset = this.scrollTop % this.lineHeight; + var minHeight = this.$size.scrollerHeight + this.lineHeight; + + var longestLine = this.$getLongestLine(); + + var horizScroll = this.$horizScrollAlwaysVisible || this.$size.scrollerWidth - longestLine < 0; + var horizScrollChanged = this.$horizScroll !== horizScroll; + this.$horizScroll = horizScroll; + if (horizScrollChanged) { + this.scroller.style.overflowX = horizScroll ? "scroll" : "hidden"; + // when we hide scrollbar scroll event isn't emited + // leaving session with wrong scrollLeft value + if (!horizScroll) + this.session.setScrollLeft(0); + } + var maxHeight = this.session.getScreenLength() * this.lineHeight; + this.session.setScrollTop(Math.max(0, Math.min(this.scrollTop, maxHeight - this.$size.scrollerHeight))); + + var lineCount = Math.ceil(minHeight / this.lineHeight) - 1; + var firstRow = Math.max(0, Math.round((this.scrollTop - offset) / this.lineHeight)); + var lastRow = firstRow + lineCount; + + // Map lines on the screen to lines in the document. + var firstRowScreen, firstRowHeight; + var lineHeight = { lineHeight: this.lineHeight }; + firstRow = session.screenToDocumentRow(firstRow, 0); + + // Check if firstRow is inside of a foldLine. If true, then use the first + // row of the foldLine. + var foldLine = session.getFoldLine(firstRow); + if (foldLine) { + firstRow = foldLine.start.row; + } + + firstRowScreen = session.documentToScreenRow(firstRow, 0); + firstRowHeight = session.getRowHeight(lineHeight, firstRow); + + lastRow = Math.min(session.screenToDocumentRow(lastRow, 0), session.getLength() - 1); + minHeight = this.$size.scrollerHeight + session.getRowHeight(lineHeight, lastRow)+ + firstRowHeight; + + offset = this.scrollTop - firstRowScreen * this.lineHeight; + + this.layerConfig = { + width : longestLine, + padding : this.$padding, + firstRow : firstRow, + firstRowScreen: firstRowScreen, + lastRow : lastRow, + lineHeight : this.lineHeight, + characterWidth : this.characterWidth, + minHeight : minHeight, + maxHeight : maxHeight, + offset : offset, + height : this.$size.scrollerHeight + }; + + // For debugging. + // console.log(JSON.stringify(this.layerConfig)); + + this.$gutter.style.marginTop = (-offset) + "px"; + this.content.style.marginTop = (-offset) + "px"; + this.content.style.width = longestLine + 2 * this.$padding + "px"; + this.content.style.height = minHeight + "px"; + + // Horizontal scrollbar visibility may have changed, which changes + // the client height of the scroller + if (horizScrollChanged) + this.onResize(true); + }; + + this.$updateLines = function() { + var firstRow = this.$changedLines.firstRow; + var lastRow = this.$changedLines.lastRow; + this.$changedLines = null; + + var layerConfig = this.layerConfig; + + // if the update changes the width of the document do a full redraw + if (layerConfig.width != this.$getLongestLine()) + return this.$textLayer.update(layerConfig); + + if (firstRow > layerConfig.lastRow + 1) { return; } + if (lastRow < layerConfig.firstRow) { return; } + + // if the last row is unknown -> redraw everything + if (lastRow === Infinity) { + if (this.showGutter) + this.$gutterLayer.update(layerConfig); + this.$textLayer.update(layerConfig); + return; + } + + // else update only the changed rows + this.$textLayer.updateLines(layerConfig, firstRow, lastRow); + return true; + }; + + this.$getLongestLine = function() { + var charCount = this.session.getScreenWidth(); + if (this.$textLayer.showInvisibles) + charCount += 1; + + return Math.max(this.$size.scrollerWidth - 2 * this.$padding, Math.round(charCount * this.characterWidth)); + }; + + /** + * VirtualRenderer.updateFrontMarkers() -> Void + * + * Schedules an update to all the front markers in the document. + **/ + this.updateFrontMarkers = function() { + this.$markerFront.setMarkers(this.session.getMarkers(true)); + this.$loop.schedule(this.CHANGE_MARKER_FRONT); + }; + + /** + * VirtualRenderer.updateBackMarkers() -> Void + * + * Schedules an update to all the back markers in the document. + **/ + this.updateBackMarkers = function() { + this.$markerBack.setMarkers(this.session.getMarkers()); + this.$loop.schedule(this.CHANGE_MARKER_BACK); + }; + + /** + * VirtualRenderer.addGutterDecoration(row, className) -> Void + * - row (Number): The row number + * - className (String): The class to add + * + * Adds `className` to the `row`, to be used for CSS stylings and whatnot. + **/ + this.addGutterDecoration = function(row, className){ + this.$gutterLayer.addGutterDecoration(row, className); + this.$loop.schedule(this.CHANGE_GUTTER); + }; + + /** + * VirtualRenderer.removeGutterDecoration(row, className)-> Void + * - row (Number): The row number + * - className (String): The class to add + * + * Removes `className` from the `row`. + **/ + this.removeGutterDecoration = function(row, className){ + this.$gutterLayer.removeGutterDecoration(row, className); + this.$loop.schedule(this.CHANGE_GUTTER); + }; + + /** + * VirtualRenderer.setBreakpoints(rows) -> Void + * - rows (Array): An array containg row numbers + * + * Sets a breakpoint for every row number indicated on `rows`. + **/ + this.setBreakpoints = function(rows) { + this.$gutterLayer.setBreakpoints(rows); + this.$loop.schedule(this.CHANGE_GUTTER); + }; + + /** + * VirtualRenderer.setAnnotations(annotations) -> Void + * - annotations (Array): An array containing annotations + * + * Sets annotations for the gutter. + **/ + this.setAnnotations = function(annotations) { + this.$gutterLayer.setAnnotations(annotations); + this.$loop.schedule(this.CHANGE_GUTTER); + }; + + /** + * VirtualRenderer.updateCursor() -> Void + * + * Updates the cursor icon. + **/ + this.updateCursor = function() { + this.$loop.schedule(this.CHANGE_CURSOR); + }; + + /** + * VirtualRenderer.hideCursor() -> Void + * + * Hides the cursor icon. + **/ + this.hideCursor = function() { + this.$cursorLayer.hideCursor(); + }; + + /** + * VirtualRenderer.showCursor() -> Void + * + * Shows the cursor icon. + **/ + this.showCursor = function() { + this.$cursorLayer.showCursor(); + }; + + this.scrollSelectionIntoView = function(anchor, lead, offset) { + // first scroll anchor into view then scroll lead into view + this.scrollCursorIntoView(anchor, offset); + this.scrollCursorIntoView(lead, offset); + }; + + /** + * VirtualRenderer.scrollCursorIntoView(cursor, offset) -> Void + * + * Scrolls the cursor into the first visibile area of the editor + **/ + this.scrollCursorIntoView = function(cursor, offset) { + // the editor is not visible + if (this.$size.scrollerHeight === 0) + return; + + var pos = this.$cursorLayer.getPixelPosition(cursor); + + var left = pos.left; + var top = pos.top; + + if (this.scrollTop > top) { + if (offset) + top -= offset * this.$size.scrollerHeight; + this.session.setScrollTop(top); + } else if (this.scrollTop + this.$size.scrollerHeight < top + this.lineHeight) { + if (offset) + top += offset * this.$size.scrollerHeight; + this.session.setScrollTop(top + this.lineHeight - this.$size.scrollerHeight); + } + + var scrollLeft = this.scrollLeft; + + if (scrollLeft > left) { + if (left < this.$padding + 2 * this.layerConfig.characterWidth) + left = 0; + this.session.setScrollLeft(left); + } else if (scrollLeft + this.$size.scrollerWidth < left + this.characterWidth) { + this.session.setScrollLeft(Math.round(left + this.characterWidth - this.$size.scrollerWidth)); + } + }; + + /** related to: EditSession.getScrollTop + * VirtualRenderer.getScrollTop() -> Number + * + * {:EditSession.getScrollTop} + **/ + this.getScrollTop = function() { + return this.session.getScrollTop(); + }; + + /** related to: EditSession.getScrollLeft + * VirtualRenderer.getScrollLeft() -> Number + * + * {:EditSession.getScrollLeft} + **/ + this.getScrollLeft = function() { + return this.session.getScrollLeft(); + }; + + /** + * VirtualRenderer.getScrollTopRow() -> Number + * + * Returns the first visible row, regardless of whether it's fully visible or not. + **/ + this.getScrollTopRow = function() { + return this.scrollTop / this.lineHeight; + }; + + /** + * VirtualRenderer.getScrollBottomRow() -> Number + * + * Returns the last visible row, regardless of whether it's fully visible or not. + **/ + this.getScrollBottomRow = function() { + return Math.max(0, Math.floor((this.scrollTop + this.$size.scrollerHeight) / this.lineHeight) - 1); + }; + + /** related to: EditSession.setScrollTop + * VirtualRenderer.scrollToRow(row) -> Void + * - row (Number): A row id + * + * Gracefully scrolls the top of the editor to the row indicated. + **/ + this.scrollToRow = function(row) { + this.session.setScrollTop(row * this.lineHeight); + }; + + this.STEPS = 8; + this.$calcSteps = function(fromValue, toValue){ + var i = 0; + var l = this.STEPS; + var steps = []; + + var func = function(t, x_min, dx) { + return dx * (Math.pow(t - 1, 3) + 1) + x_min; + }; + + for (i = 0; i < l; ++i) + steps.push(func(i / this.STEPS, fromValue, toValue - fromValue)); + + return steps; + }; + + /** + * VirtualRenderer.scrollToLine(line, center, animate, callback) -> Void + * - line (Number): A line number + * - center (Boolean): If `true`, centers the editor the to indicated line + * - animate (Boolean): If `true` animates scrolling + * - callback (Function): Function to be called after the animation has finished + * + * Gracefully scrolls the editor to the row indicated. + **/ + this.scrollToLine = function(line, center, animate, callback) { + var pos = this.$cursorLayer.getPixelPosition({row: line, column: 0}); + var offset = pos.top; + if (center) + offset -= this.$size.scrollerHeight / 2; + + var initialScroll = this.scrollTop; + this.session.setScrollTop(offset); + if (animate !== false) + this.animateScrolling(initialScroll, callback); + }; + + this.animateScrolling = function(fromValue, callback) { + var toValue = this.scrollTop; + if (this.$animatedScroll && Math.abs(fromValue - toValue) < 100000) { + var _self = this; + var steps = _self.$calcSteps(fromValue, toValue); + this.$inScrollAnimation = true; + + clearInterval(this.$timer); + + _self.session.setScrollTop(steps.shift()); + this.$timer = setInterval(function() { + if (steps.length) { + _self.session.setScrollTop(steps.shift()); + // trick session to think it's already scrolled to not loose toValue + _self.session.$scrollTop = toValue; + } else { + this.$inScrollAnimation = false; + clearInterval(_self.$timer); + + _self.session.$scrollTop = -1; + _self.session.setScrollTop(toValue); + callback && callback(); + } + }, 10); + } + }; + + /** + * VirtualRenderer.scrollToY(scrollTop) -> Number + * - scrollTop (Number): The position to scroll to + * + * Scrolls the editor to the y pixel indicated. + * + **/ + this.scrollToY = function(scrollTop) { + // after calling scrollBar.setScrollTop + // scrollbar sends us event with same scrollTop. ignore it + if (this.scrollTop !== scrollTop) { + this.$loop.schedule(this.CHANGE_SCROLL); + this.scrollTop = scrollTop; + } + }; + + /** + * VirtualRenderer.scrollToX(scrollLeft) -> Number + * - scrollLeft (Number): The position to scroll to + * + * Scrolls the editor to the x pixel indicated. + * + **/ + this.scrollToX = function(scrollLeft) { + if (scrollLeft <= this.$padding) + scrollLeft = 0; + + if (this.scrollLeft !== scrollLeft) + this.scrollLeft = scrollLeft; + this.$loop.schedule(this.CHANGE_H_SCROLL); + }; + + /** + * VirtualRenderer.scrollBy(deltaX, deltaY) -> Void + * - deltaX (Number): The x value to scroll by + * - deltaY (Number): The y value to scroll by + * + * Scrolls the editor across both x- and y-axes. + **/ + this.scrollBy = function(deltaX, deltaY) { + deltaY && this.session.setScrollTop(this.session.getScrollTop() + deltaY); + deltaX && this.session.setScrollLeft(this.session.getScrollLeft() + deltaX); + }; + + /** + * VirtualRenderer.isScrollableBy(deltaX, deltaY) -> Boolean + * - deltaX (Number): The x value to scroll by + * - deltaY (Number): The y value to scroll by + * + * Returns `true` if you can still scroll by either parameter; in other words, you haven't reached the end of the file or line. + **/ + this.isScrollableBy = function(deltaX, deltaY) { + if (deltaY < 0 && this.session.getScrollTop() > 0) + return true; + if (deltaY > 0 && this.session.getScrollTop() + this.$size.scrollerHeight < this.layerConfig.maxHeight) + return true; + // todo: handle horizontal scrolling + }; + + this.pixelToScreenCoordinates = function(x, y) { + var canvasPos = this.scroller.getBoundingClientRect(); + + var offset = (x + this.scrollLeft - canvasPos.left - this.$padding) / this.characterWidth; + var row = Math.floor((y + this.scrollTop - canvasPos.top) / this.lineHeight); + var col = Math.round(offset); + + return {row: row, column: col, side: offset - col > 0 ? 1 : -1}; + }; + + this.screenToTextCoordinates = function(x, y) { + var canvasPos = this.scroller.getBoundingClientRect(); + + var col = Math.round( + (x + this.scrollLeft - canvasPos.left - this.$padding) / this.characterWidth + ); + var row = Math.floor( + (y + this.scrollTop - canvasPos.top) / this.lineHeight + ); + + return this.session.screenToDocumentPosition(row, Math.max(col, 0)); + }; + + /** + * VirtualRenderer.textToScreenCoordinates(row, column) -> Object + * - row (Number): The document row position + * - column (Number): The document column position + * + * Returns an object containing the `pageX` and `pageY` coordinates of the document position. + * + * + **/ + this.textToScreenCoordinates = function(row, column) { + var canvasPos = this.scroller.getBoundingClientRect(); + var pos = this.session.documentToScreenPosition(row, column); + + var x = this.$padding + Math.round(pos.column * this.characterWidth); + var y = pos.row * this.lineHeight; + + return { + pageX: canvasPos.left + x - this.scrollLeft, + pageY: canvasPos.top + y - this.scrollTop + }; + }; + + /** + * VirtualRenderer.visualizeFocus() -> Void + * + * Focuses the current container. + **/ + this.visualizeFocus = function() { + dom.addCssClass(this.container, "ace_focus"); + }; + + /** + * VirtualRenderer.visualizeBlur() -> Void + * + * Blurs the current container. + **/ + this.visualizeBlur = function() { + dom.removeCssClass(this.container, "ace_focus"); + }; + + /** internal, hide + * VirtualRenderer.showComposition(position) -> Void + * - position (Number): + * + **/ + this.showComposition = function(position) { + if (!this.$composition) { + this.$composition = dom.createElement("div"); + this.$composition.className = "ace_composition"; + this.content.appendChild(this.$composition); + } + + this.$composition.innerHTML = " "; + + var pos = this.$cursorLayer.getPixelPosition(); + var style = this.$composition.style; + style.top = pos.top + "px"; + style.left = (pos.left + this.$padding) + "px"; + style.height = this.lineHeight + "px"; + + this.hideCursor(); + }; + + /** + * VirtualRenderer.setCompositionText(text) -> Void + * - text (String): A string of text to use + * + * Sets the inner text of the current composition to `text`. + **/ + this.setCompositionText = function(text) { + dom.setInnerText(this.$composition, text); + }; + + /** + * VirtualRenderer.hideComposition() -> Void + * + * Hides the current composition. + **/ + this.hideComposition = function() { + this.showCursor(); + + if (!this.$composition) + return; + + var style = this.$composition.style; + style.top = "-10000px"; + style.left = "-10000px"; + }; + + this._loadTheme = function(name, callback) { + if (!config.get("packaged")) + return callback(); + + var base = name.split("/").pop(); + var filename = config.get("themePath") + "/theme-" + base + config.get("suffix"); + net.loadScript(filename, callback); + }; + + /** + * VirtualRenderer.setTheme(theme) -> Void + * - theme (String): The path to a theme + * + * [Sets a new theme for the editor. `theme` should exist, and be a directory path, like `ace/theme/textmate`.]{: #VirtualRenderer.setTheme} + **/ + this.setTheme = function(theme) { + var _self = this; + + this.$themeValue = theme; + if (!theme || typeof theme == "string") { + var moduleName = theme || "ace/theme/textmate"; + + var module; + try { + module = require(moduleName); + } catch (e) {}; + if (module) + return afterLoad(module); + + _self._loadTheme(moduleName, function() { + require([moduleName], function(module) { + if (_self.$themeValue !== theme) + return; + + afterLoad(module); + }); + }); + } else { + afterLoad(theme); + } + + function afterLoad(theme) { + dom.importCssString( + theme.cssText, + theme.cssClass, + _self.container.ownerDocument + ); + + if (_self.$theme) + dom.removeCssClass(_self.container, _self.$theme); + + _self.$theme = theme ? theme.cssClass : null; + + if (_self.$theme) + dom.addCssClass(_self.container, _self.$theme); + + if (theme && theme.isDark) + dom.addCssClass(_self.container, "ace_dark"); + else + dom.removeCssClass(_self.container, "ace_dark"); + + // force re-measure of the gutter width + if (_self.$size) { + _self.$size.width = 0; + _self.onResize(); + } + } + }; + + /** + * VirtualRenderer.getTheme() -> String + * + * [Returns the path of the current theme.]{: #VirtualRenderer.getTheme} + **/ + this.getTheme = function() { + return this.$themeValue; + }; + + // Methods allows to add / remove CSS classnames to the editor element. + // This feature can be used by plug-ins to provide a visual indication of + // a certain mode that editor is in. + + /** + * VirtualRenderer.setStyle(style) -> Void + * - style (String): A class name + * + * [Adds a new class, `style`, to the editor.]{: #VirtualRenderer.setStyle} + **/ + this.setStyle = function setStyle(style) { + dom.addCssClass(this.container, style); + }; + + /** + * VirtualRenderer.unsetStyle(style) -> Void + * - style (String): A class name + * + * [Removes the class `style` from the editor.]{: #VirtualRenderer.unsetStyle} + **/ + this.unsetStyle = function unsetStyle(style) { + dom.removeCssClass(this.container, style); + }; + + /** + * VirtualRenderer.destroy() + * + * Destroys the text and cursor layers for this renderer. + **/ + this.destroy = function() { + this.$textLayer.destroy(); + this.$cursorLayer.destroy(); + }; + +}).call(VirtualRenderer.prototype); + +exports.VirtualRenderer = VirtualRenderer; +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * Julian Viereck + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/layer/gutter', ['require', 'exports', 'module' , 'ace/lib/dom', 'ace/lib/oop', 'ace/lib/event_emitter'], function(require, exports, module) { +"use strict"; + +var dom = require("../lib/dom"); +var oop = require("../lib/oop"); +var EventEmitter = require("../lib/event_emitter").EventEmitter; + +var Gutter = function(parentEl) { + this.element = dom.createElement("div"); + this.element.className = "ace_layer ace_gutter-layer"; + parentEl.appendChild(this.element); + this.setShowFoldWidgets(this.$showFoldWidgets); + + this.gutterWidth = 0; + + this.$breakpoints = []; + this.$annotations = []; + this.$decorations = []; +}; + +(function() { + + oop.implement(this, EventEmitter); + + this.setSession = function(session) { + this.session = session; + }; + + this.addGutterDecoration = function(row, className){ + if (!this.$decorations[row]) + this.$decorations[row] = ""; + this.$decorations[row] += " " + className; + }; + + this.removeGutterDecoration = function(row, className){ + this.$decorations[row] = this.$decorations[row].replace(" " + className, ""); + }; + + this.setBreakpoints = function(rows) { + this.$breakpoints = rows.concat(); + }; + + this.setAnnotations = function(annotations) { + // iterate over sparse array + this.$annotations = []; + for (var row in annotations) if (annotations.hasOwnProperty(row)) { + var rowAnnotations = annotations[row]; + if (!rowAnnotations) + continue; + + var rowInfo = this.$annotations[row] = { + text: [] + }; + for (var i=0; i foldStart) { + i = fold.end.row + 1; + fold = this.session.getNextFoldLine(i, fold); + foldStart = fold ?fold.start.row :Infinity; + } + if(i > lastRow) + break; + + var annotation = this.$annotations[i] || emptyAnno; + html.push("
", (i+1)); + + if (foldWidgets) { + var c = foldWidgets[i]; + // check if cached value is invalidated and we need to recompute + if (c == null) + c = foldWidgets[i] = this.session.getFoldWidget(i); + if (c) + html.push( + "" + ); + } + + var wrappedRowLength = this.session.getRowLength(i) - 1; + while (wrappedRowLength--) { + html.push("
\xA6"); + } + + html.push("
"); + + i++; + } + this.element = dom.setInnerHtml(this.element, html.join("")); + this.element.style.height = config.minHeight + "px"; + + var gutterWidth = this.element.offsetWidth; + if (gutterWidth !== this.gutterWidth) { + this.gutterWidth = gutterWidth; + this._emit("changeGutterWidth", gutterWidth); + } + }; + + this.$showFoldWidgets = true; + this.setShowFoldWidgets = function(show) { + if (show) + dom.addCssClass(this.element, "ace_folding-enabled"); + else + dom.removeCssClass(this.element, "ace_folding-enabled"); + + this.$showFoldWidgets = show; + }; + + this.getShowFoldWidgets = function() { + return this.$showFoldWidgets; + }; + +}).call(Gutter.prototype); + +exports.Gutter = Gutter; + +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * Julian Viereck + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/layer/marker', ['require', 'exports', 'module' , 'ace/range', 'ace/lib/dom'], function(require, exports, module) { +"use strict"; + +var Range = require("../range").Range; +var dom = require("../lib/dom"); + +var Marker = function(parentEl) { + this.element = dom.createElement("div"); + this.element.className = "ace_layer ace_marker-layer"; + parentEl.appendChild(this.element); +}; + +(function() { + + this.$padding = 0; + + this.setPadding = function(padding) { + this.$padding = padding; + }; + this.setSession = function(session) { + this.session = session; + }; + + this.setMarkers = function(markers) { + this.markers = markers; + }; + + this.update = function(config) { + var config = config || this.config; + if (!config) + return; + + this.config = config; + + + var html = []; + for ( var key in this.markers) { + var marker = this.markers[key]; + + var range = marker.range.clipRows(config.firstRow, config.lastRow); + if (range.isEmpty()) continue; + + range = range.toScreenRange(this.session); + if (marker.renderer) { + var top = this.$getTop(range.start.row, config); + var left = Math.round( + this.$padding + range.start.column * config.characterWidth + ); + marker.renderer(html, range, left, top, config); + } + else if (range.isMultiLine()) { + if (marker.type == "text") { + this.drawTextMarker(html, range, marker.clazz, config); + } else { + this.drawMultiLineMarker( + html, range, marker.clazz, config, + marker.type + ); + } + } + else { + this.drawSingleLineMarker( + html, range, marker.clazz + " start", config, + null, marker.type + ); + } + } + this.element = dom.setInnerHtml(this.element, html.join("")); + }; + + this.$getTop = function(row, layerConfig) { + return (row - layerConfig.firstRowScreen) * layerConfig.lineHeight; + }; + + // Draws a marker, which spans a range of text on multiple lines + this.drawTextMarker = function(stringBuilder, range, clazz, layerConfig) { + // selection start + var row = range.start.row; + + var lineRange = new Range( + row, range.start.column, + row, this.session.getScreenLastRowColumn(row) + ); + this.drawSingleLineMarker(stringBuilder, lineRange, clazz + " start", layerConfig, 1, "text"); + + // selection end + row = range.end.row; + lineRange = new Range(row, 0, row, range.end.column); + this.drawSingleLineMarker(stringBuilder, lineRange, clazz, layerConfig, 0, "text"); + + for (row = range.start.row + 1; row < range.end.row; row++) { + lineRange.start.row = row; + lineRange.end.row = row; + lineRange.end.column = this.session.getScreenLastRowColumn(row); + this.drawSingleLineMarker(stringBuilder, lineRange, clazz, layerConfig, 1, "text"); + } + }; + + // Draws a multi line marker, where lines span the full width + this.drawMultiLineMarker = function(stringBuilder, range, clazz, layerConfig, type) { + var padding = type === "background" ? 0 : this.$padding; + var layerWidth = layerConfig.width + 2 * this.$padding - padding; + // from selection start to the end of the line + var height = layerConfig.lineHeight; + var width = Math.round(layerWidth - (range.start.column * layerConfig.characterWidth)); + var top = this.$getTop(range.start.row, layerConfig); + var left = Math.round( + padding + range.start.column * layerConfig.characterWidth + ); + + stringBuilder.push( + "
" + ); + + // from start of the last line to the selection end + top = this.$getTop(range.end.row, layerConfig); + width = Math.round(range.end.column * layerConfig.characterWidth); + + stringBuilder.push( + "
" + ); + + // all the complete lines + height = (range.end.row - range.start.row - 1) * layerConfig.lineHeight; + if (height < 0) + return; + top = this.$getTop(range.start.row + 1, layerConfig); + + stringBuilder.push( + "
" + ); + }; + + // Draws a marker which covers part or whole width of a single screen line + this.drawSingleLineMarker = function(stringBuilder, range, clazz, layerConfig, extraLength, type) { + var padding = type === "background" ? 0 : this.$padding; + var height = layerConfig.lineHeight; + + if (type === "background") + var width = layerConfig.width; + else + width = Math.round((range.end.column + (extraLength || 0) - range.start.column) * layerConfig.characterWidth); + + var top = this.$getTop(range.start.row, layerConfig); + var left = Math.round( + padding + range.start.column * layerConfig.characterWidth + ); + + stringBuilder.push( + "
" + ); + }; + +}).call(Marker.prototype); + +exports.Marker = Marker; + +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * Julian Viereck + * Mihai Sucan + * Irakli Gozalishvili (http://jeditoolkit.com) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/layer/text', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/lib/dom', 'ace/lib/lang', 'ace/lib/useragent', 'ace/lib/event_emitter'], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var dom = require("../lib/dom"); +var lang = require("../lib/lang"); +var useragent = require("../lib/useragent"); +var EventEmitter = require("../lib/event_emitter").EventEmitter; + +var Text = function(parentEl) { + this.element = dom.createElement("div"); + this.element.className = "ace_layer ace_text-layer"; + parentEl.appendChild(this.element); + + this.$characterSize = this.$measureSizes() || {width: 0, height: 0}; + this.$pollSizeChanges(); +}; + +(function() { + + oop.implement(this, EventEmitter); + + this.EOF_CHAR = "\xB6"; //"¶"; + this.EOL_CHAR = "\xAC"; //"¬"; + this.TAB_CHAR = "\u2192"; //"→"; + this.SPACE_CHAR = "\xB7"; //"·"; + this.$padding = 0; + + this.setPadding = function(padding) { + this.$padding = padding; + this.element.style.padding = "0 " + padding + "px"; + }; + + this.getLineHeight = function() { + return this.$characterSize.height || 1; + }; + + this.getCharacterWidth = function() { + return this.$characterSize.width || 1; + }; + + this.checkForSizeChanges = function() { + var size = this.$measureSizes(); + if (size && (this.$characterSize.width !== size.width || this.$characterSize.height !== size.height)) { + this.$characterSize = size; + this._emit("changeCharacterSize", {data: size}); + } + }; + + this.$pollSizeChanges = function() { + var self = this; + this.$pollSizeChangesTimer = setInterval(function() { + self.checkForSizeChanges(); + }, 500); + }; + + this.$fontStyles = { + fontFamily : 1, + fontSize : 1, + fontWeight : 1, + fontStyle : 1, + lineHeight : 1 + }; + + this.$measureSizes = useragent.isIE || useragent.isOldGecko ? function() { + var n = 1000; + if (!this.$measureNode) { + var measureNode = this.$measureNode = dom.createElement("div"); + var style = measureNode.style; + + style.width = style.height = "auto"; + style.left = style.top = (-n * 40) + "px"; + + style.visibility = "hidden"; + style.position = "fixed"; + style.overflow = "visible"; + style.whiteSpace = "nowrap"; + + // in FF 3.6 monospace fonts can have a fixed sub pixel width. + // that's why we have to measure many characters + // Note: characterWidth can be a float! + measureNode.innerHTML = lang.stringRepeat("Xy", n); + + if (this.element.ownerDocument.body) { + this.element.ownerDocument.body.appendChild(measureNode); + } else { + var container = this.element.parentNode; + while (!dom.hasCssClass(container, "ace_editor")) + container = container.parentNode; + container.appendChild(measureNode); + } + } + + // Size and width can be null if the editor is not visible or + // detached from the document + if (!this.element.offsetWidth) + return null; + + var style = this.$measureNode.style; + var computedStyle = dom.computedStyle(this.element); + for (var prop in this.$fontStyles) + style[prop] = computedStyle[prop]; + + var size = { + height: this.$measureNode.offsetHeight, + width: this.$measureNode.offsetWidth / (n * 2) + }; + + // Size and width can be null if the editor is not visible or + // detached from the document + if (size.width == 0 || size.height == 0) + return null; + + return size; + } + : function() { + if (!this.$measureNode) { + var measureNode = this.$measureNode = dom.createElement("div"); + var style = measureNode.style; + + style.width = style.height = "auto"; + style.left = style.top = -100 + "px"; + + style.visibility = "hidden"; + style.position = "fixed"; + style.overflow = "visible"; + style.whiteSpace = "nowrap"; + + measureNode.innerHTML = "X"; + + var container = this.element.parentNode; + while (container && !dom.hasCssClass(container, "ace_editor")) + container = container.parentNode; + + if (!container) + return this.$measureNode = null; + + container.appendChild(measureNode); + } + + var rect = this.$measureNode.getBoundingClientRect(); + + var size = { + height: rect.height, + width: rect.width + }; + + // Size and width can be null if the editor is not visible or + // detached from the document + if (size.width == 0 || size.height == 0) + return null; + + return size; + }; + + this.setSession = function(session) { + this.session = session; + }; + + this.showInvisibles = false; + this.setShowInvisibles = function(showInvisibles) { + if (this.showInvisibles == showInvisibles) + return false; + + this.showInvisibles = showInvisibles; + return true; + }; + + this.$tabStrings = []; + this.$computeTabString = function() { + var tabSize = this.session.getTabSize(); + var tabStr = this.$tabStrings = [0]; + for (var i = 1; i < tabSize + 1; i++) { + if (this.showInvisibles) { + tabStr.push("" + + this.TAB_CHAR + + new Array(i).join(" ") + + ""); + } else { + tabStr.push(new Array(i+1).join(" ")); + } + } + + }; + + this.updateLines = function(config, firstRow, lastRow) { + this.$computeTabString(); + // Due to wrap line changes there can be new lines if e.g. + // the line to updated wrapped in the meantime. + if (this.config.lastRow != config.lastRow || + this.config.firstRow != config.firstRow) { + this.scrollLines(config); + } + this.config = config; + + var first = Math.max(firstRow, config.firstRow); + var last = Math.min(lastRow, config.lastRow); + + var lineElements = this.element.childNodes; + var lineElementsIdx = 0; + + for (var row = config.firstRow; row < first; row++) { + var foldLine = this.session.getFoldLine(row); + if (foldLine) { + if (foldLine.containsRow(first)) { + first = foldLine.start.row; + break; + } else { + row = foldLine.end.row; + } + } + lineElementsIdx ++; + } + + for (var i=first; i<=last; i++) { + var lineElement = lineElements[lineElementsIdx++]; + if (!lineElement) + continue; + + var html = []; + var tokens = this.session.getTokens(i, i); + this.$renderLine(html, i, tokens[0].tokens, !this.$useLineGroups()); + lineElement = dom.setInnerHtml(lineElement, html.join("")); + + i = this.session.getRowFoldEnd(i); + } + }; + + this.scrollLines = function(config) { + this.$computeTabString(); + var oldConfig = this.config; + this.config = config; + + if (!oldConfig || oldConfig.lastRow < config.firstRow) + return this.update(config); + + if (config.lastRow < oldConfig.firstRow) + return this.update(config); + + var el = this.element; + if (oldConfig.firstRow < config.firstRow) + for (var row=this.session.getFoldedRowCount(oldConfig.firstRow, config.firstRow - 1); row>0; row--) + el.removeChild(el.firstChild); + + if (oldConfig.lastRow > config.lastRow) + for (var row=this.session.getFoldedRowCount(config.lastRow + 1, oldConfig.lastRow); row>0; row--) + el.removeChild(el.lastChild); + + if (config.firstRow < oldConfig.firstRow) { + var fragment = this.$renderLinesFragment(config, config.firstRow, oldConfig.firstRow - 1); + if (el.firstChild) + el.insertBefore(fragment, el.firstChild); + else + el.appendChild(fragment); + } + + if (config.lastRow > oldConfig.lastRow) { + var fragment = this.$renderLinesFragment(config, oldConfig.lastRow + 1, config.lastRow); + el.appendChild(fragment); + } + }; + + this.$renderLinesFragment = function(config, firstRow, lastRow) { + var fragment = this.element.ownerDocument.createDocumentFragment(); + var row = firstRow; + var foldLine = this.session.getNextFoldLine(row); + var foldStart = foldLine ? foldLine.start.row : Infinity; + + while (true) { + if (row > foldStart) { + row = foldLine.end.row+1; + foldLine = this.session.getNextFoldLine(row, foldLine); + foldStart = foldLine ? foldLine.start.row : Infinity; + } + if (row > lastRow) + break; + + var container = dom.createElement("div"); + + var html = []; + // Get the tokens per line as there might be some lines in between + // beeing folded. + // OPTIMIZE: If there is a long block of unfolded lines, just make + // this call once for that big block of unfolded lines. + var tokens = this.session.getTokens(row, row); + if (tokens.length == 1) + this.$renderLine(html, row, tokens[0].tokens, false); + + // don't use setInnerHtml since we are working with an empty DIV + container.innerHTML = html.join(""); + if (this.$useLineGroups()) { + container.className = 'ace_line_group'; + fragment.appendChild(container); + } else { + var lines = container.childNodes + while(lines.length) + fragment.appendChild(lines[0]); + } + + row++; + } + return fragment; + }; + + this.update = function(config) { + this.$computeTabString(); + this.config = config; + + var html = []; + var firstRow = config.firstRow, lastRow = config.lastRow; + + var row = firstRow; + var foldLine = this.session.getNextFoldLine(row); + var foldStart = foldLine ? foldLine.start.row : Infinity; + + while (true) { + if (row > foldStart) { + row = foldLine.end.row+1; + foldLine = this.session.getNextFoldLine(row, foldLine); + foldStart = foldLine ? foldLine.start.row :Infinity; + } + if (row > lastRow) + break; + + if (this.$useLineGroups()) + html.push("
") + + // Get the tokens per line as there might be some lines in between + // beeing folded. + // OPTIMIZE: If there is a long block of unfolded lines, just make + // this call once for that big block of unfolded lines. + var tokens = this.session.getTokens(row, row); + if (tokens.length == 1) + this.$renderLine(html, row, tokens[0].tokens, false); + + if (this.$useLineGroups()) + html.push("
"); // end the line group + + row++; + } + this.element = dom.setInnerHtml(this.element, html.join("")); + }; + + this.$textToken = { + "text": true, + "rparen": true, + "lparen": true + }; + + this.$renderToken = function(stringBuilder, screenColumn, token, value) { + var self = this; + var replaceReg = /\t|&|<|( +)|([\u0000-\u0019\u00a0\u2000-\u200b\u2028\u2029\u3000])|[\u1100-\u115F\u11A3-\u11A7\u11FA-\u11FF\u2329-\u232A\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3000-\u303E\u3041-\u3096\u3099-\u30FF\u3105-\u312D\u3131-\u318E\u3190-\u31BA\u31C0-\u31E3\u31F0-\u321E\u3220-\u3247\u3250-\u32FE\u3300-\u4DBF\u4E00-\uA48C\uA490-\uA4C6\uA960-\uA97C\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFF01-\uFF60\uFFE0-\uFFE6]/g; + var replaceFunc = function(c, a, b, tabIdx, idx4) { + if (a) { + return new Array(c.length+1).join(" "); + } else if (c == "&") { + return "&"; + } else if (c == "<") { + return "<"; + } else if (c == "\t") { + var tabSize = self.session.getScreenTabSize(screenColumn + tabIdx); + screenColumn += tabSize - 1; + return self.$tabStrings[tabSize]; + } else if (c == "\u3000") { + // U+3000 is both invisible AND full-width, so must be handled uniquely + var classToUse = self.showInvisibles ? "ace_cjk ace_invisible" : "ace_cjk"; + var space = self.showInvisibles ? self.SPACE_CHAR : ""; + screenColumn += 1; + return "" + space + ""; + } else if (b) { + return "" + self.SPACE_CHAR + ""; + } else { + screenColumn += 1; + return "" + c + ""; + } + }; + + var output = value.replace(replaceReg, replaceFunc); + + if (!this.$textToken[token.type]) { + var classes = "ace_" + token.type.replace(/\./g, " ace_"); + var style = ""; + if (token.type == "fold") + style = " style='width:" + (token.value.length * this.config.characterWidth) + "px;' "; + stringBuilder.push("", output, ""); + } + else { + stringBuilder.push(output); + } + return screenColumn + value.length; + }; + + this.$renderLineCore = function(stringBuilder, lastRow, tokens, splits, onlyContents) { + var chars = 0; + var split = 0; + var splitChars; + var screenColumn = 0; + var self = this; + + if (!splits || splits.length == 0) + splitChars = Number.MAX_VALUE; + else + splitChars = splits[0]; + + if (!onlyContents) { + stringBuilder.push("
" + ); + } + + for (var i = 0; i < tokens.length; i++) { + var token = tokens[i]; + var value = token.value; + + if (chars + value.length < splitChars) { + screenColumn = self.$renderToken( + stringBuilder, screenColumn, token, value + ); + chars += value.length; + } + else { + while (chars + value.length >= splitChars) { + screenColumn = self.$renderToken( + stringBuilder, screenColumn, + token, value.substring(0, splitChars - chars) + ); + value = value.substring(splitChars - chars); + chars = splitChars; + + if (!onlyContents) { + stringBuilder.push("
", + "
" + ); + } + + split ++; + screenColumn = 0; + splitChars = splits[split] || Number.MAX_VALUE; + } + if (value.length != 0) { + chars += value.length; + screenColumn = self.$renderToken( + stringBuilder, screenColumn, token, value + ); + } + } + } + + if (this.showInvisibles) { + if (lastRow !== this.session.getLength() - 1) + stringBuilder.push("" + this.EOL_CHAR + ""); + else + stringBuilder.push("" + this.EOF_CHAR + ""); + } + if (!onlyContents) + stringBuilder.push("
"); + }; + + this.$renderLine = function(stringBuilder, row, tokens, onlyContents) { + // Check if the line to render is folded or not. If not, things are + // simple, otherwise, we need to fake some things... + if (!this.session.isRowFolded(row)) { + var splits = this.session.getRowSplitData(row); + this.$renderLineCore(stringBuilder, row, tokens, splits, onlyContents); + } else { + this.$renderFoldLine(stringBuilder, row, tokens, onlyContents); + } + }; + + this.$renderFoldLine = function(stringBuilder, row, tokens, onlyContents) { + var session = this.session, + foldLine = session.getFoldLine(row), + renderTokens = []; + + function addTokens(tokens, from, to) { + var idx = 0, col = 0; + while ((col + tokens[idx].value.length) < from) { + col += tokens[idx].value.length; + idx++; + + if (idx == tokens.length) { + return; + } + } + if (col != from) { + var value = tokens[idx].value.substring(from - col); + // Check if the token value is longer then the from...to spacing. + if (value.length > (to - from)) { + value = value.substring(0, to - from); + } + + renderTokens.push({ + type: tokens[idx].type, + value: value + }); + + col = from + value.length; + idx += 1; + } + + while (col < to) { + var value = tokens[idx].value; + if (value.length + col > to) { + value = value.substring(0, to - col); + } + renderTokens.push({ + type: tokens[idx].type, + value: value + }); + col += value.length; + idx += 1; + } + } + + foldLine.walk(function(placeholder, row, column, lastColumn, isNewRow) { + if (placeholder) { + renderTokens.push({ + type: "fold", + value: placeholder + }); + } else { + if (isNewRow) { + tokens = this.session.getTokens(row, row)[0].tokens; + } + if (tokens.length != 0) { + addTokens(tokens, lastColumn, column); + } + } + }.bind(this), foldLine.end.row, this.session.getLine(foldLine.end.row).length); + + // TODO: Build a fake splits array! + var splits = this.session.$useWrapMode?this.session.$wrapData[row]:null; + this.$renderLineCore(stringBuilder, row, renderTokens, splits, onlyContents); + }; + + this.$useLineGroups = function() { + // For the updateLines function to work correctly, it's important that the + // child nodes of this.element correspond on a 1-to-1 basis to rows in the + // document (as distinct from lines on the screen). For sessions that are + // wrapped, this means we need to add a layer to the node hierarchy (tagged + // with the class name ace_line_group). + return this.session.getUseWrapMode(); + }; + + this.destroy = function() { + clearInterval(this.$pollSizeChangesTimer); + if (this.$measureNode) + this.$measureNode.parentNode.removeChild(this.$measureNode); + delete this.$measureNode; + }; + +}).call(Text.prototype); + +exports.Text = Text; + +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * Julian Viereck + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/layer/cursor', ['require', 'exports', 'module' , 'ace/lib/dom'], function(require, exports, module) { +"use strict"; + +var dom = require("../lib/dom"); + +var Cursor = function(parentEl) { + this.element = dom.createElement("div"); + this.element.className = "ace_layer ace_cursor-layer"; + parentEl.appendChild(this.element); + + this.isVisible = false; + + this.cursors = []; + this.cursor = this.addCursor(); +}; + +(function() { + + this.$padding = 0; + this.setPadding = function(padding) { + this.$padding = padding; + }; + + this.setSession = function(session) { + this.session = session; + }; + + this.addCursor = function() { + var el = dom.createElement("div"); + var className = "ace_cursor"; + if (!this.isVisible) + className += " ace_hidden"; + if (this.overwrite) + className += " ace_overwrite"; + + el.className = className; + this.element.appendChild(el); + this.cursors.push(el); + return el; + }; + + this.removeCursor = function() { + if (this.cursors.length > 1) { + var el = this.cursors.pop(); + el.parentNode.removeChild(el); + return el; + } + }; + + this.hideCursor = function() { + this.isVisible = false; + for (var i = this.cursors.length; i--; ) + dom.addCssClass(this.cursors[i], "ace_hidden"); + clearInterval(this.blinkId); + }; + + this.showCursor = function() { + this.isVisible = true; + for (var i = this.cursors.length; i--; ) + dom.removeCssClass(this.cursors[i], "ace_hidden"); + + this.element.style.visibility = ""; + this.restartTimer(); + }; + + this.restartTimer = function() { + clearInterval(this.blinkId); + if (!this.isVisible) + return; + + var element = this.cursors.length == 1 ? this.cursor : this.element; + this.blinkId = setInterval(function() { + element.style.visibility = "hidden"; + setTimeout(function() { + element.style.visibility = ""; + }, 400); + }, 1000); + }; + + this.getPixelPosition = function(position, onScreen) { + if (!this.config || !this.session) { + return { + left : 0, + top : 0 + }; + } + + if (!position) + position = this.session.selection.getCursor(); + var pos = this.session.documentToScreenPosition(position); + var cursorLeft = Math.round(this.$padding + + pos.column * this.config.characterWidth); + var cursorTop = (pos.row - (onScreen ? this.config.firstRowScreen : 0)) * + this.config.lineHeight; + + return { + left : cursorLeft, + top : cursorTop + }; + }; + + this.update = function(config) { + this.config = config; + + if (this.session.selectionMarkerCount > 0) { + var selections = this.session.$selectionMarkers; + var i = 0, sel, cursorIndex = 0; + + for (var i = selections.length; i--; ) { + sel = selections[i]; + var pixelPos = this.getPixelPosition(sel.cursor, true); + + var style = (this.cursors[cursorIndex++] || this.addCursor()).style; + + style.left = pixelPos.left + "px"; + style.top = pixelPos.top + "px"; + style.width = config.characterWidth + "px"; + style.height = config.lineHeight + "px"; + } + if (cursorIndex > 1) + while (this.cursors.length > cursorIndex) + this.removeCursor(); + } else { + var pixelPos = this.getPixelPosition(null, true); + var style = this.cursor.style; + style.left = pixelPos.left + "px"; + style.top = pixelPos.top + "px"; + style.width = config.characterWidth + "px"; + style.height = config.lineHeight + "px"; + + while (this.cursors.length > 1) + this.removeCursor(); + } + + var overwrite = this.session.getOverwrite(); + if (overwrite != this.overwrite) + this.$setOverite(overwrite); + + // cache for textarea and gutter highlight + this.$pixelPos = pixelPos; + + this.restartTimer(); + }; + + this.$setOverite = function(overwrite) { + this.overwrite = overwrite; + for (var i = this.cursors.length; i--; ) { + if (overwrite) + dom.addCssClass(this.cursors[i], "ace_overwrite"); + else + dom.removeCssClass(this.cursors[i], "ace_overwrite"); + } + }; + + this.destroy = function() { + clearInterval(this.blinkId); + } + +}).call(Cursor.prototype); + +exports.Cursor = Cursor; + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * Irakli Gozalishvili (http://jeditoolkit.com) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/scrollbar', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/lib/dom', 'ace/lib/event', 'ace/lib/event_emitter'], function(require, exports, module) { +"use strict"; + +var oop = require("./lib/oop"); +var dom = require("./lib/dom"); +var event = require("./lib/event"); +var EventEmitter = require("./lib/event_emitter").EventEmitter; + +/** + * class ScrollBar + * + * A set of methods for setting and retrieving the editor's scrollbar. + * + **/ + +/** + * new ScrollBar(parent) + * - parent (DOMElement): A DOM element + * + * Creates a new `ScrollBar`. `parent` is the owner of the scroll bar. + * + **/ +var ScrollBar = function(parent) { + this.element = dom.createElement("div"); + this.element.className = "ace_sb"; + + this.inner = dom.createElement("div"); + this.element.appendChild(this.inner); + + parent.appendChild(this.element); + + // in OSX lion the scrollbars appear to have no width. In this case resize + // the to show the scrollbar but still pretend that the scrollbar has a width + // of 0px + // in Firefox 6+ scrollbar is hidden if element has the same width as scrollbar + // make element a little bit wider to retain scrollbar when page is zoomed + this.width = dom.scrollbarWidth(parent.ownerDocument); + this.element.style.width = (this.width || 15) + 5 + "px"; + + event.addListener(this.element, "scroll", this.onScroll.bind(this)); +}; + +(function() { + oop.implement(this, EventEmitter); + + /** + * ScrollBar@onScroll + * + * Emitted when the scroll bar, well, scrolls. + * + **/ + this.onScroll = function() { + this._emit("scroll", {data: this.element.scrollTop}); + }; + + /** + * ScrollBar.getWidth() -> Number + * + * Returns the width of the scroll bar. + * + **/ + this.getWidth = function() { + return this.width; + }; + + /** + * ScrollBar.setHeight(height) + * - height (Number): The new height + * + * Sets the height of the scroll bar, in pixels. + * + **/ + this.setHeight = function(height) { + this.element.style.height = height + "px"; + }; + + /** + * ScrollBar.setInnerHeight(height) + * - height (Number): The new inner height + * + * Sets the inner height of the scroll bar, in pixels. + * + **/ + this.setInnerHeight = function(height) { + this.inner.style.height = height + "px"; + }; + + /** + * ScrollBar.setScrollTop(scrollTop) + * - scrollTop (Number): The new scroll top + * + * Sets the scroll top of the scroll bar. + * + **/ + // TODO: on chrome 17+ after for small zoom levels after this function + // this.element.scrollTop != scrollTop which makes page to scroll up. + this.setScrollTop = function(scrollTop) { + this.element.scrollTop = scrollTop; + }; + +}).call(ScrollBar.prototype); + +exports.ScrollBar = ScrollBar; +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * Irakli Gozalishvili (http://jeditoolkit.com) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/renderloop', ['require', 'exports', 'module' , 'ace/lib/event'], function(require, exports, module) { +"use strict"; + +var event = require("./lib/event"); + +/** internal, hide + * class RenderLoop + * + * Batches changes (that force something to be redrawn) in the background. + * + **/ + +/** internal, hide + * new RenderLoop(onRender, win) + * + * + * +**/ +var RenderLoop = function(onRender, win) { + this.onRender = onRender; + this.pending = false; + this.changes = 0; + this.window = win || window; +}; + +(function() { + + /** internal, hide + * RenderLoop.schedule(change) + * - change (Array): + * + * + **/ + this.schedule = function(change) { + //this.onRender(change); + //return; + this.changes = this.changes | change; + if (!this.pending) { + this.pending = true; + var _self = this; + event.nextTick(function() { + _self.pending = false; + var changes; + while (changes = _self.changes) { + _self.changes = 0; + _self.onRender(changes); + } + }, this.window); + } + }; + +}).call(RenderLoop.prototype); + +exports.RenderLoop = RenderLoop; +}); +ace.define("text!ace/css/editor.css", [], "\n" + + ".ace_editor {\n" + + " position: absolute;\n" + + " overflow: hidden;\n" + + " font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Droid Sans Mono', 'Consolas', monospace;\n" + + " font-size: 12px;\n" + + "}\n" + + "\n" + + ".ace_scroller {\n" + + " position: absolute;\n" + + " overflow-x: scroll;\n" + + " overflow-y: hidden;\n" + + "}\n" + + "\n" + + ".ace_content {\n" + + " position: absolute;\n" + + " box-sizing: border-box;\n" + + " -moz-box-sizing: border-box;\n" + + " -webkit-box-sizing: border-box;\n" + + " cursor: text;\n" + + "}\n" + + "\n" + + ".ace_composition {\n" + + " position: absolute;\n" + + " background: #555;\n" + + " color: #DDD;\n" + + " z-index: 4;\n" + + "}\n" + + "\n" + + ".ace_gutter {\n" + + " position: absolute;\n" + + " overflow : hidden;\n" + + " height: 100%;\n" + + " width: auto;\n" + + " cursor: default;\n" + + " z-index: 1000;\n" + + "}\n" + + "\n" + + ".ace_gutter_active_line {\n" + + " position: absolute;\n" + + " right: 0;\n" + + " width: 100%;\n" + + "}\n" + + "\n" + + ".ace_gutter.horscroll {\n" + + " box-shadow: 0px 0px 20px rgba(0,0,0,0.4);\n" + + "}\n" + + "\n" + + ".ace_gutter-cell {\n" + + " padding-left: 19px;\n" + + " padding-right: 6px;\n" + + "}\n" + + "\n" + + ".ace_gutter-cell.ace_error {\n" + + " background-image: url(\"data:image/gif,GIF89a%10%00%10%00%D5%00%00%F5or%F5%87%88%F5nr%F4ns%EBmq%F5z%7F%DDJT%DEKS%DFOW%F1Yc%F2ah%CE(7%CE)8%D18E%DD%40M%F2KZ%EBU%60%F4%60m%DCir%C8%16(%C8%19*%CE%255%F1%3FR%F1%3FS%E6%AB%B5%CA%5DI%CEn%5E%F7%A2%9A%C9G%3E%E0a%5B%F7%89%85%F5yy%F6%82%80%ED%82%80%FF%BF%BF%E3%C4%C4%FF%FF%FF%FF%FF%FF%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00!%F9%04%01%00%00%25%00%2C%00%00%00%00%10%00%10%00%00%06p%C0%92pH%2C%1A%8F%C8%D2H%93%E1d4%23%E4%88%D3%09mB%1DN%B48%F5%90%40%60%92G%5B%94%20%3E%22%D2%87%24%FA%20%24%C5%06A%00%20%B1%07%02B%A38%89X.v%17%82%11%13q%10%0Fi%24%0F%8B%10%7BD%12%0Ei%09%92%09%0EpD%18%15%24%0A%9Ci%05%0C%18F%18%0B%07%04%01%04%06%A0H%18%12%0D%14%0D%12%A1I%B3%B4%B5IA%00%3B\");\n" + + " background-repeat: no-repeat;\n" + + " background-position: 2px center;\n" + + "}\n" + + "\n" + + ".ace_gutter-cell.ace_warning {\n" + + " background-image: url(\"data:image/gif,GIF89a%10%00%10%00%D5%00%00%FF%DBr%FF%DE%81%FF%E2%8D%FF%E2%8F%FF%E4%96%FF%E3%97%FF%E5%9D%FF%E6%9E%FF%EE%C1%FF%C8Z%FF%CDk%FF%D0s%FF%D4%81%FF%D5%82%FF%D5%83%FF%DC%97%FF%DE%9D%FF%E7%B8%FF%CCl%7BQ%13%80U%15%82W%16%81U%16%89%5B%18%87%5B%18%8C%5E%1A%94d%1D%C5%83-%C9%87%2F%C6%84.%C6%85.%CD%8B2%C9%871%CB%8A3%CD%8B5%DC%98%3F%DF%9BB%E0%9CC%E1%A5U%CB%871%CF%8B5%D1%8D6%DB%97%40%DF%9AB%DD%99B%E3%B0p%E7%CC%AE%FF%FF%FF%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00!%F9%04%01%00%00%2F%00%2C%00%00%00%00%10%00%10%00%00%06a%C0%97pH%2C%1A%8FH%A1%ABTr%25%87%2B%04%82%F4%7C%B9X%91%08%CB%99%1C!%26%13%84*iJ9(%15G%CA%84%14%01%1A%97%0C%03%80%3A%9A%3E%81%84%3E%11%08%B1%8B%20%02%12%0F%18%1A%0F%0A%03'F%1C%04%0B%10%16%18%10%0B%05%1CF%1D-%06%07%9A%9A-%1EG%1B%A0%A1%A0U%A4%A5%A6BA%00%3B\");\n" + + " background-repeat: no-repeat;\n" + + " background-position: 2px center;\n" + + "}\n" + + "\n" + + ".ace_gutter-cell.ace_info {\n" + + " background-image: url(\"data:image/gif;base64,R0lGODlhEAAQAMQAAAAAAEFBQVJSUl5eXmRkZGtra39/f4WFhYmJiZGRkaampry8vMPDw8zMzNXV1dzc3OTk5Orq6vDw8P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABQALAAAAAAQABAAAAUuICWOZGmeaBml5XGwFCQSBGyXRSAwtqQIiRuiwIM5BoYVbEFIyGCQoeJGrVptIQA7\");\n" + + " background-repeat: no-repeat;\n" + + " background-position: 2px center;\n" + + "}\n" + + "\n" + + ".ace_editor .ace_sb {\n" + + " position: absolute;\n" + + " overflow-x: hidden;\n" + + " overflow-y: scroll;\n" + + " right: 0;\n" + + "}\n" + + "\n" + + ".ace_editor .ace_sb div {\n" + + " position: absolute;\n" + + " width: 1px;\n" + + " left: 0;\n" + + "}\n" + + "\n" + + ".ace_editor .ace_print_margin_layer {\n" + + " z-index: 0;\n" + + " position: absolute;\n" + + " overflow: hidden;\n" + + " margin: 0;\n" + + " left: 0;\n" + + " height: 100%;\n" + + " width: 100%;\n" + + "}\n" + + "\n" + + ".ace_editor .ace_print_margin {\n" + + " position: absolute;\n" + + " height: 100%;\n" + + "}\n" + + "\n" + + ".ace_editor textarea {\n" + + " position: fixed;\n" + + " z-index: 0;\n" + + " width: 0.5em;\n" + + " height: 1em;\n" + + " opacity: 0;\n" + + " background: transparent;\n" + + " appearance: none;\n" + + " -moz-appearance: none;\n" + + " border: none;\n" + + " resize: none;\n" + + " outline: none;\n" + + " overflow: hidden;\n" + + "}\n" + + "\n" + + ".ace_layer {\n" + + " z-index: 1;\n" + + " position: absolute;\n" + + " overflow: hidden;\n" + + " white-space: nowrap;\n" + + " height: 100%;\n" + + " width: 100%;\n" + + " box-sizing: border-box;\n" + + " -moz-box-sizing: border-box;\n" + + " -webkit-box-sizing: border-box;\n" + + " /* setting pointer-events: auto; on node under the mouse, which changes\n" + + " during scroll, will break mouse wheel scrolling in Safari */\n" + + " pointer-events: none;\n" + + "}\n" + + "\n" + + ".ace_gutter .ace_layer {\n" + + " position: relative;\n" + + " min-width: 40px;\n" + + " text-align: right;\n" + + " pointer-events: auto;\n" + + "}\n" + + "\n" + + ".ace_text-layer {\n" + + " color: black;\n" + + " font: inherit !important;\n" + + "}\n" + + "\n" + + ".ace_cjk {\n" + + " display: inline-block;\n" + + " text-align: center;\n" + + "}\n" + + "\n" + + ".ace_cursor-layer {\n" + + " z-index: 4;\n" + + "}\n" + + "\n" + + ".ace_cursor {\n" + + " z-index: 4;\n" + + " position: absolute;\n" + + "}\n" + + "\n" + + ".ace_cursor.ace_hidden {\n" + + " opacity: 0.2;\n" + + "}\n" + + "\n" + + ".ace_editor.multiselect .ace_cursor {\n" + + " border-left-width: 1px;\n" + + "}\n" + + "\n" + + ".ace_line {\n" + + " white-space: nowrap;\n" + + "}\n" + + "\n" + + ".ace_marker-layer .ace_step {\n" + + " position: absolute;\n" + + " z-index: 3;\n" + + "}\n" + + "\n" + + ".ace_marker-layer .ace_selection {\n" + + " position: absolute;\n" + + " z-index: 5;\n" + + "}\n" + + "\n" + + ".ace_marker-layer .ace_bracket {\n" + + " position: absolute;\n" + + " z-index: 6;\n" + + "}\n" + + "\n" + + ".ace_marker-layer .ace_active_line {\n" + + " position: absolute;\n" + + " z-index: 2;\n" + + "}\n" + + "\n" + + ".ace_marker-layer .ace_selected_word {\n" + + " position: absolute;\n" + + " z-index: 4;\n" + + " box-sizing: border-box;\n" + + " -moz-box-sizing: border-box;\n" + + " -webkit-box-sizing: border-box;\n" + + "}\n" + + "\n" + + ".ace_line .ace_fold {\n" + + " box-sizing: border-box;\n" + + " -moz-box-sizing: border-box;\n" + + " -webkit-box-sizing: border-box;\n" + + " \n" + + " display: inline-block;\n" + + " height: 11px;\n" + + " margin-top: -2px;\n" + + " vertical-align: middle;\n" + + "\n" + + " background-image: \n" + + " url(\"data:image/png,%89PNG%0D%0A%1A%0A%00%00%00%0DIHDR%00%00%00%11%00%00%00%09%08%06%00%00%00%D4%E8%C7%0C%00%00%03%1EiCCPICC%20Profile%00%00x%01%85T%DFk%D3P%14%FE%DAe%9D%B0%E1%8B%3Ag%11%09%3Eh%91ndStC%9C%B6kW%BA%CDZ%EA6%B7!H%9B%A6m%5C%9A%C6%24%ED~%B0%07%D9%8Bo%3A%C5w%F1%07%3E%F9%07%0C%D9%83o%7B%92%0D%C6%14a%F8%AC%88%22L%F6%22%B3%9E%9B4M'S%03%B9%F7%BB%DF%F9%EE9'%E7%E4%5E%A0%F9qZ%D3%14%2F%0F%14USO%C5%C2%FC%C4%E4%14%DF%F2%01%5E%1CC%2B%FChM%8B%86%16J%26G%40%0F%D3%B2y%EF%B3%F3%0E%1E%C6lt%EEo%DF%AB%FEc%D5%9A%95%0C%11%F0%1C%20%BE%945%C4%22%E1Y%A0i%5C%D4t%13%E0%D6%89%EF%9D15%C2%CDLsX%A7%04%09%1Fg8oc%81%E1%8C%8D%23%96f45%40%9A%09%C2%07%C5B%3AK%B8%408%98i%E0%F3%0D%D8%CE%81%14%E4'%26%A9%92.%8B%3C%ABER%2F%E5dE%B2%0C%F6%F0%1Fs%83%F2_%B0%A8%94%E9%9B%AD%E7%10%8Dm%9A%19N%D1%7C%8A%DE%1F9%7Dp%8C%E6%00%D5%C1%3F_%18%BDA%B8%9DpX6%E3%A35~B%CD%24%AE%11%26%BD%E7%EEti%98%EDe%9A%97Y)%12%25%1C%24%BCbT%AE3li%E6%0B%03%89%9A%E6%D3%ED%F4P%92%B0%9F4%BF43Y%F3%E3%EDP%95%04%EB1%C5%F5%F6KF%F4%BA%BD%D7%DB%91%93%07%E35%3E%A7)%D6%7F%40%FE%BD%F7%F5r%8A%E5y%92%F0%EB%B4%1E%8D%D5%F4%5B%92%3AV%DB%DB%E4%CD%A6%23%C3%C4wQ%3F%03HB%82%8E%1Cd(%E0%91B%0Ca%9Ac%C4%AA%F8L%16%19%22J%A4%D2itTy%B28%D6%3B(%93%96%ED%1CGx%C9_%0E%B8%5E%16%F5%5B%B2%B8%F6%E0%FB%9E%DD%25%D7%8E%BC%15%85%C5%B7%A3%D8Q%ED%B5%81%E9%BA%B2%13%9A%1B%7Fua%A5%A3n%E17%B9%E5%9B%1Bm%AB%0B%08Q%FE%8A%E5%B1H%5Ee%CAO%82Q%D7u6%E6%90S%97%FCu%0B%CF2%94%EE%25v%12X%0C%BA%AC%F0%5E%F8*l%0AO%85%17%C2%97%BF%D4%C8%CE%DE%AD%11%CB%80q%2C%3E%AB%9ES%CD%C6%EC%25%D2L%D2%EBd%B8%BF%8A%F5B%C6%18%F9%901CZ%9D%BE%24M%9C%8A9%F2%DAP%0B'%06w%82%EB%E6%E2%5C%2F%D7%07%9E%BB%CC%5D%E1%FA%B9%08%AD.r%23%8E%C2%17%F5E%7C!%F0%BE3%BE%3E_%B7o%88a%A7%DB%BE%D3d%EB%A31Z%EB%BB%D3%91%BA%A2%B1z%94%8F%DB'%F6%3D%8E%AA%13%19%B2%B1%BE%B1~V%08%2B%B4%A2cjJ%B3tO%00%03%25mN%97%F3%05%93%EF%11%84%0B%7C%88%AE-%89%8F%ABbW%90O%2B%0Ao%99%0C%5E%97%0CI%AFH%D9.%B0%3B%8F%ED%03%B6S%D6%5D%E6i_s9%F3*p%E9%1B%FD%C3%EB.7U%06%5E%19%C0%D1s.%17%A03u%E4%09%B0%7C%5E%2C%EB%15%DB%1F%3C%9E%B7%80%91%3B%DBc%AD%3Dma%BA%8B%3EV%AB%DBt.%5B%1E%01%BB%0F%AB%D5%9F%CF%AA%D5%DD%E7%E4%7F%0Bx%A3%FC%06%A9%23%0A%D6%C2%A1_2%00%00%00%09pHYs%00%00%0B%13%00%00%0B%13%01%00%9A%9C%18%00%00%00%B5IDAT(%15%A5%91%3D%0E%02!%10%85ac%E1%05%D6%CE%D6%C6%CE%D2%E8%ED%CD%DE%C0%C6%D6N.%E0V%F8%3D%9Ca%891XH%C2%BE%D9y%3F%90!%E6%9C%C3%BFk%E5%011%C6-%F5%C8N%04%DF%BD%FF%89%DFt%83DN%60%3E%F3%AB%A0%DE%1A%5Dg%BE%10Q%97%1B%40%9C%A8o%10%8F%5E%828%B4%1B%60%87%F6%02%26%85%1Ch%1E%C1%2B%5Bk%FF%86%EE%B7j%09%9A%DA%9B%ACe%A3%F9%EC%DA!9%B4%D5%A6%81%86%86%98%CC%3C%5B%40%FA%81%B3%E9%CB%23%94%C16Azo%05%D4%E1%C1%95a%3B%8A'%A0%E8%CC%17%22%85%1D%BA%00%A2%FA%DC%0A%94%D1%D1%8D%8B%3A%84%17B%C7%60%1A%25Z%FC%8D%00%00%00%00IEND%AEB%60%82\"),\n" + + " url(\"data:image/png,%89PNG%0D%0A%1A%0A%00%00%00%0DIHDR%00%00%00%05%00%00%007%08%06%00%00%00%C4%DD%80C%00%00%03%1EiCCPICC%20Profile%00%00x%01%85T%DFk%D3P%14%FE%DAe%9D%B0%E1%8B%3Ag%11%09%3Eh%91ndStC%9C%B6kW%BA%CDZ%EA6%B7!H%9B%A6m%5C%9A%C6%24%ED~%B0%07%D9%8Bo%3A%C5w%F1%07%3E%F9%07%0C%D9%83o%7B%92%0D%C6%14a%F8%AC%88%22L%F6%22%B3%9E%9B4M'S%03%B9%F7%BB%DF%F9%EE9'%E7%E4%5E%A0%F9qZ%D3%14%2F%0F%14USO%C5%C2%FC%C4%E4%14%DF%F2%01%5E%1CC%2B%FChM%8B%86%16J%26G%40%0F%D3%B2y%EF%B3%F3%0E%1E%C6lt%EEo%DF%AB%FEc%D5%9A%95%0C%11%F0%1C%20%BE%945%C4%22%E1Y%A0i%5C%D4t%13%E0%D6%89%EF%9D15%C2%CDLsX%A7%04%09%1Fg8oc%81%E1%8C%8D%23%96f45%40%9A%09%C2%07%C5B%3AK%B8%408%98i%E0%F3%0D%D8%CE%81%14%E4'%26%A9%92.%8B%3C%ABER%2F%E5dE%B2%0C%F6%F0%1Fs%83%F2_%B0%A8%94%E9%9B%AD%E7%10%8Dm%9A%19N%D1%7C%8A%DE%1F9%7Dp%8C%E6%00%D5%C1%3F_%18%BDA%B8%9DpX6%E3%A35~B%CD%24%AE%11%26%BD%E7%EEti%98%EDe%9A%97Y)%12%25%1C%24%BCbT%AE3li%E6%0B%03%89%9A%E6%D3%ED%F4P%92%B0%9F4%BF43Y%F3%E3%EDP%95%04%EB1%C5%F5%F6KF%F4%BA%BD%D7%DB%91%93%07%E35%3E%A7)%D6%7F%40%FE%BD%F7%F5r%8A%E5y%92%F0%EB%B4%1E%8D%D5%F4%5B%92%3AV%DB%DB%E4%CD%A6%23%C3%C4wQ%3F%03HB%82%8E%1Cd(%E0%91B%0Ca%9Ac%C4%AA%F8L%16%19%22J%A4%D2itTy%B28%D6%3B(%93%96%ED%1CGx%C9_%0E%B8%5E%16%F5%5B%B2%B8%F6%E0%FB%9E%DD%25%D7%8E%BC%15%85%C5%B7%A3%D8Q%ED%B5%81%E9%BA%B2%13%9A%1B%7Fua%A5%A3n%E17%B9%E5%9B%1Bm%AB%0B%08Q%FE%8A%E5%B1H%5Ee%CAO%82Q%D7u6%E6%90S%97%FCu%0B%CF2%94%EE%25v%12X%0C%BA%AC%F0%5E%F8*l%0AO%85%17%C2%97%BF%D4%C8%CE%DE%AD%11%CB%80q%2C%3E%AB%9ES%CD%C6%EC%25%D2L%D2%EBd%B8%BF%8A%F5B%C6%18%F9%901CZ%9D%BE%24M%9C%8A9%F2%DAP%0B'%06w%82%EB%E6%E2%5C%2F%D7%07%9E%BB%CC%5D%E1%FA%B9%08%AD.r%23%8E%C2%17%F5E%7C!%F0%BE3%BE%3E_%B7o%88a%A7%DB%BE%D3d%EB%A31Z%EB%BB%D3%91%BA%A2%B1z%94%8F%DB'%F6%3D%8E%AA%13%19%B2%B1%BE%B1~V%08%2B%B4%A2cjJ%B3tO%00%03%25mN%97%F3%05%93%EF%11%84%0B%7C%88%AE-%89%8F%ABbW%90O%2B%0Ao%99%0C%5E%97%0CI%AFH%D9.%B0%3B%8F%ED%03%B6S%D6%5D%E6i_s9%F3*p%E9%1B%FD%C3%EB.7U%06%5E%19%C0%D1s.%17%A03u%E4%09%B0%7C%5E%2C%EB%15%DB%1F%3C%9E%B7%80%91%3B%DBc%AD%3Dma%BA%8B%3EV%AB%DBt.%5B%1E%01%BB%0F%AB%D5%9F%CF%AA%D5%DD%E7%E4%7F%0Bx%A3%FC%06%A9%23%0A%D6%C2%A1_2%00%00%00%09pHYs%00%00%0B%13%00%00%0B%13%01%00%9A%9C%18%00%00%00%3AIDAT8%11c%FC%FF%FF%7F%18%03%1A%60%01%F2%3F%A0%891%80%04%FF%11-%F8%17%9BJ%E2%05%B1ZD%81v%26t%E7%80%F8%A3%82h%A12%1A%20%A3%01%02%0F%01%BA%25%06%00%19%C0%0D%AEF%D5%3ES%00%00%00%00IEND%AEB%60%82\");\n" + + " background-repeat: no-repeat, repeat-x;\n" + + " background-position: center center, top left;\n" + + " color: transparent;\n" + + "\n" + + " border: 1px solid black;\n" + + " -moz-border-radius: 2px;\n" + + " -webkit-border-radius: 2px;\n" + + " border-radius: 2px;\n" + + " \n" + + " cursor: pointer;\n" + + " pointer-events: auto;\n" + + "}\n" + + "\n" + + ".ace_dark .ace_fold {\n" + + "}\n" + + "\n" + + ".ace_fold:hover{\n" + + " background-image: \n" + + " url(\"data:image/png,%89PNG%0D%0A%1A%0A%00%00%00%0DIHDR%00%00%00%11%00%00%00%09%08%06%00%00%00%D4%E8%C7%0C%00%00%03%1EiCCPICC%20Profile%00%00x%01%85T%DFk%D3P%14%FE%DAe%9D%B0%E1%8B%3Ag%11%09%3Eh%91ndStC%9C%B6kW%BA%CDZ%EA6%B7!H%9B%A6m%5C%9A%C6%24%ED~%B0%07%D9%8Bo%3A%C5w%F1%07%3E%F9%07%0C%D9%83o%7B%92%0D%C6%14a%F8%AC%88%22L%F6%22%B3%9E%9B4M'S%03%B9%F7%BB%DF%F9%EE9'%E7%E4%5E%A0%F9qZ%D3%14%2F%0F%14USO%C5%C2%FC%C4%E4%14%DF%F2%01%5E%1CC%2B%FChM%8B%86%16J%26G%40%0F%D3%B2y%EF%B3%F3%0E%1E%C6lt%EEo%DF%AB%FEc%D5%9A%95%0C%11%F0%1C%20%BE%945%C4%22%E1Y%A0i%5C%D4t%13%E0%D6%89%EF%9D15%C2%CDLsX%A7%04%09%1Fg8oc%81%E1%8C%8D%23%96f45%40%9A%09%C2%07%C5B%3AK%B8%408%98i%E0%F3%0D%D8%CE%81%14%E4'%26%A9%92.%8B%3C%ABER%2F%E5dE%B2%0C%F6%F0%1Fs%83%F2_%B0%A8%94%E9%9B%AD%E7%10%8Dm%9A%19N%D1%7C%8A%DE%1F9%7Dp%8C%E6%00%D5%C1%3F_%18%BDA%B8%9DpX6%E3%A35~B%CD%24%AE%11%26%BD%E7%EEti%98%EDe%9A%97Y)%12%25%1C%24%BCbT%AE3li%E6%0B%03%89%9A%E6%D3%ED%F4P%92%B0%9F4%BF43Y%F3%E3%EDP%95%04%EB1%C5%F5%F6KF%F4%BA%BD%D7%DB%91%93%07%E35%3E%A7)%D6%7F%40%FE%BD%F7%F5r%8A%E5y%92%F0%EB%B4%1E%8D%D5%F4%5B%92%3AV%DB%DB%E4%CD%A6%23%C3%C4wQ%3F%03HB%82%8E%1Cd(%E0%91B%0Ca%9Ac%C4%AA%F8L%16%19%22J%A4%D2itTy%B28%D6%3B(%93%96%ED%1CGx%C9_%0E%B8%5E%16%F5%5B%B2%B8%F6%E0%FB%9E%DD%25%D7%8E%BC%15%85%C5%B7%A3%D8Q%ED%B5%81%E9%BA%B2%13%9A%1B%7Fua%A5%A3n%E17%B9%E5%9B%1Bm%AB%0B%08Q%FE%8A%E5%B1H%5Ee%CAO%82Q%D7u6%E6%90S%97%FCu%0B%CF2%94%EE%25v%12X%0C%BA%AC%F0%5E%F8*l%0AO%85%17%C2%97%BF%D4%C8%CE%DE%AD%11%CB%80q%2C%3E%AB%9ES%CD%C6%EC%25%D2L%D2%EBd%B8%BF%8A%F5B%C6%18%F9%901CZ%9D%BE%24M%9C%8A9%F2%DAP%0B'%06w%82%EB%E6%E2%5C%2F%D7%07%9E%BB%CC%5D%E1%FA%B9%08%AD.r%23%8E%C2%17%F5E%7C!%F0%BE3%BE%3E_%B7o%88a%A7%DB%BE%D3d%EB%A31Z%EB%BB%D3%91%BA%A2%B1z%94%8F%DB'%F6%3D%8E%AA%13%19%B2%B1%BE%B1~V%08%2B%B4%A2cjJ%B3tO%00%03%25mN%97%F3%05%93%EF%11%84%0B%7C%88%AE-%89%8F%ABbW%90O%2B%0Ao%99%0C%5E%97%0CI%AFH%D9.%B0%3B%8F%ED%03%B6S%D6%5D%E6i_s9%F3*p%E9%1B%FD%C3%EB.7U%06%5E%19%C0%D1s.%17%A03u%E4%09%B0%7C%5E%2C%EB%15%DB%1F%3C%9E%B7%80%91%3B%DBc%AD%3Dma%BA%8B%3EV%AB%DBt.%5B%1E%01%BB%0F%AB%D5%9F%CF%AA%D5%DD%E7%E4%7F%0Bx%A3%FC%06%A9%23%0A%D6%C2%A1_2%00%00%00%09pHYs%00%00%0B%13%00%00%0B%13%01%00%9A%9C%18%00%00%00%B5IDAT(%15%A5%91%3D%0E%02!%10%85ac%E1%05%D6%CE%D6%C6%CE%D2%E8%ED%CD%DE%C0%C6%D6N.%E0V%F8%3D%9Ca%891XH%C2%BE%D9y%3F%90!%E6%9C%C3%BFk%E5%011%C6-%F5%C8N%04%DF%BD%FF%89%DFt%83DN%60%3E%F3%AB%A0%DE%1A%5Dg%BE%10Q%97%1B%40%9C%A8o%10%8F%5E%828%B4%1B%60%87%F6%02%26%85%1Ch%1E%C1%2B%5Bk%FF%86%EE%B7j%09%9A%DA%9B%ACe%A3%F9%EC%DA!9%B4%D5%A6%81%86%86%98%CC%3C%5B%40%FA%81%B3%E9%CB%23%94%C16Azo%05%D4%E1%C1%95a%3B%8A'%A0%E8%CC%17%22%85%1D%BA%00%A2%FA%DC%0A%94%D1%D1%8D%8B%3A%84%17B%C7%60%1A%25Z%FC%8D%00%00%00%00IEND%AEB%60%82\"),\n" + + " url(\"data:image/png,%89PNG%0D%0A%1A%0A%00%00%00%0DIHDR%00%00%00%05%00%00%007%08%06%00%00%00%C4%DD%80C%00%00%03%1EiCCPICC%20Profile%00%00x%01%85T%DFk%D3P%14%FE%DAe%9D%B0%E1%8B%3Ag%11%09%3Eh%91ndStC%9C%B6kW%BA%CDZ%EA6%B7!H%9B%A6m%5C%9A%C6%24%ED~%B0%07%D9%8Bo%3A%C5w%F1%07%3E%F9%07%0C%D9%83o%7B%92%0D%C6%14a%F8%AC%88%22L%F6%22%B3%9E%9B4M'S%03%B9%F7%BB%DF%F9%EE9'%E7%E4%5E%A0%F9qZ%D3%14%2F%0F%14USO%C5%C2%FC%C4%E4%14%DF%F2%01%5E%1CC%2B%FChM%8B%86%16J%26G%40%0F%D3%B2y%EF%B3%F3%0E%1E%C6lt%EEo%DF%AB%FEc%D5%9A%95%0C%11%F0%1C%20%BE%945%C4%22%E1Y%A0i%5C%D4t%13%E0%D6%89%EF%9D15%C2%CDLsX%A7%04%09%1Fg8oc%81%E1%8C%8D%23%96f45%40%9A%09%C2%07%C5B%3AK%B8%408%98i%E0%F3%0D%D8%CE%81%14%E4'%26%A9%92.%8B%3C%ABER%2F%E5dE%B2%0C%F6%F0%1Fs%83%F2_%B0%A8%94%E9%9B%AD%E7%10%8Dm%9A%19N%D1%7C%8A%DE%1F9%7Dp%8C%E6%00%D5%C1%3F_%18%BDA%B8%9DpX6%E3%A35~B%CD%24%AE%11%26%BD%E7%EEti%98%EDe%9A%97Y)%12%25%1C%24%BCbT%AE3li%E6%0B%03%89%9A%E6%D3%ED%F4P%92%B0%9F4%BF43Y%F3%E3%EDP%95%04%EB1%C5%F5%F6KF%F4%BA%BD%D7%DB%91%93%07%E35%3E%A7)%D6%7F%40%FE%BD%F7%F5r%8A%E5y%92%F0%EB%B4%1E%8D%D5%F4%5B%92%3AV%DB%DB%E4%CD%A6%23%C3%C4wQ%3F%03HB%82%8E%1Cd(%E0%91B%0Ca%9Ac%C4%AA%F8L%16%19%22J%A4%D2itTy%B28%D6%3B(%93%96%ED%1CGx%C9_%0E%B8%5E%16%F5%5B%B2%B8%F6%E0%FB%9E%DD%25%D7%8E%BC%15%85%C5%B7%A3%D8Q%ED%B5%81%E9%BA%B2%13%9A%1B%7Fua%A5%A3n%E17%B9%E5%9B%1Bm%AB%0B%08Q%FE%8A%E5%B1H%5Ee%CAO%82Q%D7u6%E6%90S%97%FCu%0B%CF2%94%EE%25v%12X%0C%BA%AC%F0%5E%F8*l%0AO%85%17%C2%97%BF%D4%C8%CE%DE%AD%11%CB%80q%2C%3E%AB%9ES%CD%C6%EC%25%D2L%D2%EBd%B8%BF%8A%F5B%C6%18%F9%901CZ%9D%BE%24M%9C%8A9%F2%DAP%0B'%06w%82%EB%E6%E2%5C%2F%D7%07%9E%BB%CC%5D%E1%FA%B9%08%AD.r%23%8E%C2%17%F5E%7C!%F0%BE3%BE%3E_%B7o%88a%A7%DB%BE%D3d%EB%A31Z%EB%BB%D3%91%BA%A2%B1z%94%8F%DB'%F6%3D%8E%AA%13%19%B2%B1%BE%B1~V%08%2B%B4%A2cjJ%B3tO%00%03%25mN%97%F3%05%93%EF%11%84%0B%7C%88%AE-%89%8F%ABbW%90O%2B%0Ao%99%0C%5E%97%0CI%AFH%D9.%B0%3B%8F%ED%03%B6S%D6%5D%E6i_s9%F3*p%E9%1B%FD%C3%EB.7U%06%5E%19%C0%D1s.%17%A03u%E4%09%B0%7C%5E%2C%EB%15%DB%1F%3C%9E%B7%80%91%3B%DBc%AD%3Dma%BA%8B%3EV%AB%DBt.%5B%1E%01%BB%0F%AB%D5%9F%CF%AA%D5%DD%E7%E4%7F%0Bx%A3%FC%06%A9%23%0A%D6%C2%A1_2%00%00%00%09pHYs%00%00%0B%13%00%00%0B%13%01%00%9A%9C%18%00%00%003IDAT8%11c%FC%FF%FF%7F%3E%03%1A%60%01%F2%3F%A3%891%80%04%FFQ%26%F8w%C0%B43%A1%DB%0C%E2%8F%0A%A2%85%CAh%80%8C%06%08%3C%04%E8%96%18%00%A3S%0D%CD%CF%D8%C1%9D%00%00%00%00IEND%AEB%60%82\");\n" + + " background-repeat: no-repeat, repeat-x;\n" + + " background-position: center center, top left;\n" + + "}\n" + + "\n" + + ".ace_dragging .ace_content {\n" + + " cursor: move;\n" + + "}\n" + + "\n" + + ".ace_folding-enabled > .ace_gutter-cell {\n" + + " padding-right: 13px;\n" + + "}\n" + + "\n" + + ".ace_fold-widget {\n" + + " box-sizing: border-box;\n" + + " -moz-box-sizing: border-box;\n" + + " -webkit-box-sizing: border-box;\n" + + "\n" + + " margin: 0 -12px 1px 1px;\n" + + " display: inline-block;\n" + + " height: 14px;\n" + + " width: 11px;\n" + + " vertical-align: text-bottom;\n" + + " \n" + + " background-image: url(\"data:image/png,%89PNG%0D%0A%1A%0A%00%00%00%0DIHDR%00%00%00%05%00%00%00%05%08%06%00%00%00%8Do%26%E5%00%00%004IDATx%DAe%8A%B1%0D%000%0C%C2%F2%2CK%96%BC%D0%8F9%81%88H%E9%D0%0E%96%C0%10%92%3E%02%80%5E%82%E4%A9*-%EEsw%C8%CC%11%EE%96w%D8%DC%E9*Eh%0C%151(%00%00%00%00IEND%AEB%60%82\");\n" + + " background-repeat: no-repeat;\n" + + " background-position: center 5px;\n" + + "\n" + + " border-radius: 3px;\n" + + "}\n" + + "\n" + + ".ace_fold-widget.end {\n" + + " background-image: url(\"data:image/png,%89PNG%0D%0A%1A%0A%00%00%00%0DIHDR%00%00%00%05%00%00%00%05%08%06%00%00%00%8Do%26%E5%00%00%004IDATx%DAm%C7%C1%09%000%08C%D1%8C%ECE%C8E(%8E%EC%02)%1EZJ%F1%C1'%04%07I%E1%E5%EE%CAL%F5%A2%99%99%22%E2%D6%1FU%B5%FE0%D9x%A7%26Wz5%0E%D5%00%00%00%00IEND%AEB%60%82\");\n" + + "}\n" + + "\n" + + ".ace_fold-widget.closed {\n" + + " background-image: url(\"data:image/png,%89PNG%0D%0A%1A%0A%00%00%00%0DIHDR%00%00%00%03%00%00%00%06%08%06%00%00%00%06%E5%24%0C%00%00%009IDATx%DA5%CA%C1%09%000%08%03%C0%AC*(%3E%04%C1%0D%BA%B1%23%A4Uh%E0%20%81%C0%CC%F8%82%81%AA%A2%AArGfr%88%08%11%11%1C%DD%7D%E0%EE%5B%F6%F6%CB%B8%05Q%2F%E9tai%D9%00%00%00%00IEND%AEB%60%82\");\n" + + "}\n" + + "\n" + + ".ace_fold-widget:hover {\n" + + " border: 1px solid rgba(0, 0, 0, 0.3);\n" + + " background-color: rgba(255, 255, 255, 0.2);\n" + + " -moz-box-shadow:inset 0 1px 1px rgba(255, 255, 255, 0.7);\n" + + " -moz-box-shadow: 0 1px 1px rgba(255, 255, 255, 0.7);\n" + + " -webkit-box-shadow:inset 0 1px 1px rgba(255, 255, 255, 0.7);\n" + + " -webkit-box-shadow: 0 1px 1px rgba(255, 255, 255, 0.7);\n" + + " box-shadow:inset 0 1px 1px rgba(255, 255, 255, 0.7);\n" + + " box-shadow: 0 1px 1px rgba(255, 255, 255, 0.7);\n" + + " background-position: center 4px;\n" + + "}\n" + + "\n" + + ".ace_fold-widget:active {\n" + + " border: 1px solid rgba(0, 0, 0, 0.4);\n" + + " background-color: rgba(0, 0, 0, 0.05);\n" + + " -moz-box-shadow:inset 0 1px 1px rgba(255, 255, 255);\n" + + " -moz-box-shadow: 0 1px 1px rgba(255, 255, 255, 0.8);\n" + + " -webkit-box-shadow:inset 0 1px 1px rgba(255, 255, 255);\n" + + " -webkit-box-shadow: 0 1px 1px rgba(255, 255, 255, 0.8);\n" + + " box-shadow:inset 0 1px 1px rgba(255, 255, 255);\n" + + " box-shadow: 0 1px 1px rgba(255, 255, 255, 0.8);\n" + + "}\n" + + "\n" + + ".ace_fold-widget.invalid {\n" + + " background-color: #FFB4B4;\n" + + " border-color: #DE5555;\n" + + "}\n" + + "\n" + + ".ace_fade-fold-widgets .ace_fold-widget {\n" + + " -moz-transition: 0.5s opacity;\n" + + " -webkit-transition: 0.5s opacity;\n" + + " -o-transition: 0.5s opacity;\n" + + " -ms-transition: 0.5s opacity;\n" + + " transition: 0.5s opacity;\n" + + " opacity: 0;\n" + + "}\n" + + ".ace_fade-fold-widgets:hover .ace_fold-widget {\n" + + " -moz-transition-duration: 0.05s;\n" + + " -webkit-transition-duration: 0.05s;\n" + + " -o-transition-duration: 0.05s;\n" + + " -ms-transition-duration: 0.05s;\n" + + " transition-duration: 0.05s;\n" + + " -moz-transition-delay: 0.2s;\n" + + " -webkit-transition-delay: 0.2s;\n" + + " -o-transition-delay: 0.2s;\n" + + " -ms-transition-delay: 0.2s;\n" + + " transition-delay: 0.2s; \n" + + " opacity:1;\n" + + "}\n" + + ""); + +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Harutyun Amirjanyan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/multi_select', ['require', 'exports', 'module' , 'ace/range_list', 'ace/range', 'ace/selection', 'ace/mouse/multi_select_handler', 'ace/lib/event', 'ace/commands/multi_select_commands', 'ace/search', 'ace/edit_session', 'ace/editor'], function(require, exports, module) { + +var RangeList = require("./range_list").RangeList; +var Range = require("./range").Range; +var Selection = require("./selection").Selection; +var onMouseDown = require("./mouse/multi_select_handler").onMouseDown; +var event = require("./lib/event"); +exports.commands = require("./commands/multi_select_commands"); + +// Todo: session.find or editor.findVolatile that returns range +var Search = require("./search").Search; +var search = new Search(); + +function find(session, needle, dir) { + search.$options.wrap = true; + search.$options.needle = needle; + search.$options.backwards = dir == -1; + return search.find(session); +} + +// extend EditSession +var EditSession = require("./edit_session").EditSession; +(function() { + this.getSelectionMarkers = function() { + return this.$selectionMarkers; + }; +}).call(EditSession.prototype); + +// extend Selection +(function() { + // list of ranges in reverse addition order + this.ranges = null; + + // automatically sorted list of ranges + this.rangeList = null; + + /** extension + * Selection.addRange(range, $blockChangeEvents) + * - range (Range): The new range to add + * - $blockChangeEvents (Boolean): Whether or not to block changing events + * + * Adds a range to a selection by entering multiselect mode, if necessary. + **/ + this.addRange = function(range, $blockChangeEvents) { + if (!range) + return; + + if (!this.inMultiSelectMode && this.rangeCount == 0) { + var oldRange = this.toOrientedRange(); + if (range.intersects(oldRange)) + return $blockChangeEvents || this.fromOrientedRange(range); + + this.rangeList.add(oldRange); + this.$onAddRange(oldRange); + } + + if (!range.cursor) + range.cursor = range.end; + + var removed = this.rangeList.add(range); + + this.$onAddRange(range); + + if (removed.length) + this.$onRemoveRange(removed); + + if (this.rangeCount > 1 && !this.inMultiSelectMode) { + this._emit("multiSelect"); + this.inMultiSelectMode = true; + this.session.$undoSelect = false; + this.rangeList.attach(this.session); + } + + return $blockChangeEvents || this.fromOrientedRange(range); + }; + + this.toSingleRange = function(range) { + range = range || this.ranges[0]; + var removed = this.rangeList.removeAll(); + if (removed.length) + this.$onRemoveRange(removed); + + range && this.fromOrientedRange(range); + }; + + /** extension + * Selection.substractPoint(pos) -> Range + * - pos (Range): The position to remove, as a `{row, column}` object + * + * Removes a Range containing pos (if it exists). + **/ + this.substractPoint = function(pos) { + var removed = this.rangeList.substractPoint(pos); + if (removed) { + this.$onRemoveRange(removed); + return removed[0]; + } + }; + + /** extension + * Selection.mergeOverlappingRanges() + * + * Merges overlapping ranges ensuring consistency after changes + **/ + this.mergeOverlappingRanges = function() { + var removed = this.rangeList.merge(); + if (removed.length) + this.$onRemoveRange(removed); + else if(this.ranges[0]) + this.fromOrientedRange(this.ranges[0]); + }; + + this.$onAddRange = function(range) { + this.rangeCount = this.rangeList.ranges.length; + this.ranges.unshift(range); + this._emit("addRange", {range: range}); + }; + + this.$onRemoveRange = function(removed) { + this.rangeCount = this.rangeList.ranges.length; + if (this.rangeCount == 1 && this.inMultiSelectMode) { + var lastRange = this.rangeList.ranges.pop(); + removed.push(lastRange); + this.rangeCount = 0; + } + + for (var i = removed.length; i--; ) { + var index = this.ranges.indexOf(removed[i]); + this.ranges.splice(index, 1); + } + + this._emit("removeRange", {ranges: removed}); + + if (this.rangeCount == 0 && this.inMultiSelectMode) { + this.inMultiSelectMode = false; + this._emit("singleSelect"); + this.session.$undoSelect = true; + this.rangeList.detach(this.session); + } + + lastRange = lastRange || this.ranges[0]; + if (lastRange && !lastRange.isEqual(this.getRange())) + this.fromOrientedRange(lastRange); + }; + + // adds multicursor support to selection + this.$initRangeList = function() { + if (this.rangeList) + return; + + this.rangeList = new RangeList(); + this.ranges = []; + this.rangeCount = 0; + }; + + this.getAllRanges = function() { + return this.rangeList.ranges.concat(); + }; + + this.splitIntoLines = function () { + if (this.rangeCount > 1) { + var ranges = this.rangeList.ranges; + var lastRange = ranges[ranges.length - 1]; + var range = Range.fromPoints(ranges[0].start, lastRange.end); + + this.toSingleRange(); + this.setSelectionRange(range, lastRange.cursor == lastRange.start); + } else { + var range = this.getRange(); + var startRow = range.start.row; + var endRow = range.end.row; + if (startRow == endRow) + return; + + var rectSel = []; + var r = this.getLineRange(startRow, true); + r.start.column = range.start.column; + rectSel.push(r); + + for (var i = startRow + 1; i < endRow; i++) + rectSel.push(this.getLineRange(i, true)); + + r = this.getLineRange(endRow, true); + r.end.column = range.end.column; + rectSel.push(r); + + rectSel.forEach(this.addRange, this); + } + }; + + this.toggleBlockSelection = function () { + if (this.rangeCount > 1) { + var ranges = this.rangeList.ranges; + var lastRange = ranges[ranges.length - 1]; + var range = Range.fromPoints(ranges[0].start, lastRange.end); + + this.toSingleRange(); + this.setSelectionRange(range, lastRange.cursor == lastRange.start); + } else { + var cursor = this.session.documentToScreenPosition(this.selectionLead); + var anchor = this.session.documentToScreenPosition(this.selectionAnchor); + + var rectSel = this.rectangularRangeBlock(cursor, anchor); + rectSel.forEach(this.addRange, this); + } + }; + + /** extension + * Selection.rectangularRangeBlock(screenCursor, screenAnchor, includeEmptyLines) -> Range + * - screenCursor (Cursor): The cursor to use + * - screenAnchor (Anchor): The anchor to use + * - includeEmptyLins (Boolean): If true, this includes ranges inside the block which are empty due to clipping + * + * Gets list of ranges composing rectangular block on the screen + * + */ + this.rectangularRangeBlock = function(screenCursor, screenAnchor, includeEmptyLines) { + var rectSel = []; + + var xBackwards = screenCursor.column < screenAnchor.column; + if (xBackwards) { + var startColumn = screenCursor.column; + var endColumn = screenAnchor.column; + } else { + var startColumn = screenAnchor.column; + var endColumn = screenCursor.column; + } + + var yBackwards = screenCursor.row < screenAnchor.row; + if (yBackwards) { + var startRow = screenCursor.row; + var endRow = screenAnchor.row; + } else { + var startRow = screenAnchor.row; + var endRow = screenCursor.row; + } + + if (startColumn < 0) + startColumn = 0; + if (startRow < 0) + startRow = 0; + + if (startRow == endRow) + includeEmptyLines = true; + + for (var row = startRow; row <= endRow; row++) { + var range = Range.fromPoints( + this.session.screenToDocumentPosition(row, startColumn), + this.session.screenToDocumentPosition(row, endColumn) + ); + if (range.isEmpty()) { + if (docEnd && isSamePoint(range.end, docEnd)) + break; + var docEnd = range.end; + } + range.cursor = xBackwards ? range.start : range.end; + rectSel.push(range); + } + + if (yBackwards) + rectSel.reverse(); + + if (!includeEmptyLines) { + var end = rectSel.length - 1; + while (rectSel[end].isEmpty() && end > 0) + end--; + if (end > 0) { + var start = 0; + while (rectSel[start].isEmpty()) + start++; + } + for (var i = end; i >= start; i--) { + if (rectSel[i].isEmpty()) + rectSel.splice(i, 1); + } + } + + return rectSel; + }; +}).call(Selection.prototype); + +// extend Editor +var Editor = require("./editor").Editor; +(function() { + + /** extension + * Editor.updateSelectionMarkers() + * + * Updates the cursor and marker layers. + **/ + this.updateSelectionMarkers = function() { + this.renderer.updateCursor(); + this.renderer.updateBackMarkers(); + }; + + /** extension + * Editor.addSelectionMarker(orientedRange) -> Range + * - orientedRange (Range): A range containing a cursor + * + * Adds the selection and cursor. + **/ + this.addSelectionMarker = function(orientedRange) { + if (!orientedRange.cursor) + orientedRange.cursor = orientedRange.end; + + var style = this.getSelectionStyle(); + orientedRange.marker = this.session.addMarker(orientedRange, "ace_selection", style); + + this.session.$selectionMarkers.push(orientedRange); + this.session.selectionMarkerCount = this.session.$selectionMarkers.length; + return orientedRange; + }; + + /** extension + * Editor.removeSelectionMarker(range) + * - range (Range): The selection range added with [[Editor.addSelectionMarker `addSelectionMarker()`]]. + * + * Removes the selection marker. + **/ + this.removeSelectionMarker = function(range) { + if (!range.marker) + return; + this.session.removeMarker(range.marker); + var index = this.session.$selectionMarkers.indexOf(range); + if (index != -1) + this.session.$selectionMarkers.splice(index, 1); + this.session.selectionMarkerCount = this.session.$selectionMarkers.length; + }; + + this.removeSelectionMarkers = function(ranges) { + var markerList = this.session.$selectionMarkers; + for (var i = ranges.length; i--; ) { + var range = ranges[i]; + if (!range.marker) + continue; + this.session.removeMarker(range.marker); + var index = markerList.indexOf(range); + if (index != -1) + markerList.splice(index, 1); + } + this.session.selectionMarkerCount = markerList.length; + }; + + this.$onAddRange = function(e) { + this.addSelectionMarker(e.range); + this.renderer.updateCursor(); + this.renderer.updateBackMarkers(); + }; + + this.$onRemoveRange = function(e) { + this.removeSelectionMarkers(e.ranges); + this.renderer.updateCursor(); + this.renderer.updateBackMarkers(); + }; + + this.$onMultiSelect = function(e) { + if (this.inMultiSelectMode) + return; + this.inMultiSelectMode = true; + + this.setStyle("multiselect"); + this.keyBinding.addKeyboardHandler(exports.commands.keyboardHandler); + this.commands.on("exec", this.$onMultiSelectExec); + + this.renderer.updateCursor(); + this.renderer.updateBackMarkers(); + }; + + this.$onSingleSelect = function(e) { + if (this.session.multiSelect.inVirtualMode) + return; + this.inMultiSelectMode = false; + + this.unsetStyle("multiselect"); + this.keyBinding.removeKeyboardHandler(exports.commands.keyboardHandler); + + this.commands.removeEventListener("exec", this.$onMultiSelectExec); + this.renderer.updateCursor(); + this.renderer.updateBackMarkers(); + }; + + this.$onMultiSelectExec = function(e) { + var command = e.command; + var editor = e.editor; + if (!command.multiSelectAction) { + command.exec(editor, e.args || {}); + editor.multiSelect.addRange(editor.multiSelect.toOrientedRange()); + editor.multiSelect.mergeOverlappingRanges(); + } else if (command.multiSelectAction == "forEach") { + editor.forEachSelection(command, e.args); + } else if (command.multiSelectAction == "single") { + editor.exitMultiSelectMode(); + command.exec(editor, e.args || {}); + } else { + command.multiSelectAction(editor, e.args || {}); + } + e.preventDefault(); + }; + + /** extension + * Editor.forEachSelection(cmd, args) + * - cmd (String): The command to execute + * - args (String): Any arguments for the command + * + * Executes a command for each selection range. + **/ + this.forEachSelection = function(cmd, args) { + if (this.inVirtualSelectionMode) + return; + + var session = this.session; + var selection = this.selection; + var rangeList = selection.rangeList; + + var reg = selection._eventRegistry; + selection._eventRegistry = {}; + + var tmpSel = new Selection(session); + this.inVirtualSelectionMode = true; + for (var i = rangeList.ranges.length; i--;) { + tmpSel.fromOrientedRange(rangeList.ranges[i]); + this.selection = session.selection = tmpSel; + cmd.exec(this, args || {}); + tmpSel.toOrientedRange(rangeList.ranges[i]); + } + tmpSel.detach(); + + this.selection = session.selection = selection; + this.inVirtualSelectionMode = false; + selection._eventRegistry = reg; + selection.mergeOverlappingRanges(); + + this.onCursorChange(); + this.onSelectionChange(); + }; + + /** extension + * Editor.exitMultiSelectMode() -> Void + * + * Removes all the selections except the last added one. + **/ + this.exitMultiSelectMode = function() { + if (this.inVirtualSelectionMode) + return; + this.multiSelect.toSingleRange(); + }; + + this.getCopyText = function() { + var text = ""; + if (this.inMultiSelectMode) { + var ranges = this.multiSelect.rangeList.ranges; + text = []; + for (var i = 0; i < ranges.length; i++) { + text.push(this.session.getTextRange(ranges[i])); + } + text = text.join(this.session.getDocument().getNewLineCharacter()); + } else if (!this.selection.isEmpty()) { + text = this.session.getTextRange(this.getSelectionRange()); + } + + return text; + }; + + this.onPaste = function(text) { + this._emit("paste", text); + if (!this.inMultiSelectMode) + return this.insert(text); + + var lines = text.split(this.session.getDocument().getNewLineCharacter()); + var ranges = this.selection.rangeList.ranges; + + if (lines.length > ranges.length) { + this.commands.exec("insertstring", this, text); + return; + } + + for (var i = ranges.length; i--; ) { + var range = ranges[i]; + if (!range.isEmpty()) + this.session.remove(range); + + this.session.insert(range.start, lines[i]); + } + }; + + /** extension + * Editor.findAll(dir, options) -> Number + * - needle: text to find + * - options: search options + * - additive: keeps + * + * Finds and selects all the occurences of `needle`. + **/ + this.findAll = function(needle, options, additive) { + options = options || {}; + options.needle = needle || options.needle; + this.$search.set(options); + + var ranges = this.$search.findAll(this.session); + if (!ranges.length) + return 0; + + this.$blockScrolling += 1; + var selection = this.multiSelect; + + if (!additive) + selection.toSingleRange(ranges[0]); + + for (var i = ranges.length; i--; ) + selection.addRange(ranges[i], true); + + this.$blockScrolling -= 1; + + return ranges.length; + }; + + // commands + /** extension + * Editor.selectMoreLines(dir, skip) + * - dir (Number): The direction of lines to select: -1 for up, 1 for down + * - skip (Boolean): If `true`, removes the active selection range + * + * Adds a cursor above or below the active cursor. + **/ + this.selectMoreLines = function(dir, skip) { + var range = this.selection.toOrientedRange(); + var isBackwards = range.cursor == range.end; + + var screenLead = this.session.documentToScreenPosition(range.cursor); + if (this.selection.$desiredColumn) + screenLead.column = this.selection.$desiredColumn; + + var lead = this.session.screenToDocumentPosition(screenLead.row + dir, screenLead.column); + + if (!range.isEmpty()) { + var screenAnchor = this.session.documentToScreenPosition(isBackwards ? range.end : range.start); + var anchor = this.session.screenToDocumentPosition(screenAnchor.row + dir, screenAnchor.column); + } else { + var anchor = lead; + } + + if (isBackwards) { + var newRange = Range.fromPoints(lead, anchor); + newRange.cursor = newRange.start; + } else { + var newRange = Range.fromPoints(anchor, lead); + newRange.cursor = newRange.end; + } + + newRange.desiredColumn = screenLead.column; + if (!this.selection.inMultiSelectMode) { + this.selection.addRange(range); + } else { + if (skip) + var toRemove = range.cursor; + } + + this.selection.addRange(newRange); + if (toRemove) + this.selection.substractPoint(toRemove); + }; + + /** extension + * Editor.transposeSelections(dir) + * - dir (Number): The direction to rotate selections + * + * Transposes the selected ranges. + **/ + this.transposeSelections = function(dir) { + var session = this.session; + var sel = session.multiSelect; + var all = sel.ranges; + + for (var i = all.length; i--; ) { + var range = all[i]; + if (range.isEmpty()) { + var tmp = session.getWordRange(range.start.row, range.start.column); + range.start.row = tmp.start.row; + range.start.column = tmp.start.column; + range.end.row = tmp.end.row; + range.end.column = tmp.end.column; + } + } + sel.mergeOverlappingRanges(); + + var words = []; + for (var i = all.length; i--; ) { + var range = all[i]; + words.unshift(session.getTextRange(range)); + } + + if (dir < 0) + words.unshift(words.pop()); + else + words.push(words.shift()); + + for (var i = all.length; i--; ) { + var range = all[i]; + var tmp = range.clone(); + session.replace(range, words[i]); + range.start.row = tmp.start.row; + range.start.column = tmp.start.column; + } + } + + /** extension + * Editor.selectMore(dir, skip) + * - dir (Number): The direction of lines to select: -1 for up, 1 for down + * - skip (Boolean): If `true`, removes the active selection range + * + * Finds the next occurence of text in an active selection and adds it to the selections. + **/ + this.selectMore = function (dir, skip) { + var session = this.session; + var sel = session.multiSelect; + + var range = sel.toOrientedRange(); + if (range.isEmpty()) { + var range = session.getWordRange(range.start.row, range.start.column); + range.cursor = range.end; + this.multiSelect.addRange(range); + } + var needle = session.getTextRange(range); + + var newRange = find(session, needle, dir); + if (newRange) { + newRange.cursor = dir == -1 ? newRange.start : newRange.end; + this.multiSelect.addRange(newRange); + } + if (skip) + this.multiSelect.substractPoint(range.cursor); + }; +}).call(Editor.prototype); + + +function isSamePoint(p1, p2) { + return p1.row == p2.row && p1.column == p2.column; +} + +// patch +// adds multicursor support to a session +exports.onSessionChange = function(e) { + var session = e.session; + if (!session.multiSelect) { + session.$selectionMarkers = []; + session.selection.$initRangeList(); + session.multiSelect = session.selection; + } + this.multiSelect = session.multiSelect; + + var oldSession = e.oldSession; + if (oldSession) { + // todo use events + if (oldSession.multiSelect && oldSession.multiSelect.editor == this) + oldSession.multiSelect.editor = null; + + session.multiSelect.removeEventListener("addRange", this.$onAddRange); + session.multiSelect.removeEventListener("removeRange", this.$onRemoveRange); + session.multiSelect.removeEventListener("multiSelect", this.$onMultiSelect); + session.multiSelect.removeEventListener("singleSelect", this.$onSingleSelect); + } + + session.multiSelect.on("addRange", this.$onAddRange); + session.multiSelect.on("removeRange", this.$onRemoveRange); + session.multiSelect.on("multiSelect", this.$onMultiSelect); + session.multiSelect.on("singleSelect", this.$onSingleSelect); + + // this.$onSelectionChange = this.onSelectionChange.bind(this); + + if (this.inMultiSelectMode != session.selection.inMultiSelectMode) { + if (session.selection.inMultiSelectMode) + this.$onMultiSelect(); + else + this.$onSingleSelect(); + } +}; + +// MultiSelect(editor) +// adds multiple selection support to the editor +// (note: should be called only once for each editor instance) +function MultiSelect(editor) { + editor.$onAddRange = editor.$onAddRange.bind(editor); + editor.$onRemoveRange = editor.$onRemoveRange.bind(editor); + editor.$onMultiSelect = editor.$onMultiSelect.bind(editor); + editor.$onSingleSelect = editor.$onSingleSelect.bind(editor); + + exports.onSessionChange.call(editor, editor); + editor.on("changeSession", exports.onSessionChange.bind(editor)); + + editor.on("mousedown", onMouseDown); + editor.commands.addCommands(exports.commands.defaultCommands); + + addAltCursorListeners(editor); +} + +function addAltCursorListeners(editor){ + var el = editor.textInput.getElement(); + var altCursor = false; + var contentEl = editor.renderer.content; + event.addListener(el, "keydown", function(e) { + if (e.keyCode == 18 && !(e.ctrlKey || e.shiftKey || e.metaKey)) { + if (!altCursor) { + contentEl.style.cursor = "crosshair"; + altCursor = true; + } + } else if (altCursor) { + contentEl.style.cursor = ""; + } + }); + + event.addListener(el, "keyup", reset); + event.addListener(el, "blur", reset); + function reset() { + if (altCursor) { + contentEl.style.cursor = ""; + altCursor = false; + } + } +} + +exports.MultiSelect = MultiSelect; + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Harutyun Amirjanyan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/range_list', ['require', 'exports', 'module' ], function(require, exports, module) { +"use strict"; + + +var RangeList = function() { + this.ranges = []; +}; + +(function() { + this.comparePoints = function(p1, p2) { + return p1.row - p2.row || p1.column - p2.column; + }; + + this.pointIndex = function(pos, startIndex) { + var list = this.ranges; + + for (var i = startIndex || 0; i < list.length; i++) { + var range = list[i]; + var cmp = this.comparePoints(pos, range.end); + + if (cmp > 0) + continue; + if (cmp == 0) + return i; + cmp = this.comparePoints(pos, range.start); + if (cmp >= 0) + return i; + + return -i-1; + } + return -i - 1; + }; + + this.add = function(range) { + var startIndex = this.pointIndex(range.start); + if (startIndex < 0) + startIndex = -startIndex - 1; + + var endIndex = this.pointIndex(range.end, startIndex); + + if (endIndex < 0) + endIndex = -endIndex - 1; + else + endIndex++; + + return this.ranges.splice(startIndex, endIndex - startIndex, range); + }; + + this.addList = function(list) { + var removed = []; + for (var i = list.length; i--; ) { + removed.push.call(removed, this.add(list[i])); + } + return removed; + }; + + this.substractPoint = function(pos) { + var i = this.pointIndex(pos); + + if (i >= 0) + return this.ranges.splice(i, 1); + }; + + // merge overlapping ranges + this.merge = function() { + var removed = []; + var list = this.ranges; + var next = list[0], range; + for (var i = 1; i < list.length; i++) { + range = next; + next = list[i]; + var cmp = this.comparePoints(range.end, next.start); + if (cmp < 0) + continue; + + if (cmp == 0 && !(range.isEmpty() || next.isEmpty())) + continue; + + if (this.comparePoints(range.end, next.end) < 0) { + range.end.row = next.end.row; + range.end.column = next.end.column; + } + + list.splice(i, 1); + removed.push(next); + next = range; + i--; + } + + return removed; + }; + + this.contains = function(row, column) { + return this.pointIndex({row: row, column: column}) >= 0; + }; + + this.containsPoint = function(pos) { + return this.pointIndex(pos) >= 0; + }; + + this.rangeAtPoint = function(pos) { + var i = this.pointIndex(pos); + if (i >= 0) + return this.ranges[i]; + }; + + + this.clipRows = function(startRow, endRow) { + var list = this.ranges; + if (list[0].start.row > endRow || list[list.length - 1].start.row < startRow) + return []; + + var startIndex = this.pointIndex({row: startRow, column: 0}); + if (startIndex < 0) + startIndex = -startIndex - 1; + var endIndex = this.pointIndex({row: endRow, column: 0}, startIndex); + if (endIndex < 0) + endIndex = -endIndex - 1; + + var clipped = []; + for (var i = startIndex; i < endIndex; i++) { + clipped.push(list[i]); + } + return clipped; + }; + + this.removeAll = function() { + return this.ranges.splice(0, this.ranges.length); + }; + + this.attach = function(session) { + if (this.session) + this.detach(); + + this.session = session; + this.onChange = this.$onChange.bind(this); + + this.session.on('change', this.onChange); + }; + + this.detach = function() { + if (!this.session) + return; + this.session.removeListener('change', this.onChange); + this.session = null; + }; + + this.$onChange = function(e) { + var changeRange = e.data.range; + if (e.data.action[0] == "i"){ + var start = changeRange.start; + var end = changeRange.end; + } else { + var end = changeRange.start; + var start = changeRange.end; + } + var startRow = start.row; + var endRow = end.row; + var lineDif = endRow - startRow; + + var colDiff = -start.column + end.column; + + var ranges = this.ranges; + + for (var i=0, n = ranges.length; i < n; i++) { + var r = ranges[i]; + if (r.end.row < startRow) + continue; + if (r.start.row > startRow) + break; + + if (r.start.row == startRow && r.start.column >= start.column ) { + r.start.column += colDiff; + r.start.row += lineDif; + } + if (r.end.row == startRow && r.end.column >= start.column) { + r.end.column += colDiff; + r.end.row += lineDif; + } + } + + if (lineDif != 0 && i < n) { + for (; i < n; i++) { + var r = ranges[i]; + r.start.row += lineDif; + r.end.row += lineDif; + } + } + }; + +}).call(RangeList.prototype); + +exports.RangeList = RangeList; +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Harutyun Amirjanyan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mouse/multi_select_handler', ['require', 'exports', 'module' , 'ace/lib/event'], function(require, exports, module) { + +var event = require("../lib/event"); + + +// mouse +function isSamePoint(p1, p2) { + return p1.row == p2.row && p1.column == p2.column; +} + +function onMouseDown(e) { + var ev = e.domEvent; + var alt = ev.altKey; + var shift = ev.shiftKey; + var ctrl = e.getAccelKey(); + var button = e.getButton(); + + if (!ctrl && !alt) { + if (e.editor.inMultiSelectMode) { + if (button == 0) { + e.editor.exitMultiSelectMode(); + } else if (button == 2) { + var editor = e.editor; + var selectionEmpty = editor.selection.isEmpty(); + editor.textInput.onContextMenu({x: e.clientX, y: e.clientY}, selectionEmpty); + event.capture(editor.container, function(){}, editor.textInput.onContextMenuClose); + e.stop(); + } + } + return; + } + + var editor = e.editor; + var selection = editor.selection; + var isMultiSelect = editor.inMultiSelectMode; + var pos = e.getDocumentPosition(); + var cursor = selection.getCursor(); + var inSelection = e.inSelection() || (selection.isEmpty() && isSamePoint(pos, cursor)); + + + var mouseX = e.x, mouseY = e.y; + var onMouseSelection = function(e) { + mouseX = e.clientX; + mouseY = e.clientY; + }; + + var blockSelect = function() { + var newCursor = editor.renderer.pixelToScreenCoordinates(mouseX, mouseY); + var cursor = session.screenToDocumentPosition(newCursor.row, newCursor.column); + + if (isSamePoint(screenCursor, newCursor) + && isSamePoint(cursor, selection.selectionLead)) + return; + screenCursor = newCursor; + + editor.selection.moveCursorToPosition(cursor); + editor.selection.clearSelection(); + editor.renderer.scrollCursorIntoView(); + + editor.removeSelectionMarkers(rectSel); + rectSel = selection.rectangularRangeBlock(screenCursor, screenAnchor); + rectSel.forEach(editor.addSelectionMarker, editor); + editor.updateSelectionMarkers(); + }; + + var session = editor.session; + var screenAnchor = editor.renderer.pixelToScreenCoordinates(mouseX, mouseY); + var screenCursor = screenAnchor; + + + + if (ctrl && !shift && !alt && button == 0) { + if (!isMultiSelect && inSelection) + return; // dragging + + if (!isMultiSelect) { + var range = selection.toOrientedRange(); + editor.addSelectionMarker(range); + } + + var oldRange = selection.rangeList.rangeAtPoint(pos); + + event.capture(editor.container, function(){}, function() { + var tmpSel = selection.toOrientedRange(); + + if (oldRange && tmpSel.isEmpty() && isSamePoint(oldRange.cursor, tmpSel.cursor)) + selection.substractPoint(tmpSel.cursor); + else { + if (range) { + editor.removeSelectionMarker(range); + selection.addRange(range); + } + selection.addRange(tmpSel); + } + }); + + } else if (!shift && alt && button == 0) { + e.stop(); + + if (isMultiSelect && !ctrl) + selection.toSingleRange(); + else if (!isMultiSelect && ctrl) + selection.addRange(); + + selection.moveCursorToPosition(pos); + selection.clearSelection(); + + var rectSel = []; + + var onMouseSelectionEnd = function(e) { + clearInterval(timerId); + editor.removeSelectionMarkers(rectSel); + for (var i = 0; i < rectSel.length; i++) + selection.addRange(rectSel[i]); + }; + + var onSelectionInterval = blockSelect; + + event.capture(editor.container, onMouseSelection, onMouseSelectionEnd); + var timerId = setInterval(function() {onSelectionInterval();}, 20); + + return e.preventDefault(); + } +} + + +exports.onMouseDown = onMouseDown; + +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Harutyun Amirjanyan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/commands/multi_select_commands', ['require', 'exports', 'module' , 'ace/keyboard/hash_handler'], function(require, exports, module) { + +// commands to enter multiselect mode +exports.defaultCommands = [{ + name: "addCursorAbove", + exec: function(editor) { editor.selectMoreLines(-1); }, + bindKey: {win: "Ctrl-Alt-Up", mac: "Ctrl-Alt-Up"}, + readonly: true +}, { + name: "addCursorBelow", + exec: function(editor) { editor.selectMoreLines(1); }, + bindKey: {win: "Ctrl-Alt-Down", mac: "Ctrl-Alt-Down"}, + readonly: true +}, { + name: "addCursorAboveSkipCurrent", + exec: function(editor) { editor.selectMoreLines(-1, true); }, + bindKey: {win: "Ctrl-Alt-Shift-Up", mac: "Ctrl-Alt-Shift-Up"}, + readonly: true +}, { + name: "addCursorBelowSkipCurrent", + exec: function(editor) { editor.selectMoreLines(1, true); }, + bindKey: {win: "Ctrl-Alt-Shift-Down", mac: "Ctrl-Alt-Shift-Down"}, + readonly: true +}, { + name: "selectMoreBefore", + exec: function(editor) { editor.selectMore(-1); }, + bindKey: {win: "Ctrl-Alt-Left", mac: "Ctrl-Alt-Left"}, + readonly: true +}, { + name: "selectMoreAfter", + exec: function(editor) { editor.selectMore(1); }, + bindKey: {win: "Ctrl-Alt-Right", mac: "Ctrl-Alt-Right"}, + readonly: true +}, { + name: "selectNextBefore", + exec: function(editor) { editor.selectMore(-1, true); }, + bindKey: {win: "Ctrl-Alt-Shift-Left", mac: "Ctrl-Alt-Shift-Left"}, + readonly: true +}, { + name: "selectNextAfter", + exec: function(editor) { editor.selectMore(1, true); }, + bindKey: {win: "Ctrl-Alt-Shift-Right", mac: "Ctrl-Alt-Shift-Right"}, + readonly: true +}, { + name: "splitIntoLines", + exec: function(editor) { editor.multiSelect.splitIntoLines(); }, + bindKey: {win: "Ctrl-Shift-L", mac: "Ctrl-Shift-L"}, + readonly: true +}, { + name: "singleSelection", + bindKey: "esc", + exec: function(editor) { editor.exitMultiSelectMode(); }, + readonly: true, + isAvailable: function(editor) {return editor.inMultiSelectMode} +}]; + +// commands active in multiselect mode +exports.multiEditCommands = {"singleSelection": "esc"}; + +var HashHandler = require("../keyboard/hash_handler").HashHandler; +exports.keyboardHandler = new HashHandler(exports.multiEditCommands); + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/worker/worker_client', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/lib/event_emitter', 'ace/config'], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var EventEmitter = require("../lib/event_emitter").EventEmitter; +var config = require("../config"); + +var WorkerClient = function(topLevelNamespaces, packagedJs, mod, classname) { + + this.changeListener = this.changeListener.bind(this); + + if (config.get("packaged")) { + this.$worker = new Worker(config.get("workerPath") + "/" + packagedJs); + } + else { + var workerUrl = this.$normalizePath(require.nameToUrl("ace/worker/worker", null, "_")); + this.$worker = new Worker(workerUrl); + + var tlns = {}; + for (var i=0; i + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +ace.define('ace/placeholder', ['require', 'exports', 'module' , 'ace/range', 'ace/lib/event_emitter', 'ace/lib/oop'], function(require, exports, module) { +"use strict"; + +var Range = require('./range').Range; +var EventEmitter = require("./lib/event_emitter").EventEmitter; +var oop = require("./lib/oop"); + +/** + * class PlaceHolder + * + * TODO + * + **/ + +/** + * new PlaceHolder(session, length, pos, others, mainClass, othersClass) + * - session (Document): The document to associate with the anchor + * - length (Number): The starting row position + * - pos (Number): The starting column position + * - others (String): + * - mainClass (String): + * - othersClass (String): + * + * TODO + * + **/ + +var PlaceHolder = function(session, length, pos, others, mainClass, othersClass) { + var _self = this; + this.length = length; + this.session = session; + this.doc = session.getDocument(); + this.mainClass = mainClass; + this.othersClass = othersClass; + this.$onUpdate = this.onUpdate.bind(this); + this.doc.on("change", this.$onUpdate); + this.$others = others; + + this.$onCursorChange = function() { + setTimeout(function() { + _self.onCursorChange(); + }); + }; + + this.$pos = pos; + // Used for reset + var undoStack = session.getUndoManager().$undoStack || session.getUndoManager().$undostack || {length: -1}; + this.$undoStackDepth = undoStack.length; + this.setup(); + + session.selection.on("changeCursor", this.$onCursorChange); +}; + +(function() { + + oop.implement(this, EventEmitter); + + /** + * PlaceHolder.setup() + * + * TODO + * + **/ + this.setup = function() { + var _self = this; + var doc = this.doc; + var session = this.session; + var pos = this.$pos; + + this.pos = doc.createAnchor(pos.row, pos.column); + this.markerId = session.addMarker(new Range(pos.row, pos.column, pos.row, pos.column + this.length), this.mainClass, null, false); + this.pos.on("change", function(event) { + session.removeMarker(_self.markerId); + _self.markerId = session.addMarker(new Range(event.value.row, event.value.column, event.value.row, event.value.column+_self.length), _self.mainClass, null, false); + }); + this.others = []; + this.$others.forEach(function(other) { + var anchor = doc.createAnchor(other.row, other.column); + _self.others.push(anchor); + }); + session.setUndoSelect(false); + }; + + /** + * PlaceHolder.showOtherMarkers() + * + * TODO + * + **/ + this.showOtherMarkers = function() { + if(this.othersActive) return; + var session = this.session; + var _self = this; + this.othersActive = true; + this.others.forEach(function(anchor) { + anchor.markerId = session.addMarker(new Range(anchor.row, anchor.column, anchor.row, anchor.column+_self.length), _self.othersClass, null, false); + anchor.on("change", function(event) { + session.removeMarker(anchor.markerId); + anchor.markerId = session.addMarker(new Range(event.value.row, event.value.column, event.value.row, event.value.column+_self.length), _self.othersClass, null, false); + }); + }); + }; + + /** + * PlaceHolder.hideOtherMarkers() + * + * Hides all over markers in the [[EditSession `EditSession`]] that are not the currently selected one. + * + **/ + this.hideOtherMarkers = function() { + if(!this.othersActive) return; + this.othersActive = false; + for (var i = 0; i < this.others.length; i++) { + this.session.removeMarker(this.others[i].markerId); + } + }; + + /** + * PlaceHolder@onUpdate(e) + * + * Emitted when the place holder updates. + * + **/ + this.onUpdate = function(event) { + var delta = event.data; + var range = delta.range; + if(range.start.row !== range.end.row) return; + if(range.start.row !== this.pos.row) return; + if (this.$updating) return; + this.$updating = true; + var lengthDiff = delta.action === "insertText" ? range.end.column - range.start.column : range.start.column - range.end.column; + + if(range.start.column >= this.pos.column && range.start.column <= this.pos.column + this.length + 1) { + var distanceFromStart = range.start.column - this.pos.column; + this.length += lengthDiff; + if(!this.session.$fromUndo) { + if(delta.action === "insertText") { + for (var i = this.others.length - 1; i >= 0; i--) { + var otherPos = this.others[i]; + var newPos = {row: otherPos.row, column: otherPos.column + distanceFromStart}; + if(otherPos.row === range.start.row && range.start.column < otherPos.column) + newPos.column += lengthDiff; + this.doc.insert(newPos, delta.text); + } + } else if(delta.action === "removeText") { + for (var i = this.others.length - 1; i >= 0; i--) { + var otherPos = this.others[i]; + var newPos = {row: otherPos.row, column: otherPos.column + distanceFromStart}; + if(otherPos.row === range.start.row && range.start.column < otherPos.column) + newPos.column += lengthDiff; + this.doc.remove(new Range(newPos.row, newPos.column, newPos.row, newPos.column - lengthDiff)); + } + } + // Special case: insert in beginning + if(range.start.column === this.pos.column && delta.action === "insertText") { + setTimeout(function() { + this.pos.setPosition(this.pos.row, this.pos.column - lengthDiff); + for (var i = 0; i < this.others.length; i++) { + var other = this.others[i]; + var newPos = {row: other.row, column: other.column - lengthDiff}; + if(other.row === range.start.row && range.start.column < other.column) + newPos.column += lengthDiff; + other.setPosition(newPos.row, newPos.column); + } + }.bind(this), 0); + } + else if(range.start.column === this.pos.column && delta.action === "removeText") { + setTimeout(function() { + for (var i = 0; i < this.others.length; i++) { + var other = this.others[i]; + if(other.row === range.start.row && range.start.column < other.column) { + other.setPosition(other.row, other.column - lengthDiff); + } + } + }.bind(this), 0); + } + } + this.pos._emit("change", {value: this.pos}); + for (var i = 0; i < this.others.length; i++) { + this.others[i]._emit("change", {value: this.others[i]}); + } + } + this.$updating = false; + }; + + /** + * PlaceHolder@onCursorChange(e) + * + * Emitted when the cursor changes. + * + **/ + + this.onCursorChange = function(event) { + if (this.$updating) return; + var pos = this.session.selection.getCursor(); + if(pos.row === this.pos.row && pos.column >= this.pos.column && pos.column <= this.pos.column + this.length) { + this.showOtherMarkers(); + this._emit("cursorEnter", event); + } else { + this.hideOtherMarkers(); + this._emit("cursorLeave", event); + } + }; + + /** + * PlaceHolder.detach() + * + * TODO + * + **/ + this.detach = function() { + this.session.removeMarker(this.markerId); + this.hideOtherMarkers(); + this.doc.removeEventListener("change", this.$onUpdate); + this.session.selection.removeEventListener("changeCursor", this.$onCursorChange); + this.pos.detach(); + for (var i = 0; i < this.others.length; i++) { + this.others[i].detach(); + } + this.session.setUndoSelect(true); + }; + + /** + * PlaceHolder.cancel() + * + * TODO + * + **/ + this.cancel = function() { + if(this.$undoStackDepth === -1) + throw Error("Canceling placeholders only supported with undo manager attached to session."); + var undoManager = this.session.getUndoManager(); + var undosRequired = (undoManager.$undoStack || undoManager.$undostack).length - this.$undoStackDepth; + for (var i = 0; i < undosRequired; i++) { + undoManager.undo(true); + } + }; +}).call(PlaceHolder.prototype); + + +exports.PlaceHolder = PlaceHolder; +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/theme/textmate', ['require', 'exports', 'module' , 'ace/lib/dom'], function(require, exports, module) { +"use strict"; + +exports.isDark = false; +exports.cssClass = "ace-tm"; +exports.cssText = ".ace-tm .ace_editor {\ + border: 2px solid rgb(159, 159, 159);\ +}\ +\ +.ace-tm .ace_editor.ace_focus {\ + border: 2px solid #327fbd;\ +}\ +\ +.ace-tm .ace_gutter {\ + background: #e8e8e8;\ + color: #333;\ +}\ +\ +.ace-tm .ace_print_margin {\ + width: 1px;\ + background: #e8e8e8;\ +}\ +\ +.ace-tm .ace_fold {\ + background-color: #6B72E6;\ +}\ +\ +.ace-tm .ace_text-layer {\ + cursor: text;\ +}\ +\ +.ace-tm .ace_cursor {\ + border-left: 2px solid black;\ +}\ +\ +.ace-tm .ace_cursor.ace_overwrite {\ + border-left: 0px;\ + border-bottom: 1px solid black;\ +}\ + \ +.ace-tm .ace_line .ace_invisible {\ + color: rgb(191, 191, 191);\ +}\ +\ +.ace-tm .ace_line .ace_storage,\ +.ace-tm .ace_line .ace_keyword {\ + color: blue;\ +}\ +\ +.ace-tm .ace_line .ace_constant {\ + color: rgb(197, 6, 11);\ +}\ +\ +.ace-tm .ace_line .ace_constant.ace_buildin {\ + color: rgb(88, 72, 246);\ +}\ +\ +.ace-tm .ace_line .ace_constant.ace_language {\ + color: rgb(88, 92, 246);\ +}\ +\ +.ace-tm .ace_line .ace_constant.ace_library {\ + color: rgb(6, 150, 14);\ +}\ +\ +.ace-tm .ace_line .ace_invalid {\ + background-color: rgba(255, 0, 0, 0.1);\ + color: red;\ +}\ +\ +.ace-tm .ace_line .ace_support.ace_function {\ + color: rgb(60, 76, 114);\ +}\ +\ +.ace-tm .ace_line .ace_support.ace_constant {\ + color: rgb(6, 150, 14);\ +}\ +\ +.ace-tm .ace_line .ace_support.ace_type,\ +.ace-tm .ace_line .ace_support.ace_class {\ + color: rgb(109, 121, 222);\ +}\ +\ +.ace-tm .ace_line .ace_keyword.ace_operator {\ + color: rgb(104, 118, 135);\ +}\ +\ +.ace-tm .ace_line .ace_string {\ + color: rgb(3, 106, 7);\ +}\ +\ +.ace-tm .ace_line .ace_comment {\ + color: rgb(76, 136, 107);\ +}\ +\ +.ace-tm .ace_line .ace_comment.ace_doc {\ + color: rgb(0, 102, 255);\ +}\ +\ +.ace-tm .ace_line .ace_comment.ace_doc.ace_tag {\ + color: rgb(128, 159, 191);\ +}\ +\ +.ace-tm .ace_line .ace_constant.ace_numeric {\ + color: rgb(0, 0, 205);\ +}\ +\ +.ace-tm .ace_line .ace_variable {\ + color: rgb(49, 132, 149);\ +}\ +\ +.ace-tm .ace_line .ace_xml_pe {\ + color: rgb(104, 104, 91);\ +}\ +\ +.ace-tm .ace_entity.ace_name.ace_function {\ + color: #0000A2;\ +}\ +\ +.ace-tm .ace_markup.ace_markupine {\ + text-decoration:underline;\ +}\ +\ +.ace-tm .ace_markup.ace_heading {\ + color: rgb(12, 7, 255);\ +}\ +\ +.ace-tm .ace_markup.ace_list {\ + color:rgb(185, 6, 144);\ +}\ +\ +.ace-tm .ace_marker-layer .ace_selection {\ + background: rgb(181, 213, 255);\ +}\ +.ace-tm.multiselect .ace_selection.start {\ + box-shadow: 0 0 3px 0px white;\ + border-radius: 2px;\ +}\ +.ace-tm .ace_marker-layer .ace_step {\ + background: rgb(252, 255, 0);\ +}\ +\ +.ace-tm .ace_marker-layer .ace_stack {\ + background: rgb(164, 229, 101);\ +}\ +\ +.ace-tm .ace_marker-layer .ace_bracket {\ + margin: -1px 0 0 -1px;\ + border: 1px solid rgb(192, 192, 192);\ +}\ +\ +.ace-tm .ace_marker-layer .ace_active_line {\ + background: rgba(0, 0, 0, 0.07);\ +}\ +.ace-tm .ace_gutter_active_line{\ + background-color : #dcdcdc;\ +}\ +\ +.ace-tm .ace_marker-layer .ace_selected_word {\ + background: rgb(250, 250, 255);\ + border: 1px solid rgb(200, 200, 250);\ +}\ +\ +.ace-tm .ace_meta.ace_tag {\ + color:rgb(28, 2, 255);\ +}\ +\ +.ace-tm .ace_string.ace_regex {\ + color: rgb(255, 0, 0)\ +}"; + +var dom = require("../lib/dom"); +dom.importCssString(exports.cssText, exports.cssClass); +}); +; + (function() { + ace.require(["ace/ace"], function(a) { + if (!window.ace) + window.ace = {}; + for (var key in a) if (a.hasOwnProperty(key)) + ace[key] = a[key]; + }); + })(); + \ No newline at end of file diff --git a/graphterm/www/graphterm.css b/graphterm/www/graphterm.css new file mode 100644 index 0000000..7484617 --- /dev/null +++ b/graphterm/www/graphterm.css @@ -0,0 +1,377 @@ +/* graphterm.css: default style sheet for graphterm index.html */ + +/* General styles */ +.noshow { + display: none; +} + +/* Default styles */ +body { + background-color: #FFFFFF; +} + +/* Need font styling for both body and pre; otherwise pre styling will be overridden by useragent default */ +body, pre { + font-family: elite, courier, monaco, monospace; + font-size: 16px; +} + +/* Body screen styles (with button label font sizes) */ +body.smallscreen { + font-size: 12px; +} +body.mediumscreen { + font-size: 14px; +} +body.largescreen { + font-size: 16px; +} +body.extralargescreen { + font-size: 18px; +} + +body.ipadscreen { + /* font-weight: bold; */ +} + +/* Generic element styles */ +div, pre, span { + margin: 0; + padding-top: 0; + padding-bottom: 0; + } + +pre { + line-height: 120%; +} + +form { + margin: 0; + padding-top: 0; + padding-bottom: 0; + } + +hr { + margin: 5px; + padding-top: 0; + padding-bottom: 0; + } + +/* Content Editable styles */ + +*[contenteditable="true"]:not(.cursorhighlight) { +/* + padding: 10px; + outline: 3px dashed #CCC; +*/ + outline: none; +} + +*[contenteditable="true"]:hover { +/* + background: rgba(255, 255, 153, 1); + outline: 3px dashed #0090D2; +*/ +} + + +/* Screen styles */ +pre.row { + margin: 0; + padding-top: 0; + padding-bottom: 0; + white-space: pre; } + +span.row { + display: block; + margin: 0; + padding-top: 0; + padding-bottom: 0; +} + +span.row .bold { + font-weight: bold; +} + +span.row .inverse { + background-color: lightgray; +} + +span.row .cursorspan { + display: inline-block; + background-color: gray; +} + +span.row .cursorspan .cursorloc { + display: inline-block; + background-color: red; +} + +span.row .cursorspan.cursorhighlight .cursorloc { + /* outline: 1px dashed #ccc; */ + background-color: lightblue; +} + +span.row .cmd-completion, span.row .typeahead { + opacity: 0.6; +} + +span.row.gterm-hideoutput.promptrow .gterm-cmd-prompt { + text-decoration: underline; +} + +span.row.gterm-hideoutput:not(.promptrow), + .pagelet.gterm-hideoutput { + display: none; +} + +#session-headermenu, + #session-footermenu { + background-color: lightgray; +} + +#session-headermenu { + border-bottom-style: none; + border-bottom-color: gray; + border-bottom-width: 2px; +} + +#session-footermenu { + margin-top: 4px; + border-top-style: none; + border-top-color: gray; + border-top-width: 2px; +} + +#session-headermenu span.gterm-headfoot-active, +#session-footermenu span.gterm-headfoot-active { + text-decoration: underline; +} + +span.boldstyle { + font-weight: bold; +} + +span.underlinestyle { + text-decoration: underline; +} + +span.blinkstyle { + font-weight: bold; +} + +span.inversestyle { + font-weight: bold; +} + + +.pagelet td { + vertical-align: top; + padding-left: 10px; + padding-right: 10px; +} + +.pagelet a, .row a { + margin: 0; + padding-top: 0; + padding-bottom: 0; + } + +#session-bufellipsis h3 { + margin: 0; + padding-top: 0; + padding-bottom: 0; + } + +/* Hyperlinks */ +span.menubar-label, a.menubar-label { + color: gray; + text-decoration: none; + font-weight: bold; +} + +span.headfoot { + color: blue; + cursor: pointer; + text-decoration: none; + /* font-weight: bold; */ +} + +span.gterm-link, a.gterm-link { + color: blue; + cursor: pointer; + text-decoration: none; +} + +span.gterm-not-implemented { + color: gray; +} + +#terminal:not(.webcast) .webcast-alert { + display: none; +} + +#terminal.webcast #menubar-webcast { + background-color: red; +} + +#terminal.webcast #menubar-label-webcast { + color: red; +} + +/* Screens */ +#session-term:not(.split-screen) .session-screen, .session-altscreen { + margin-bottom: 1.0em; +} + +/* Split screen */ +#session-term.split-screen #session-bufscreen, + #session-term.split-screen #session-findercontainer { + margin-bottom: 3em; /* Should be dynamic (=session-screencontainer height) */ +} + +#session-term.split-screen.display-footer #session-bufscreen, + #session-term.split-screen.display-footer #session-findercontainer { + margin-bottom: 4em; /* Should be dynamic (=session-screencontainer height) */ +} + +#session-term:not(.display-footer) #gterm-pre0 .gterm-cmd-prompt { + text-decoration: underline; +} + +#session-term:not(.display-footer) #session-footermenu { + display: none; +} + +#session-findercontainer { + position: absolute; + z-index: 50; + top: 0; + left: 10em; + width: 100%; + padding-bottom: 1.0em; + margin-bottom: 0; + opacity: 0.5; +} + +#session-finderbody { + opacity: 1.0; + background-color: white; +} + +#session-term.split-screen #session-screencontainer { + position: fixed; + z-index: 100; + bottom: 0; + width: 100%; + margin: 0; + padding: 0; +} + +#session-term.split-screen #session-screenfold { + height: 0.6em; + margin: 0; + padding: 0; + background: -webkit-gradient( linear, center bottom, center top, from(rgba(128,128,128,1.0)), to(rgba(255,255,255,0.0)) ); + background: -moz-linear-gradient( 90deg, rgba(128,128,128,1.0), rgba(255,255,255,0.0) ); + border-bottom-style: solid; + border-bottom-color: gray; + border-bottom-width: 0px; +} + +#session-term:not(.split-screen) #session-screenfold { + display: none; +} + +#session-term.split-screen #session-screenfix { + opacity: 1.0; + background-color: white; + margin: 0; + padding-top: 0.25em; + padding-bottom: 1.0em; +} + +/* Popup styles */ +.gterm-popupmask { + position: absolute; + z-index: 9000; + top: 0; + left: 0; + background-color: #000000; +} + +.gterm-popup { + position: absolute; + width: 90%; + z-index: 9900; + padding: 20px; + background-color: #ffffff; +} + +.gterm-popupnarrow { + width: 70%; +} + +/* Log style */ +#gtermsplash { + margin-top: 75; + background: url(themes/stars-images/starry-night-PublicDomain-VeraKratochvil.jpg); + background-repeat: repeat; + opacity: 0.9; + height: 100%; +} + +#gtermsplashtext { + color: black; + background: -webkit-gradient( linear, center bottom, center top, color-stop(0.0, rgba(253,253,253,0.0)), color-stop(0.2, rgba(254,254,254,0.0)), color-stop(0.85, rgba(255,255,255,1.0)) ); + background: -moz-linear-gradient( 90deg, rgba(253,253,253,0.0) 0%, rgba(254,254,254,0.0) 20%, rgba(255,255,255,1.0) 85% ); + z-index: 200; + padding: 10px; +} + +#gtermsplashtext .gtermsplashalt, #gtermsplashtext .gtermsplashalt a { + color: #eeeeee; /*#ffdd00;*/ +} + +/* Editor style */ +#acearea_content { + position: absolute; + top: 40; + left: 0; + width: 100%; + height: 100%; +} + +/* Tilting 3D perspective style */ +.three-d .session-container { + z-index: 1; + width: 100%; + height: 575px; +} +.three-d .session { + position: absolute; + z-index: 1; + left: 50%; + margin-left: -300px; + top: 50px; + height: 500px; + overflow-y: scroll; + -webkit-transform: perspective(300) rotateX(20deg); /* target: perspective(300) rotateX(20deg) */ + -webkit-transform-style: preserve-3d; + -moz-perspective: 800px; + -moz-transform: rotateX(45deg); + -moz-transform-style: preserve-3d; +} + +/* Lateral rotation 3D style */ +.lateral { + z-index: 1; + height: 30px; + overflow-y: scroll; + -webkit-transform: perspective(300) rotateY(60deg); + -webkit-transform-style: preserve-3d; + -moz-perspective: 800px; + -moz-transform: rotateX(45deg); + -moz-transform-style: preserve-3d; +} + diff --git a/graphterm/www/graphterm.js b/graphterm/www/graphterm.js new file mode 100644 index 0000000..8022159 --- /dev/null +++ b/graphterm/www/graphterm.js @@ -0,0 +1,2023 @@ +// GraphTerm Page Commands + +// CONVENTION: All pre-defined GraphTerm Javascript functions and global +// variables should begin with an upper-case letter. +// This would allow them to be easily distinguished from +// user defined functions, which should begin with a lower case +// letter. + + +// Global variables + +var gMobileBrowser = navigator.userAgent.toLowerCase().indexOf('mobile') > -1; +var gFirefoxBrowser = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; +var gWebkitBrowser = navigator.userAgent.toLowerCase().indexOf('webkit') > -1; +var gChromeBrowser = navigator.userAgent.toLowerCase().indexOf('chrome') > -1; +var gSafariBrowser = !gChromeBrowser && navigator.userAgent.toLowerCase().indexOf('safari') > -1; + +var gSafariIPad = gSafariBrowser && navigator.userAgent.toLowerCase().indexOf('ipad') > -1; + +var FILE_URI_PREFIX = "file:/"+"/"; // Split double slash for minification + +var MAX_LINE_BUFFER = 500; + +var WRITE_LOG = function (str) {}; +var DEBUG_LOG = function (str) {}; +var DEBUG_LOG = function (str) {console.log(str)}; + +var gRowHeight = 16; +var gColWidth = 8; +var gBottomMargin = 14; + +var gRows = 0; +var gCols = 0; +var gWebSocket = null; + +var gEditing = null; + +var gForm = null; +var gFormIndex = null; + +var gDebug = true; +var gDebugKeys = false; + +var gTypeAhead = false; // Does not work very well + +var gAlwaysSplitScreen = gMobileBrowser; +var gSplitScreen = false; +var gShowingFinder = false; + +var gProgrammaticScroll = false; +var gManualScroll = false; +var gMaxScrollOffset = 0; + +var gTestBatchedScroll = false; // TEMPORARY (for testing batched programmatic scrolling) + +var gEntryIndex = 0; +var gCommandId = null; +var gCommandPrefix = "command"; +var gCommandMatchIndex = null; +var gCommandMatchPrev = null; +var gCommandBuffer = null; +var gCursorAtEOL = null; + +var gControlActive = false; + +var gParams = {}; + +var GTPrompt = "> "; +var GTCurDirURI = ""; + +// Scroll line array components +var JINDEX = 0 +var JOFFSET = 1 +var JDIR = 2 +var JMARKUP = 3 +var JLINE = 4 + +function bind_method(obj, method) { + return function() { + return method.apply(obj, arguments); + } +} + +// Bind event to handler, with optional context (for "this" object in handler) +(function( $ ){ +$.fn.rebind = function(eventType, handler, context) { + this.unbind(eventType); + if (context) + handler = bind_method(context, handler) + this.bind(eventType, handler); + }; + })( jQuery); + +// Bind click event to handler, with optional context (for "this" object in handler) +(function( $, clickEvent ){ +$.fn.bindclick = function(handler, context) { + this.unbind(clickEvent); + if (context) + handler = bind_method(context, handler) + this.bind(clickEvent, handler); + }; + })( jQuery, "click"); + +// Returns triplet [hostname, filename, fullpath] for file://host/path URIs +// If not file URI, returns [] +function splitFileURI(uri) { + if (uri.substr(0,FILE_URI_PREFIX.length) != FILE_URI_PREFIX) + return []; + var hostPath = uri.substr(FILE_URI_PREFIX.length); + var comps = hostPath.split("/"); + return [comps[0], comps[comps.length-1], "/"+comps.slice(1).join("/")] +} + +function getCookie(name) { + var regexp_match = document.cookie.match("\\b" + name + "=([^;]*)\\b"); + return regexp_match ? unescape(regexp_match[1]) : ""; +} + +function setCookie(name, value, exp_days) { + var cookie_value = escape(value); + if (exp_days) { + var exp_date = new Date(); + exp_date.setDate(exp_date.getDate() + exp_days); + cookie_value += ";expires="+exp_date.toUTCString(); + } + document.cookie = name + "=" + cookie_value; +} + +function AuthPage(need_user, need_code, msg) { + $("#authcode").val(""); + + if (need_user) + $("#authusercontainer").show(); + else + $("#authusercontainer").hide(); + + if (need_code) + $("#authcodecontainer").show(); + else + $("#authcodecontainer").hide(); + + $("#authmessage").text(msg||""); + $("#authenticate").show(); + + $("#terminal").hide(); + $("#session-container").hide(); +} + +function Authenticate() { + Connect($("#authuser").val(), $("#authcode").val()); +} + +function Connect(auth_user, auth_code) { + gWebSocket = new GTWebSocket(auth_user, auth_code); +} + +function SignOut() { + setCookie("GRAPHTERM_AUTH", ""); + $("body").html('Signed out.

Sign in again'); +} + +function bind_method(obj, method) { + return function() { + return method.apply(obj, arguments); + } +} + +function setupTerminal() { + gRowHeight = $("#session-screen").height() / 25; + gColWidth = $("#session-screen-testrow").width() / 80; + + $("#session-screen").html(""); + $("#session-term").hide(); + $("#session-altscreen").hide(); + $("#session-log .curentry .input .command").focus(); +} + +function handle_resize() { + gRows = Math.floor($(window).height() / gRowHeight) - 1; + gCols = Math.floor($(window).width() / gColWidth) - 1; + if (gWebSocket && gParams.controller) + gWebSocket.write([["set_size", [gRows, gCols]]]); +} + +function openTerminal() { + $("#session-term").show(); + $("#session-roll").hide(); + if (gWebSocket) + gWebSocket.terminal = true; +} + +function closeTerminal() { + $("#session-term").hide(); + $("#session-roll").show(); + if (gWebSocket) + gWebSocket.terminal = false; +} + +function GTNextEntry() { + gEntryIndex += 1; + gCommandId = gCommandPrefix+gEntryIndex; + return '

'+GTPrompt+' 
'; +} + +function GTFirstEntryIndex() { + var firstEntry, commandPrefix; + if (gWebSocket && gWebSocket.terminal) { + commandPrefix = "entry"; + firstEntry = $("#session-bufscreen .promptrow:first"); + } else { + commandPrefix = gCommandPrefix; + firstEntry = $("#session-log .entry .command:first"); + } + if (!firstEntry.length) + return 0; + return parseInt(firstEntry.attr("id").substr(commandPrefix.length)); +} + +function GTGetCommandText(n) { + var commandPrefix = (gWebSocket && gWebSocket.terminal) ? "entry" : gCommandPrefix + var cmd_id = "#"+commandPrefix+n; + if (gWebSocket && gWebSocket.terminal) { + var cmdText = $(cmd_id).text().substr($(cmd_id+" .gterm-cmd-prompt").text().length+1); + if (cmdText && cmdText.charCodeAt(cmdText.length-1) == 10) + cmdText = cmdText.substr(0,cmdText.length-1); // Chop off the newline + return cmdText; + } else { + return $(cmd_id).text() || ""; + } +} + +function GTGetCurCommandText() { + if (gWebSocket && gWebSocket.terminal) { + var curtext = $("#gterm-pre0").text().substr($("#gterm-pre0 .gterm-cmd-prompt").text().length+1); + curtext = curtext.substr(0, curtext.length-$("#gterm-pre0 .cmd-completion").text().length-$("#gterm-pre0 .cursorloc").text().length-1); + return curtext; + } else { + return $("#session-log .curentry .input .command").text(); + } +} + +function GTSetCommandText(text, noClear) { + // Set (unescaped) text for current command line + if (gWebSocket && gWebSocket.terminal) { + var curtext = GTGetCurCommandText(); + if (curtext == text.substr(0, curtext.length)) { + var tailtext = text.substr(curtext.length); + $("#gterm-pre0 .cmd-completion").text(tailtext); + if (tailtext) + $("#gterm-pre0 .cursorloc").text(""); + else + $("#gterm-pre0 .cursorloc").text(" "); + } + } else { + $("#session-log .curentry .input .command").text(text); + } + if (!noClear) + gCommandBuffer = null; // Clear command buffer +} + +function GTCommandMatch(prefix, downward) { + // Finds command with matching prefix in history + // If null prefix, find any non-null command. + // If downward, search downwards. + // Returns non-null command string on success + var curIndex = (gWebSocket && gWebSocket.terminal) ? gPromptIndex+1 : gEntryIndex; + if (!gCommandMatchIndex) { + gCommandMatchIndex = curIndex; + gCommandatchPrev = null; + } + + if (downward) { + while (gCommandMatchIndex < curIndex) { + gCommandMatchIndex += 1; + if (gCommandMatchIndex >= curIndex) + return gCommandBuffer; + + var cmd = GTGetCommandText(gCommandMatchIndex); + if (cmd && cmd != gCommandMatchPrev) { + if (cmd.substr(0,prefix.length) == prefix) { + gCommandMatchPrev = cmd; + return cmd; + } + } + } + } else { + var firstIndex = GTFirstEntryIndex(); + while (gCommandMatchIndex > firstIndex) { + gCommandMatchIndex -= 1; + var cmd = GTGetCommandText(gCommandMatchIndex); + if (cmd && cmd != gCommandMatchPrev) { + if (cmd.substr(0,prefix.length) == prefix) { + gCommandMatchPrev = cmd; + return cmd; + } + } + if (gCommandMatchIndex <= firstIndex) + break; + } + } + return null; +} + +function GTStrip(text) { + // Strip leading/trailing non-breaking spaces + return text.replace(/^[\xa0]/,"").replace(/[\xa0]$/,"").replace(/[\xa0]/g," "); +} + +var gPromptIndex = 0; +var gScrollTop = false; + +function GTEscape(text, pre_offset, prompt_offset, prompt_id) { + var prefix = ""; + if (prompt_offset) { + var prompt_idattr = prompt_id ? ' id="'+prompt_id+'"' : ''; + prefix = '' + text.substring(pre_offset, prompt_offset) + ''; + text = text.substr(prompt_offset); + } + var text2 = prefix + text.replace(/&/g, "&").replace(//g, ">"); + return text2; +} + +function GTEscapeSpan(text, style_list) { + // Return styled (and escaped) SPAN string + if (!text) + return ""; + var span_escaped = GTEscape(text); + + if (style_list && style_list.length) { + return ''+span_escaped+''; + } else { + return span_escaped; + } +} + +function GTCursorSpan(cursor_char) { + // Return SPAN string for cursor character + if (!cursor_char) + return ""; + var tag = gFirefoxBrowser ? "div" : "span"; // Can't paste into span in firefox + var innerEditable = gFirefoxBrowser ? "true" : "false"; + return '<'+tag+' class="cursorspan" contentEditable="true" data-gtermcursorchar="'+cursor_char+'"><'+tag+' class="cursorloc" autocapitalize="off" contentEditable="'+innerEditable+'">'+GTEscape(cursor_char)+''; +} + +function GTGetFilePath(fileURI, parentURI) { + // Returns unescaped file path, relative to parent URI (without trailing /) (if specified) + parentURI = parentURI || ""; + if (fileURI == parentURI) + return "."; + var parentPrefix = parentURI ? parentURI + "/" : ""; + var newPath; + if (parentPrefix && fileURI.substr(0,parentPrefix.length) == parentPrefix) { + newPath = fileURI.substr(parentPrefix.length); + } else { + var filePrefix = fileURI + "/"; + if (parentURI && parentURI.substr(0,filePrefix.length) == filePrefix) { + var comps = parentURI.substr(filePrefix.length).split("/"); + var relPath = ".."; + for (var j=0; j= 32 && text.charCodeAt(j) < 127) { + // Printable character + aheadText += text.charAt(j); + } else if (text.charCodeAt(j) == 127 && aheadText.length) { + // Backspace/DEL (with non-empty typeahead text) + aheadText = aheadText.substr(0,aheadText.length-1); + } else { + // Non-printable character + aheadElem.attr("frozen", "true"); + break; + } + } + $(".typeahead").text(aheadText); + } + GTReceivedUserInput("key"); + this.write([["keypress", text]]); + GTCurDirURI = ""; +} + +GTWebSocket.prototype.write = function(msg) { + try { + if (this.ws.readyState > WebSocket.OPEN) + throw "Websocket closed"; + this.ws.send(JSON.stringify(msg)); + } catch(err) { + if (window.confirm("Error in websocket ("+err+"). Reload page?")) { + window.location.reload(); + } + } +} + +GTWebSocket.prototype.onopen = function(evt) { + console.log("GTWebSocket.onopen: "); +} + +GTWebSocket.prototype.onmessage = function(evt) { + if (this.closed) + return; + + var payload = evt.data; + //if (gDebug) + //console.log("GTWebSocket.onmessage: "+payload); + + if (!this.opened) { + // Validate + this.opened = true; + } + + try { + var payload_obj = JSON.parse(payload); + for (var j=0; j'+hosts[j]+''; + host_html += '

Sign out'; + $("body").html(host_html); + + } else if (action == "term_list") { + if (command[1]) + setCookie("GRAPHTERM_AUTH", command[1]); + var host = command[2]; + var terms = command[3]; + var term_html = 'Connect to session:

    '; + for (var j=0; j'+terms[j]+''; + term_html += '
  1. new
  2. '; + term_html += '

Sign out'; + $("body").html(term_html); + + } else if (action == "log") { + var logtype = command[1] || "log"; + var prefix = logtype.toUpperCase()+": "; + console.log(prefix+command[3]); + $('

'+prefix+command[3]+'
').appendTo("#session-log .curentry"); + $("#session-log .curentry .gterm-log .gterm-link").bindclick(otraceClickHandler); + + } else if (action == "prompt") { + GTPrompt = command[1]; + GTCurDirURI = command[2]; + + } else if (action == "input") { + var command_line = command[1]; + var alt_command_line = $("#session-log .curentry .prompt").attr("data-gtermaltcmd"); + if (alt_command_line) + command_line = alt_command_line; + GTSetCommandText(command_line); // Unescaped text + + } else if (action == "output" || action == "html_output") { + if (action == "html_output") { + var pagelet_html = '
'+command[1]+'
\n'; + var new_elem = $(pagelet_html).hide().appendTo("#session-log .curentry"); + $("#session-log .curentry .pagelet .gterm-rowimg").addClass("icons"); + if (!gWebSocket.icons) + $("#session-log .curentry .pagelet .gterm-rowimg").hide(); + $("#session-log .curentry .pagelet .gterm-link").bindclick(otraceClickHandler); + new_elem.show(); + } else { + $(command[1]).appendTo("#session-log .curentry"); + } + $("#session-log .curentry .input .command").removeAttr("contentEditable"); + + if ($("#session-log .preventry .prompt").attr("data-gtermsaveduri") && + $("#session-log .curentry .prompt").attr("data-gtermsaveduri")) { + // Remove previous entry for "cdls" consolidation + $("#session-log .preventry").remove(); + } else { + $("#session-log .preventry").removeClass("preventry"); + } + $("#session-log .curentry").addClass("preventry"); + $("#session-log .curentry").removeClass("curentry"); + + $(GTNextEntry()).appendTo("#session-log"); + + var scroll_msec = 0; + if (scroll_msec && $("#session-log .preventry").length) { + var scrollY = $("#session-log .preventry").offset().top + $("#session-log .preventry").height() - 20; + $("html:not(:animated), body:not(:animated)").animate({scrollTop: scrollY}, scroll_msec, + "linear", function() {$("#session-log .curentry .input .command").focus();}); + } else { + // Last prompt will appear at bottom of window + $("#session-log .curentry .input .command").focus(); + } + + } else if (action == "terminal") { + if (!this.terminal) + openTerminal(); + var cmd_type = command[1]; + var cmd_arg = command[2]; + if (cmd_type == "save_status") { + alert("File "+cmd_arg[0]+": "+(cmd_arg[1] || "saved")); + } else if (cmd_type == "graphterm_output") { + var entry_class = "entry"+gPromptIndex; + var params = cmd_arg[0]; + var content = cmd_arg[1]; + var content_type = params.headers.content_type; + var response_type = params.headers.x_gterm_response; + var response_params = params.headers.x_gterm_parameters; + //console.log("graphterm_output: params: ", params); + if (response_type == "error_message") { + alert(content); + } else if (response_type == "open_terminal") { + gWebSocket.write([["open_terminal", [response_params.term_name, + response_params.command]]]); + } else if (response_type == "display_finder") { + ShowFinder(response_params, content); + + } else if (response_type == "edit_file") { + EndFullscreen(); + GTStartEdit(response_params, content); + + } else if (!response_type || response_type.substr(0,7) == "pagelet") { + if (response_type == "pagelet_form" || response_type == "pagelet_fullscreen") { + // Hide previous entries, removing previous pagelets for this entry + if (response_type == "pagelet_form") { + StartFullscreen(false); + GTStartForm(response_params, gPromptIndex); + } else { + StartFullscreen(true); + } + $("#session-bufscreen").children(".pagelet."+entry_class).remove(); + $("#session-bufscreen").children("."+entry_class).show(); + if ("scroll" in response_params && response_params.scroll != "down") + gScrollTop = true; + } else { + // New pagelet entry; show previous entries + EndFullscreen(); + } + var current_dir = ("current_directory" in params.headers) ? params.headers.current_directory : ""; + var pagelet_html = (content_type == "text/html") ? '
'+content+'
\n' : '
'+content+'
\n'; + + var new_elem = $(pagelet_html).hide().appendTo("#session-bufscreen"); + if (response_type == "pagelet_form") { + new_elem.find(".gterm-form-button").bindclick(GTFormCommand); + new_elem.find(".gterm-form-label").bind("hover", GTFormHelp); + } + $("#session-bufscreen .pagelet .gterm-rowimg").addClass("icons"); + if (!gWebSocket.icons) + $("#session-bufscreen .pagelet .gterm-rowimg").hide(); + + $('#session-bufscreen .pagelet."'+entry_class+'" td .gterm-link').bindclick(gtermPageletClickHandler); + $('#session-bufscreen .pagelet."'+entry_class+'" td img').bind("dragstart", function(evt) {evt.preventDefault();}); + + GTDropBindings($('#session-bufscreen .pagelet."'+entry_class+'" .droppable')); + new_elem.show(); + } + } else if (cmd_type == "row_update") { + var alt_mode = cmd_arg[0]; + var reset = cmd_arg[1]; + var active_rows = cmd_arg[2]; + var term_width = cmd_arg[3]; + var term_height = cmd_arg[4]; + var cursor_x = cmd_arg[5]; + var cursor_y = cmd_arg[6]; + var pre_offset = cmd_arg[7]; + var update_rows = cmd_arg[8]; + var update_scroll = cmd_arg[9]; + + if (alt_mode && !this.alt_mode) { + this.alt_mode = true; + if (gSplitScreen) + MergeScreen("alt_mode"); + $("#session-screen").hide(); + $("#session-altscreen").show(); + } else if (!alt_mode && this.alt_mode) { + this.alt_mode = false; + $("#session-screen").show(); + $("#session-altscreen").hide(); + } + + // Note: Paste operation pastes DOM elements with "span.row" class into screen + // Therefore, need to restrict selector to immediate children of #session-screen + var nrows = $("#session-screen > span.row").length; + + if (!alt_mode && (reset || nrows != active_rows)) { + if (reset) { + $("#session-screen").empty(); + nrows = 0; + if (gSplitScreen) + MergeScreen("reset"); + } + + if (gSplitScreen && active_rows != 1) + MergeScreen("rows"); + + if (active_rows < nrows) { + for (var k=active_rows; k nrows) { + for (var k=nrows; k\n').appendTo("#session-screen"); + if (gSplitScreen) + ResizeSplitScreen(true); + } + } + + if (alt_mode && (reset || !$("#session-altscreen span.row").length)) { + var preList = ["
"]; + for (var k=0; k\n'); + $("#session-altscreen").html(preList.join("")); + } + + gCursorAtEOL = false; + for (var j=0; j= row_offset && cursor_x < row_offset+span.length) { + var rel_offset = cursor_x - row_offset; + line_html += GTEscapeSpan(span.substr(0,rel_offset), style_list); + line_html += GTCursorSpan(span.substr(rel_offset,1)); + line_html += GTEscapeSpan(span.substr(rel_offset+1), style_list); + } else { + line_html += GTEscapeSpan(span, style_list); + } + row_offset += span.length; + } + if (row_num == cursor_y && cursor_x >= row_line.length) { + // Cursor at end of line; pad with spaces to extend to cursor location + for (var k=row_line.length; k'+line_html+'\n'; + if ($("#"+idstr).length) { + GTDropBindings($(row_html).replaceAll("#"+idstr).filter(".droppable")); + if (prompt_offset) + $("#gterm-pre0 .gterm-cmd-prompt").bindclick(ToggleFooter); + + } else { + console.log("graphterm: Error - missing element with ID "+idstr); + } + } + if (update_rows.length) { + $(".cursorspan").rebind('paste', pasteHandler); + $(".cursorspan").rebind('click', pasteReadyHandler); + } + + if (update_scroll && update_scroll.length) { + for (var j=0; j= MAX_LINE_BUFFER) + $("#session-bufscreen span.row:first").remove(); + var newPromptIndex = update_scroll[j][JINDEX]; + var entry_id = "entry"+newPromptIndex; + var prompt_id = "prompt"+newPromptIndex; + var prompt_offset = update_scroll[j][JOFFSET]; + var entry_class = entry_id; + if (prompt_offset) { + entry_class += " promptrow"; + if (gPromptIndex == newPromptIndex) { + // Repeat entry; remove any previous versions of same entry + $("."+"entry"+newPromptIndex).remove(); + // Hide older entries + StartFullscreen(false); + } else { + // New entry; show any older entries + EndFullscreen(); + } + } + gPromptIndex = newPromptIndex; + var markup = update_scroll[j][JMARKUP]; + var row_escaped = (markup == null) ? GTEscape(update_scroll[j][JLINE], pre_offset, prompt_offset, prompt_id) : markup; + var row_html = ''+row_escaped+"\n"; + $(row_html).appendTo("#session-bufscreen"); + $("#"+entry_id+" .gterm-link").bindclick(gtermLinkClickHandler); + } + } + + if (gScrollTop) { + gScrollTop = false; + ScrollTop(0); + } else if (this.theme == "default") { + if (!gSplitScreen) + ScrollScreen(alt_mode); + } else { + $("#session-term").scrollTop($("#session-term")[0].scrollHeight - $("#session-term").height()) + } + } + + } else if (action == "completed_input") { + if (command[1].length == 1) { + GTSetCommandText(command[1][0]); // Unescaped text + GTSetCursor(gCommandId); + } + $("#session-log .curentry .input .command").focus(); + + } else if (action == "edit") { + var editParams = command[1]; + var content = command[2]; + + GTStartEdit(editParams, content); + + } else if (action == "errmsg") { + alert(command[1]); + + } else { + console.log("GTWebSocket.onmessage: Invalid message type: "+action); + } + } + return; + + } catch(err) { + console.log("GTWebSocket.onmessage", err); + this.write([["errmsg", ""+err]]); + this.close(); + } +} + +GTWebSocket.prototype.onclose = function(e) { + console.log("GTWebSocket.onclose: "); + if (this.opened) { + } else { + this.failed = true; + } +} + +GTWebSocket.prototype.abort = function() { + console.log("GTWebSocket.abort: "); + this.close(); +} + +GTWebSocket.prototype.close = function() { + console.log("GTWebSocket.close: "); + if (this.closed) + return; + + this.closed = true; + this.opened = false; + try { + this.ws.close(); + } catch(err) { + } +} + +function GTGetCursorOffset(elementId) { + // Return text cursor offset relative to immediate container node + // If elementId is specified, and startContainer does not match it, + // null is returned + try { + var range = rangy.getSelection().getRangeAt(0); + if (elementId) { + if (elementId != $(range.startContainer).parent().attr("id")) + return null; + } + return range.startOffset; + } catch (err) { + return null; + } +} + +function GTSetCursor(elementId, cursorPos) { + // Position text cursor within element (default: end of text) + try { + if (typeof(cursorPos) == "undefined") + cursorPos = $("#"+elementId).text().length; + var elem = $("#"+elementId); + var range = rangy.createRange(); + range.setStart(elem[0].childNodes[0], cursorPos); + range.collapse(true); + var sel = rangy.getSelection(); + sel.setSingleRange(range); + } catch(err) { + console.log("GTSetCursor: ", err); + } +} + +function GTHandleRecall(up_arrow) { + // otrace version + // Returns true/false for event handling + var commandId = gCommandPrefix+gEntryIndex; + var commandText = $("#"+commandId).text(); + if (gCommandBuffer == null) { + gCommandBuffer = commandText; // First recall; save current command text + gCommandMatchIndex = gEntryIndex; + gCommandMatchPrev = null; + } + var offset = GTGetCursorOffset(commandId) || 0; + var prefix = commandText.substr(0,offset); + var nbsp_offset = prefix.indexOf("\xa0"); + if (nbsp_offset >= 0) { + prefix = prefix.replace(/[\xa0]/g,""); // Strip non-breaking spaces + if (nbsp_offset < offset) + offset = offset - 1 + } + var matchCommand = GTCommandMatch(prefix, !up_arrow); + if (matchCommand) { + // Update command (without updating buffer) + GTSetCommandText(matchCommand, true); + GTSetCursor(commandId, offset); + } + return false; +} + +function GTHandleHistory(up_arrow) { + // Terminal version + // Returns true/false for event handling + var commandText = GTGetCurCommandText(); + if (gCommandBuffer == null) { + gCommandBuffer = commandText; // First recall; save current command text + gCommandMatchIndex = gPromptIndex+1; + gCommandMatchPrev = null; + } + var matchCommand = GTCommandMatch(commandText, !up_arrow); + if (matchCommand) { + // Update command (without updating buffer) + GTSetCommandText(matchCommand, true); + } + return false; +} + +function AnimatePerspective(persp, rot, millisec, completed) { + var maxIter = 50; + var jIter = 0; + if (!millisec) millisec = 3000; + function iterCallback() { + if (jIter >= maxIter) { + return completed ? completed() : null; + } + jIter += 1; + var fac = jIter/maxIter; + var css_prop = "perspective("+persp/fac+") rotateX("+rot*fac+"deg)"; + $(".perspective").css("-webkit-transform", css_prop); + setTimeout(iterCallback, millisec/maxIter); + } + iterCallback(); +} + +function pasteReadyHandler(evt) { + var cursorElem = $(".session-screen .cursorspan"); + cursorElem.addClass("cursorhighlight"); + if (gFirefoxBrowser) { + try { + // Save range offsets (used for firefox paste) + var cur_sel = rangy.getSelection(); + var cur_range = cur_sel.getRangeAt(0); + + if (cur_range.startOffset != 0 || cur_range.endOffset != 0) { + try { + // Try to collapse range to start of text + var new_range = rangy.createRange(); + var startElem = $(cur_range.startContainer); + //console.log("pasteReadyHandler:", startElem, cur_range); + new_range.setStart(startElem[0], 0); + new_range.collapse(true); + cur_sel.setSingleRange(new_range); + cur_range = cur_sel.getRangeAt(0); + } catch (err) { + //console.log("pasteReadyHandler: ERR", err); + } + } + + cursorElem.attr("data-gtermpastestart", cur_range.startOffset+""); + cursorElem.attr("data-gtermpasteend", cur_range.endOffset+""); + } catch(err) {} + } +} + +function pasteHandler(evt) { + var elem = $(this); + setTimeout(function() { + var cursor_char = elem.attr("data-gtermcursorchar"); + var pasteText = ""; + var innerElem = elem.children(".cursorloc"); + + if (innerElem.attr("contentEditable") == "true") { + // Firefox paste implementation (setting inner span to not contentEditable does not work) + pasteText = elem.text(); + var pasteStart = elem.attr("data-gtermpastestart"); + var pasteEnd = elem.attr("data-gtermpasteend"); + if (pasteText && pasteStart && pasteEnd) { + var startOffset = parseInt(pasteStart); + var endOffset = parseInt(pasteEnd); + if (startOffset == 0 && endOffset == 0) { + pasteText = pasteText.substr(0,pasteText.length-1); + } else if (startOffset == 1 && endOffset == 1) { + pasteText = pasteText.substr(1); + } + } else if (pasteText) { + console.log("pasteHandler: ERROR no paste offset defined") + pasteText = ""; + } + } else { + // Default paste implementation + var cursorElem = innerElem.remove(); + pasteText = elem.text(); + elem.empty(); + elem.append(cursorElem); + } + //console.log("pasteHandler:", elem, pasteText.length, pasteText) + if (gWebSocket && pasteText) + gWebSocket.term_input(pasteText); + }, 100); +} + +function gtermSelectHandler(event) { + var idcomps = $(this).attr("id").split("-"); + var selectedOption = $(this).val(); + console.log("gtermSelectHandler: ", idcomps[1], selectedOption); + + GTReceivedUserInput("select"); + switch (idcomps[1]) { + case "actions": + $(this).val(1); + if (selectedOption == "reconnect") + ReconnectHost(); + if (selectedOption == "steal") + StealSession(); + break; + + case "icons": + gWebSocket.icons = (selectedOption == "on"); + if (gWebSocket.icons) { + $(".noicons").hide(); + $(".icons").show(); + } else { + $(".noicons").show(); + $(".icons").hide(); + } + break; + + case "webcast": + // Webcast + Webcast(selectedOption == "on"); + break; + + case "theme": + // Select theme + var three_d = (selectedOption.substr(selectedOption.length-2) == "3d"); + var base_theme = three_d ? selectedOption.substr(0, selectedOption.length-2) : selectedOption; + + if (gWebSocket.theme && gWebSocket.theme != "default") + $("body").removeClass(gWebSocket.theme); + if (base_theme && base_theme != "default") + $("body").addClass(base_theme); + gWebSocket.theme = base_theme; + + if (three_d) { + $("body").addClass("three-d"); + $("#session-container").css("height", $(window).height()-100); + $("#session-term, #session-roll").css("height", 0.75*$(window).height()); + //AnimatePerspective(300, 20); + } else { + $("body").removeClass("three-d"); + $("#session-container").css("height", ""); + $("#session-term, #session-roll").css("height", ""); + } + + break; + } +} + +function gtermMenuClickHandler(event) { + var idcomps = $(this).attr("id").split("-"); + console.log("gtermMenuClickHandler", $(this).attr("id"), idcomps[1]); + var text = ""; + switch (idcomps[1]) { + case "home": + if (gWebSocket) + gWebSocket.term_input("cd; gls\n"); + break; + case "bottom": + ScrollScreen(); + break; + case "help": + alert("Help not yet implemented"); + break; + case "collapse": + $("#session-bufscreen .row").addClass("gterm-hideoutput"); + $("#session-bufscreen .pagelet").addClass("gterm-hideoutput"); + break; + case "expand": + $("#session-bufscreen .row").removeClass("gterm-hideoutput"); + $("#session-bufscreen .pagelet").removeClass("gterm-hideoutput"); + break; + case "detach": + window.location = "/"; + break; + case "new": + OpenNew(); + break; + case "about": + alert("See http://info.mindmeldr.com/code/graphterm for more info"); + break; + case "control": + $("#headfoot-control").toggleClass("gterm-headfoot-active"); + gControlActive = $("#headfoot-control").hasClass("gterm-headfoot-active"); + break; + case "top": + ScrollTop(0); + break; + case "up": + if (HandleArrowKeys(38)) + text = "\x1b[A"; + break; + case "down": + if (HandleArrowKeys(40)) + text = "\x1b[B"; + break; + case "delete": + text = "\x7f"; + break; + case "left": + if (HandleArrowKeys(37)) + text = "\x1b[D"; + break; + case "right": + if (HandleArrowKeys(39)) + text = "\x1b[C"; + break; + case "clear": + text = "\x01\x0B"; // Ctrl-A Ctrl-K + break; + case "command": + GetFinder("command"); + break; + case "options": + GetFinder("options"); + break; + case "file": + GetFinder("file"); + break; + case "enter": + text = "\n"; + if (gShowingFinder) + HideFinder(); + break; + } + if (text.length && gWebSocket) { + gWebSocket.term_input(text); + } + return false; +} + +function gtermClickPaste(text, file_uri, options) { + gWebSocket.write([["click_paste", text, file_uri, options]]); + if (!gSplitScreen) + SplitScreen("paste"); +} + +function gtermLinkClickHandler(event) { + var contextMenu = gControlActive; + GTReceivedUserInput("click"); + var text = $(this).text(); + var file_uri = ""; + var options = {} + + if ($(this).hasClass("gterm-cmd-prompt")) { + if (contextMenu) { + if (gWebSocket) { + var cmdText = $(this).parent().text().substr($(this).text().length+1); + cmdText = cmdText.substr(0,cmdText.length-1); + gWebSocket.term_input(cmdText); + } + return false; + } + var cmd_output = $(this).parent().nextUntil(".promptrow"); + cmd_output.toggleClass("gterm-hideoutput"); + $(this).parent().toggleClass("gterm-hideoutput"); + } if ($(this).hasClass("gterm-cmd-text")) { + gtermClickPaste(text, file_uri, options); + } if ($(this).hasClass("gterm-cmd-path")) { + file_uri = $(this).attr("href"); + gtermClickPaste("", file_uri, options); + } + if (contextMenu) { + alert("Context menu not yet implemented"); + return false; + } + + console.log("gtermLinkClickHandler", file_uri, options); + return false; +} + +function gtermPageletClickHandler(event) { + var contextMenu = gControlActive; + GTReceivedUserInput("click"); + var text = $(this).text(); + var pagelet = $(this).closest(".pagelet"); + var file_uri = $(this).attr("href"); + + if (contextMenu) { + alert("Context menu not yet implemented"); + return false; + } + + var options = {enter: true} + options.command = $(this).attr("data-gtermcmd"); + var cd_command = (options.command.indexOf("cd ") == 0); + options.clear_last = (pagelet.length && cd_command) ? pagelet.attr("data-gtermpromptindex") : "0"; + gtermClickPaste("", file_uri, options); + if (cd_command) + GTCurDirURI = file_uri; + //console.log("gtermPageletClickHandler", file_uri, options); + return false; +} + +function gtermFinderClickHandler(event) { + // Paste selected Command/Option/File + // TODO: If empty command line and File finder, display context menu, + // and paste command as well as filepath + var contextMenu = gControlActive; + GTReceivedUserInput("click"); + var text = $(this).text(); + var file_uri = $(this).attr("href"); + + if (contextMenu) { + alert("Context menu not yet implemented"); + return false; + } + + var options = {clear_last: 0}; + options.command = $(this).attr("data-gtermcmd"); + console.log("gtermFinderClickHandler", text, file_uri, options); + gtermClickPaste("", file_uri, options); + HideFinder(); + return false; +} + +function otraceClickHandler(event) { + var prev_command = $("#session-log .preventry").length ? GTStrip($("#session-log .preventry .input .command").text()) : ""; + var cur_command = GTStrip(GTGetCurCommandText()); + var file_uri = $(this).attr("data-gtermuri") || $(this).attr("href"); + var filepath = GTGetFilePath(file_uri, GTCurDirURI); + + console.log("otraceClickHandler", GTCurDirURI); + if (cur_command.length) { + GTSetCommandText(cur_command + " " + filepath); + GTSetCursor(gCommandId); + $("#session-log .curentry .input .command").focus(); + GTExpandCurEntry(false); + } else { + var new_command = $(this).attr("data-gtermcmd"); + var command_line = (new_command == "cdls") ? new_command+" "+filepath : new_command.replace("%(path)", filepath); + + if ((new_command == "cdls" || new_command.indexOf("cdls ") == 0) && + (prev_command == "cdls" || prev_command.indexOf("cdls ") == 0)) { + // Successive open commands; consolidate by preparing to overwrite previous entry + var saved_uri = $("#session-log .preventry .prompt").attr("data-gtermsaveduri"); + if (!saved_uri) { + saved_uri = $("#session-log .preventry .prompt").attr("data-gtermuri"); + $("#session-log .preventry .prompt").attr("data-gtermsaveduri", saved_uri); + } + var alt_filepath = GTGetFilePath(file_uri, saved_uri); + var alt_command_line = new_command+" "+alt_filepath; + $("#session-log .curentry .prompt").attr("data-gtermsaveduri", saved_uri); + $("#session-log .curentry .prompt").attr("data-gtermaltcmd", alt_command_line); + GTExpandCurEntry(true); + } else { + GTExpandCurEntry(false); + } + gWebSocket.write([["input", command_line, null]]); + gCommandBuffer = null; + } + return false; +} + +function GTExpandCurEntry(expand) { + // Expand current entry to fill window + if (expand) { + $("#session-log .curentry").addClass("open-expanded"); + $("#session-log .curentry").css("min-height", $(window).height()-30); + } else { + $(".open-expanded").css("min-height", ""); + $(".open-expanded").removeClass("open-expanded"); + } +} + +function keydownHandler(evt) { + var activeTag = ""; + var activeType = ""; + var activeElem = $(document.activeElement); + + if (gEditing || gForm) + return true; + + if (gDebugKeys) + console.log("graphterm.keydownHandler: ", evt.keyCode, evt.which, evt); + + GTExpandCurEntry(false); + + if (evt.which >= 33 && evt.which <= 46) { + // Special keys (arrow keys etc.) + if (evt.which==0 || (gWebkitBrowser && evt.charCode==0) ) { + if (gWebSocket && gWebSocket.terminal) + return AjaxKeypress(evt); + } + } + + if (evt.which == 38 || evt.which == 40) { + // Up/down arrows + return GTHandleRecall((evt.which == 38)); + } + gCommandBuffer = null; // Clear command recall buffer + + if (gFirefoxBrowser) // Firefox handles TAB/ESC/DEL on keypress + return true; + + if (evt.which == 9) { + // TAB key + var text = GTStrip(GTGetCurCommandText()); + if (gWebSocket && gWebSocket.terminal) + gWebSocket.term_input(String.fromCharCode(9)); + else if (gWebSocket) + gWebSocket.write([["incomplete_input", text]]); + return false; + } else if (evt.which == 27) { + // ESC key + gWebSocket.term_input(String.fromCharCode(27)); + return false; + } else if (evt.which == 8 || evt.which == 127) { + // BSP/DEL key + if (gWebSocket && gWebSocket.terminal) { + gWebSocket.term_input(String.fromCharCode(127), true); + return false; + } + } + + if (gSafariBrowser && evt.ctrlKey && evt.which >= 65) { + // Control keys on Safari + if (gWebSocket && gWebSocket.terminal) + return AjaxKeypress(evt); + } + + return true; +} + +function keypressHandler(evt) { + if (gDebugKeys) + console.log("graphterm.keypressHandler: code ", evt.keyCode, evt.which, evt); + + if (gWebSocket && gWebSocket.terminal) + return AjaxKeypress(evt); + + var activeTag = ""; + var activeType = ""; + var activeElem = $(document.activeElement); + + if (evt.which == 27) + return false; + + if (gEditing || gForm) + return true; + + if (evt.which == 13) { + // Enter key + var text = GTStrip(GTGetCurCommandText()); // Unescaped text + gWebSocket.write([["input", text, null]]); + return false; + } + + return true; +} + +function HandleArrowKeys(keyCode) { + if (!gCursorAtEOL) + return true; + // Cursor at end of command line + if (keyCode == 38 || keyCode == 40) { + // Up/down arrows; command history recall + return GTHandleHistory((keyCode == 38)); + + } else if (keyCode == 39) { + // Right arrow; command history completion + if ($("#gterm-pre0 .cmd-completion").length) { + var comptext = $("#gterm-pre0 .cmd-completion").text(); + $("#gterm-pre0 .cmd-completion").text(""); + if (comptext) { + gWebSocket.term_input(String.fromCharCode(5)+comptext); + return false; + } + } + } + // Default handling + return true; +} + +function AjaxKeypress(evt) { + // From ajaxterm.js + if (!evt) var evt = window.event; + + if (evt.metaKey && !evt.ctrlKey) + return true; + + if (evt.which >= 37 && evt.which <= 40) { + if (!HandleArrowKeys(evt.which)) + return false + } + + // s="kp keyCode="+evt.keyCode+" which="+evt.which+" shiftKey="+evt.shiftKey+" ctrlKey="+evt.ctrlKey+" altKey="+evt.altKey; + // debug(s); + // return false; + // else { if (!evt.ctrlKey || evt.keyCode==17) { return; } + + var k = ""; + var clearForm = false; + var kc = 0; + + if (evt.keyCode) + kc = evt.keyCode; + + if (evt.which) + kc = evt.which; + + if (gDebugKeys) + console.log("graphterm.AjaxKeypress1", gControlActive, kc, evt.ctrlKey, evt); + + var formSubmitter = ".pagelet.entry"+gPromptIndex+" .gterm-form-command"; + if (kc == 13 && gForm && $(formSubmitter).length == 1) { + $(formSubmitter).click(); + return false; + } + + if (!evt.ctrlKey && !gControlActive && (gEditing || gForm)) { + // Not Ctrl character; editing/processing form + return true; + } + + if (evt.altKey) { + if (kc>=65 && kc<=90) + kc+=32; + if (kc>=97 && kc<=122) { + k=String.fromCharCode(27)+String.fromCharCode(kc); + } + + } else if (gControlActive && kc == 91) { + k=String.fromCharCode(kc-64); // Ctrl-[ (ESC) + + } else if (evt.ctrlKey || gControlActive) { + if (kc>=0 && kc<=31) k=String.fromCharCode(kc); // Ctrl-@..Z.._ + else if (kc>=64 && kc<=90) k=String.fromCharCode(kc-64); // Ctrl-@..Z + else if (kc>=96 && kc<=122) k=String.fromCharCode(kc-96); // Ctrl-@..Z + else if (kc==54) k=String.fromCharCode(30); // Ctrl-^ + else if (kc==109) k=String.fromCharCode(31); // Ctrl-_ + else if (kc==219) k=String.fromCharCode(27); // Ctrl-[ + else if (kc==220) k=String.fromCharCode(28); // Ctrl-\ + else if (kc==221) k=String.fromCharCode(29); // Ctrl-] + else if (kc==219) k=String.fromCharCode(29); // Ctrl-] + else if (kc==219) k=String.fromCharCode(0); // Ctrl-@ + + } else if (evt.which==0 || (gWebkitBrowser && evt.charCode==0 && kc >=33 && kc <= 46) ) { + if (kc==9) k=String.fromCharCode(9); // Tab + else if (kc==8) k=String.fromCharCode(127); // Backspace + else if (kc==27) k=String.fromCharCode(27); // Escape + else { + if (kc==33) k="[5~"; // PgUp + else if (kc==34) k="[6~"; // PgDn + else if (kc==35) k="[4~"; // End + else if (kc==36) k="[1~"; // Home + else if (kc==37) k="[D"; // Left + else if (kc==38) k="[A"; // Up + else if (kc==39) k="[C"; // Right + else if (kc==40) k="[B"; // Down + else if (kc==45) k="[2~"; // Ins + else if (kc==46) k="[3~"; // Del + else if (kc==112) k="[[A"; // F1 + else if (kc==113) k="[[B"; // F2 + else if (kc==114) k="[[C"; // F3 + else if (kc==115) k="[[D"; // F4 + else if (kc==116) k="[[E"; // F5 + else if (kc==117) k="[17~"; // F6 + else if (kc==118) k="[18~"; // F7 + else if (kc==119) k="[19~"; // F8 + else if (kc==120) k="[20~"; // F9 + else if (kc==121) k="[21~"; // F10 + else if (kc==122) k="[23~"; // F11 + else if (kc==123) k="[24~"; // F12 + if (k.length) { + k=String.fromCharCode(27)+k; + } + } + } else { + if (kc==8) + k=String.fromCharCode(127); // Backspace + else + k=String.fromCharCode(kc); + } + + if (gDebugKeys) + console.log("graphterm.AjaxKeypress2", kc, k, k.charCodeAt(0)); + + if (gForm && k == String.fromCharCode(3)) { + // Ctrl-C exit from form + GTEndForm("", true); + } else if (gEditing || gForm) { + // Editing or processing form + return true; + } + + if (k.length) { + if (k.charCodeAt(k.length-1) == 13) { + // Enter key + if (gShowingFinder) + HideFinder(); + if (gCommandMatchPrev) + HandleArrowKeys(39); // Simulate right arrow for command completion + } + gWebSocket.term_input(k, true); + } + evt.cancelBubble = true; + return GTPreventHandler(evt); +} + +function OpenNew(term_name, options) { + var tty = term_name || ""; + var new_url = window.location.protocol+"/"+"/"+window.location.host+"/"+gParams.host+"/"+tty; // Split the double slash to avoid confusing the JS minifier + console.log("open", new_url); + window.open(new_url, target="_blank"); +} + +function ReconnectHost() { + console.log("ReconnectHost"); + if (window.confirm("Reconnect to host? (will take over 15 sec)")) { + gWebSocket.write([["reconnect_host"]]); + } +} + +function StealSession() { + if (!window.confirm("Steal control of "+gParams.host+"/"+gParams.term+"?")) + return; + var steal_url = window.location.protocol+"/"+"/"+window.location.host+"/"+gParams.host+"/"+gParams.term+"/steal"; // Split the double slash to avoid confusing the JS minifier + console.log("StealSession: ", steal_url); + window.location = steal_url; +} + +function Webcast(start) { + console.log("Webcast", start); + if (!gWebSocket) + return; + if (start && !window.confirm('Make terminal publicly viewable ("webcast")?')) + return; + + $("#terminal").toggleClass("webcast", start); + gWebSocket.webcast = start; + gWebSocket.write([["webcast", gWebSocket.webcast]]); +} + +// Popup management +var gPopupType = ""; +var gPopupParams = null; +var gPopupConfirmClose = null; + +function popupSetup() { + // Initialize bindings for popup handling + $(".gterm-popup").hide(); + $(".gterm-popupmask").hide(); + $(".gterm-popupmask, .gterm-popupclose").click(popupClose); +} + +function popupClose(confirm) { + // Hide mask and popup window + if (confirm && gPopupConfirmClose) { + if (!gPopupConfirmClose()) { + // Cancel close + return false; + } + } + $(".gterm-popup").hide(); + $(".gterm-popupmask").hide(); + gPopupType = ""; + gPopupParams = null; + gPopupConfirmClose = null; +} + +function popupShow(elementId, popupType, popupParams, popupConfirmClose) { + // Display elementId as modal popup window + var maskId = elementId + "_mask"; + + gPopupType = popupType || ""; + gPopupParams = popupParams || null; + gPopupConfirmClose = popupConfirmClose || null; + + var animateMs = 0; + + // Get screen height/width + var maxWidth = 1600; + var minWidth = 100; + + var winWidth = $(window).width(); + var winHeight = $(window).height(); + var docHeight = $(document).height(); + + // Fade in mask + ScrollTop(0); + $(maskId).css({width: winWidth, height: docHeight}); + + //$(maskId).fadeIn(1000); + $(maskId).fadeTo(0.6*animateMs, 0.7); + + // Position popup window + //$(elementId).css("top", winHeight/2 - $(elementId).outerHeight()/2); + $(elementId).css("top", 0); + $(elementId).css("left", (winWidth/2) - ($(elementId).outerWidth()/2)); + + // Fade in popup + $(elementId).fadeIn(1.0*animateMs); + + $(elementId).find("textarea").focus(); + $(elementId).find("input:text").focus(); +} + +function GTStartEdit(params, content) { + $("#terminal").hide(); + gEditing = {params: params, content: content}; + if (gEditing.params.editor == "web") { + $("#pop_editarea_content").val(content); + popupShow("#pop_editarea", "editarea"); + } else { + if ($("#acearea_content").length) + $("#acearea_content").remove(); + $('
/div>').appendTo("#acearea"); + $("#acearea").show(); + gEditing.ace = ace.edit("acearea_content"); + try { + if (params.filetype) + gEditing.ace.getSession().setMode("ace/mode/"+params.filetype); + } catch(err) { + console.log("ERROR in mode:", params.filetype, err); + } + gEditing.ace.getSession().setValue(content); + } +} + +function GTEndEdit(save) { + var newContent; + if (gEditing.params.editor == "web") { + newContent = $("#pop_editarea_content").val(); + } else { + newContent = gEditing.ace.getSession().getValue(); + } + if (gEditing.params.modify && newContent != gEditing.content) { + if (save) { + if (gEditing.params.command) { + gWebSocket.write([["input", gEditing.params.command, newContent]]); + } else { + gWebSocket.write([["save_file", gEditing.params.filepath, newContent]]); + } + } else if (!window.confirm("Discard changes?")) { + return false; + } + } + if (gEditing.params.editor == "web") { + popupClose(false); + } else { + $("#acearea").hide(); + $("#acearea_content").remove(); + } + gEditing = null; + $("#terminal").show(); + ScrollScreen(); + return false; +} + +function GTStartForm(params, promptIndex) { + gForm = params || {}; + gFormIndex = promptIndex || 0; + $("#session-screen").hide(); +} + +function GTEndForm(text, cancel) { + text = text || ""; + console.log("GTEndForm: ", text, cancel); + $("#session-screen").show(); + if (cancel) { + $("#session-bufscreen").children(".pagelet.entry"+gFormIndex).remove(); + } else { + gWebSocket.write([["clear_last_entry", gFormIndex+""]]); + gWebSocket.term_input(text+"\n"); + } + gForm = null; + gFormIndex = null; +} + +function GTFormSubmit(text, cancel) { + GTEndForm(text, cancel); +} + +function GTFormHelp(evt) { + var helpStr = $(this).attr("data-gtermhelp"); +} + +function GTFormCommand(evt) { + if ($(this).hasClass("gterm-form-cancel")) + GTEndForm("", true); + + var formElem = $(this).closest(".gterm-form"); + var argNames = $(this).attr("data-gtermformargs").split(","); + var command = $(this).attr("data-gtermformcmd"); + var argStr = ""; + for (var j=0; j -1) + inputValue = '"' + inputValue + '"'; + if (inputValue) { + if (argName.substr(0,3) == "arg") { + // Command line arguments + argStr += ' ' + inputValue; + } else { + // Command options + command += ' --' + argName + '=' + inputValue; + } + } + } + command += argStr; + console.log("GTFormCommand", this, formElem, command, evt); + GTEndForm(command); +} + +function StartFullscreen(split) { + $("#session-bufscreen").children().hide(); + if (split) { + $("#session-bufellipsis").show(); + if (gAlwaysSplitScreen && !gSplitScreen) + SplitScreen("fullscreen"); + } +} + +function EndFullscreen() { + $("#session-bufscreen span.row:not(.gterm-hideoutput)").show(); + $("#session-bufscreen span.row.promptrow").show(); + $("#session-bufellipsis").hide(); + if (gSplitScreen) + MergeScreen("fullscreen"); +} + +function ExitFullscreen() { + EndFullscreen(); + var offset = $(document).height() - $(window).height(); + if (offset > 0) + ScrollTop(offset); +} + +function SplitScreen(code) { + //console.log("SplitScreen: ", code); + $("#session-term").addClass("split-screen"); + gSplitScreen = true; + ResizeSplitScreen(true); +} + +function MergeScreen(code) { + //console.log("MergeScreen: "+code); + $("#session-term").removeClass("split-screen"); + gSplitScreen = false; + ResizeSplitScreen(false); +} + +function ResizeSplitScreen(split) { + //console.log("ResizeSplitScreen:", split); + gMaxScrollOffset = 0; + if (split) { + // NOTE: Does this need to be delayed using SetTimeout? + $("#session-bufscreen").css("margin-bottom", (10+$("#session-screencontainer").height())+"px"); + $("#session-findercontainer").css("margin-bottom", (10+$("#session-screencontainer").height())+"px"); + } else { + $("#session-bufscreen").css("margin-bottom", ""); + $("#session-findercontainer").css("margin-bottom", ""); + } +} + +function ToggleFooter() { + if ($("#session-term").hasClass("display-footer")) + HideFooter(); + else + DisplayFooter() +} + +function DisplayFooter() { + $("#session-term").addClass("display-footer"); + ScrollScreen(); +} + +function HideFooter() { + $("#session-term").removeClass("display-footer"); +} + +function GetFinder(kind) { + if (gWebSocket && gWebSocket.terminal) + gWebSocket.write([["get_finder", kind || "", ""]]); +} + +function ShowFinder(params, content) { + //console.log("ShowFinder: ", params); + gShowingFinder = true; + var current_dir = params.current_directory; + var finder_html = '
'+content+'
\n'; + + $("#session-finderbody").html(finder_html); + $("#session-finderbody .gterm-rowimg").addClass("icons"); + if (!gWebSocket.icons) + $("#session-finderbody .gterm-rowimg").hide(); + + $('#session-finderbody td .gterm-link').bindclick(gtermFinderClickHandler) + + if (!gSplitScreen) + SplitScreen("finder"); + $("#gterm-header").hide(); + $("#session-bufellipsis").hide(); + $("#session-bufscreen").hide(); + $("#session-findercontainer").show(); +} + +function HideFinder() { + $("#session-findercontainer").hide(); + $("#session-finderbody").empty(); + $("#session-bufscreen").show(); + $("#gterm-header").show(); + gShowingFinder = false; + MergeScreen("finder"); + ScrollScreen(); +} + +// HTML5 Drag/Drop +function GTPreventHandler(evt) { + if (evt.preventDefault) evt.preventDefault(); + if (evt.stopPropagation) evt.stopPropagation(); + return false; +} + +function GTDragBindings(elem) { + elem.bind('dragstart', GTDragStart); +} + +function GTDropBindings(elem) { + //console.log("GTDropBindings", elem); + if (!elem.length) + return; + try { + elem.bind('dragover', GTDragOver); + elem.bind('drop', GTDropHandler); + } catch(err) { + //console.log("GTDropBindings:", err); + } +} + +var gtDragElement = null; + +function GTDragStart(evt) { + gtDragElement = this; + //evt.dataTransfer.effectAllowed = 'move'; + //evt.dataTransfer.setData('text/html', this.innerHTML); +} + +function GTDragOver(evt) { + //console.log("GTDragOver", evt); + if (evt.originalEvent) + evt.originalEvent.dropEffect = "copy"; + else + evt.dropEffect = "none"; + return GTPreventHandler(evt); +} + +function GTDropHandler(evt) { + console.log("GTDropHandler", this, evt); + if (gtDragElement != this) { + try { + var gterm_mime = $(this).attr("data-gtermmime") || ""; + var gterm_uri = $(this).attr("href") || ""; + if (evt.originalEvent.dataTransfer.files.length) { + } else { + var text = evt.originalEvent.dataTransfer.getData("text/plain") || ""; + var file_uri = ""; + if (text) { + var comps = splitFileURI(text); + if (comps.length) { + file_uri = text; + text = comps[1]; + } + var options = {}; + if (gterm_mime == "x-graphterm/directory") { + options.command = "mv"; + options.dest_uri = gterm_uri; + options.enter = true; + gtermClickPaste("", file_uri, options); + } else if (gterm_mime == "x-graphterm/executable") { + options.command = splitFileURI(gterm_uri)[2]; + gtermClickPaste("", file_uri, options); + } else { + gtermClickPaste(text, file_uri, options); + } + } + } + } catch (err) { + console.log("graphterm: GTDropHandler: "+err); + } + } + + return GTPreventHandler(evt); +} + +function ScrollEventHandler(event) { + if (gProgrammaticScroll) { + gProgrammaticScroll = false; + gManualScroll = false; + return + } + + // Non-programatic scroll event + //console.log("ScrollEventHandler"); + gManualScroll = true; + if (gWebSocket && gWebSocket.terminal && !gWebSocket.alt_mode) { + var nrows = $("#session-screen > span.row").length; + if (gSplitScreen) { + if (gAlwaysSplitScreen) { + // Unsplit screen if not single command line + if (nrows != 1 || !$("#session-screen > span.row span.gterm-cmd-prompt").length) + MergeScreen("scrollevent"); + } else { + // Merge screen if scrolling down to bottom (after scrolling up at least a bit) + var offset = $("#session-bufscreen").offset().top+$("#session-bufscreen").height() - $("#session-screencontainer").offset().top; + //console.log("ScrollEvent: offset", offset); + if (offset < -5 && gMaxScrollOffset > 0) + MergeScreen("scrollevent"); + gMaxScrollOffset = Math.max(offset, gMaxScrollOffset); + } + + } else if (gAlwaysSplitScreen && nrows == 1 && $("#session-screen > span.row span.gterm-cmd-prompt").length) { + // Split screen if command line non-empty + var nprompt = $("#session-screen > span.row span.gterm-cmd-prompt").text().length; + if ($("#session-screen > span.row").text().length > nprompt+1) + SplitScreen("scroll"); + } + } +} + +function ScrollTop(offset) { + gProgrammaticScroll = true; + $(window).scrollTop(offset); +} + +function GTShowSplash(animate) { + if ($("#gtermsplash").hasClass("hidesplash")) + return; + $("#gtermsplash").removeClass("noshow"); +} + +function GTHideSplash(animate) { + if ($("#gtermsplash").hasClass("hidesplash")) + return; + $("#gtermsplash").addClass("hidesplash"); + if (animate) { + $("#gtermsplash").animate({ + "margin-top": "+=300px", + opacity: 0.0, + }, 1000, function() { $("#gtermsplash").hide(); }); + } else { + $("#gtermsplash").hide(); + } +} + +function ScrollScreen(alt_mode) { + if (gTestBatchedScroll && !gManualScroll) + return; + var screen_id = alt_mode ? "#session-altscreen" : "#session-screencontainer"; + var bot_offset = $(screen_id).offset().top + $(screen_id).height(); + var winHeight = $(window).height(); + if (gMobileBrowser && winHeight != window.innerHeight) + winHeight = Math.min($(window).height(), window.innerHeight) - (window.orientation?50:25); + if (bot_offset > winHeight) + ScrollTop(bot_offset - winHeight + gBottomMargin); + else + ScrollTop(0); +} + +$(document).ready(function() { + //LoadHandler(); + console.log("Ready"); + $(document).attr("title", window.location.pathname.substr(1)); + + if (gSafariIPad) + $("body").addClass("ipadscreen"); + + setupTerminal(); + $("#acearea").hide(); + $("#session-bufellipsis").hide(); + $("#session-findercontainer").hide(); + $(".menubar-select").change(gtermSelectHandler); + $("#session-headermenu .headfoot").bindclick(gtermMenuClickHandler); + $("#session-footermenu .headfoot").bindclick(gtermMenuClickHandler); + + //window.addEventListener("dragover", GTDragOver); + window.addEventListener("drop", GTDropHandler); + + $(".gterm-popup").hide(); + $(".gterm-popupmask").hide(); + $(".gterm-popupmask, .gterm-popupclose").click(popupClose); + + if (gMobileBrowser) + GTHideSplash(); + + $(GTNextEntry()).appendTo("#session-log"); + + $(window).scroll(ScrollEventHandler); + + + // Bind window keydown/keypress events + $(document).keydown(keydownHandler); + $(document).keypress(keypressHandler); + + $(window).resize(handle_resize); + + Connect(); +}); diff --git a/graphterm/www/images/tango-application-x-executable.png b/graphterm/www/images/tango-application-x-executable.png new file mode 100644 index 0000000..8a150b8 Binary files /dev/null and b/graphterm/www/images/tango-application-x-executable.png differ diff --git a/graphterm/www/images/tango-audio-x-generic.png b/graphterm/www/images/tango-audio-x-generic.png new file mode 100644 index 0000000..c60b595 Binary files /dev/null and b/graphterm/www/images/tango-audio-x-generic.png differ diff --git a/graphterm/www/images/tango-folder.png b/graphterm/www/images/tango-folder.png new file mode 100644 index 0000000..472484f Binary files /dev/null and b/graphterm/www/images/tango-folder.png differ diff --git a/graphterm/www/images/tango-image-x-generic.png b/graphterm/www/images/tango-image-x-generic.png new file mode 100644 index 0000000..6f118cd Binary files /dev/null and b/graphterm/www/images/tango-image-x-generic.png differ diff --git a/graphterm/www/images/tango-text-html.png b/graphterm/www/images/tango-text-html.png new file mode 100644 index 0000000..a896697 Binary files /dev/null and b/graphterm/www/images/tango-text-html.png differ diff --git a/graphterm/www/images/tango-text-x-generic-template.png b/graphterm/www/images/tango-text-x-generic-template.png new file mode 100644 index 0000000..5b7e649 Binary files /dev/null and b/graphterm/www/images/tango-text-x-generic-template.png differ diff --git a/graphterm/www/images/tango-text-x-generic.png b/graphterm/www/images/tango-text-x-generic.png new file mode 100644 index 0000000..928a679 Binary files /dev/null and b/graphterm/www/images/tango-text-x-generic.png differ diff --git a/graphterm/www/images/tango-text-x-script.png b/graphterm/www/images/tango-text-x-script.png new file mode 100644 index 0000000..801dcd6 Binary files /dev/null and b/graphterm/www/images/tango-text-x-script.png differ diff --git a/graphterm/www/images/tango-video-x-generic.png b/graphterm/www/images/tango-video-x-generic.png new file mode 100644 index 0000000..5d6c8d1 Binary files /dev/null and b/graphterm/www/images/tango-video-x-generic.png differ diff --git a/graphterm/www/index.html b/graphterm/www/index.html new file mode 100644 index 0000000..45168f6 --- /dev/null +++ b/graphterm/www/index.html @@ -0,0 +1,185 @@ + + + + GraphTerm page + + + + + + + + + + + + + + + + + +
+ User:
+ Code:
+ Authenticate +

+ +

+ +
+
+ + +   + + +   + Icons: + + +   + Webcast: + + +   + Theme: + + +

+

+ Home + Bottom + Collapse + Expand + Detach + New + Help + About +
+ +
+ +
+ +
+
+ + +
+          
+ +
+ Close (FINDER NOT YET IMPLEMENTED!) +
+
+
+ +
+ +
0
+1
+2
+3
+4
+5
+6
+7
+8
+9
+0
+1
+2
+3
+4
+5
+6
+7
+8
+9
+0
+1
+2
+3
+01234567890123456789012345678901234567890123456789012345678901234567890123456789
+
+
+ Control + Top   +    +    +    + <   + >   + Clear + Cmd + Opt + File + Enter + +
+
+
+
+          
+
+ +
+
+
+
+
+ +
+ +
+ + +
+
+

GraphTerm is a graphical terminal interface the blends +the command line with the graphical user interface.

+

+

Type a Bash shell command or click Home on the menubar to get started...

+

+

GraphTerm was developed as part of the Mindmeldr project. +For more information, see info.mindmeldr.com/code/graphterm

+
+
+ + +
+ Ajax.org Cloud9 Editor + + +
+ + +
+ + + +
+
+ + + diff --git a/graphterm/www/jquery/jquery.min.js b/graphterm/www/jquery/jquery.min.js new file mode 100644 index 0000000..16ad06c --- /dev/null +++ b/graphterm/www/jquery/jquery.min.js @@ -0,0 +1,4 @@ +/*! jQuery v1.7.2 jquery.com | jquery.org/license */ +(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cu(a){if(!cj[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ck||(ck=c.createElement("iframe"),ck.frameBorder=ck.width=ck.height=0),b.appendChild(ck);if(!cl||!ck.createElement)cl=(ck.contentWindow||ck.contentDocument).document,cl.write((f.support.boxModel?"":"")+""),cl.close();d=cl.createElement(a),cl.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ck)}cj[a]=e}return cj[a]}function ct(a,b){var c={};f.each(cp.concat.apply([],cp.slice(0,b)),function(){c[this]=a});return c}function cs(){cq=b}function cr(){setTimeout(cs,0);return cq=f.now()}function ci(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ch(){try{return new a.XMLHttpRequest}catch(b){}}function cb(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){if(c!=="border")for(;e=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?+d:j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.2",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){if(typeof c!="string"||!c)return null;var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c
a",d=p.getElementsByTagName("*"),e=p.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=p.getElementsByTagName("input")[0],b={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:p.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,pixelMargin:!0},f.boxModel=b.boxModel=c.compatMode==="CSS1Compat",i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete p.test}catch(r){b.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",function(){b.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),i.setAttribute("name","t"),p.appendChild(i),j=c.createDocumentFragment(),j.appendChild(p.lastChild),b.checkClone=j.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,j.removeChild(i),j.appendChild(p);if(p.attachEvent)for(n in{submit:1,change:1,focusin:1})m="on"+n,o=m in p,o||(p.setAttribute(m,"return;"),o=typeof p[m]=="function"),b[n+"Bubbles"]=o;j.removeChild(p),j=g=h=p=i=null,f(function(){var d,e,g,h,i,j,l,m,n,q,r,s,t,u=c.getElementsByTagName("body")[0];!u||(m=1,t="padding:0;margin:0;border:",r="position:absolute;top:0;left:0;width:1px;height:1px;",s=t+"0;visibility:hidden;",n="style='"+r+t+"5px solid #000;",q="
"+""+"
",d=c.createElement("div"),d.style.cssText=s+"width:0;height:0;position:static;top:0;margin-top:"+m+"px",u.insertBefore(d,u.firstChild),p=c.createElement("div"),d.appendChild(p),p.innerHTML="
t
",k=p.getElementsByTagName("td"),o=k[0].offsetHeight===0,k[0].style.display="",k[1].style.display="none",b.reliableHiddenOffsets=o&&k[0].offsetHeight===0,a.getComputedStyle&&(p.innerHTML="",l=c.createElement("div"),l.style.width="0",l.style.marginRight="0",p.style.width="2px",p.appendChild(l),b.reliableMarginRight=(parseInt((a.getComputedStyle(l,null)||{marginRight:0}).marginRight,10)||0)===0),typeof p.style.zoom!="undefined"&&(p.innerHTML="",p.style.width=p.style.padding="1px",p.style.border=0,p.style.overflow="hidden",p.style.display="inline",p.style.zoom=1,b.inlineBlockNeedsLayout=p.offsetWidth===3,p.style.display="block",p.style.overflow="visible",p.innerHTML="
",b.shrinkWrapBlocks=p.offsetWidth!==3),p.style.cssText=r+s,p.innerHTML=q,e=p.firstChild,g=e.firstChild,i=e.nextSibling.firstChild.firstChild,j={doesNotAddBorder:g.offsetTop!==5,doesAddBorderForTableAndCells:i.offsetTop===5},g.style.position="fixed",g.style.top="20px",j.fixedPosition=g.offsetTop===20||g.offsetTop===15,g.style.position=g.style.top="",e.style.overflow="hidden",e.style.position="relative",j.subtractsBorderForOverflowNotVisible=g.offsetTop===-5,j.doesNotIncludeMarginInBodyOffset=u.offsetTop!==m,a.getComputedStyle&&(p.style.marginTop="1%",b.pixelMargin=(a.getComputedStyle(p,null)||{marginTop:0}).marginTop!=="1%"),typeof d.style.zoom!="undefined"&&(d.style.zoom=1),u.removeChild(d),l=p=d=null,f.extend(b,j))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e1,null,!1)},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,b){a&&(b=(b||"fx")+"mark",f._data(a,b,(f._data(a,b)||0)+1))},_unmark:function(a,b,c){a!==!0&&(c=b,b=a,a=!1);if(b){c=c||"fx";var d=c+"mark",e=a?0:(f._data(b,d)||1)-1;e?f._data(b,d,e):(f.removeData(b,d,!0),n(b,c,"mark"))}},queue:function(a,b,c){var d;if(a){b=(b||"fx")+"queue",d=f._data(a,b),c&&(!d||f.isArray(c)?d=f._data(a,b,f.makeArray(c)):d.push(c));return d||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e={};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),f._data(a,b+".run",e),d.call(a,function(){f.dequeue(a,b)},e)),c.length||(f.removeData(a,b+"queue "+b+".run",!0),n(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){var d=2;typeof a!="string"&&(c=a,a="fx",d--);if(arguments.length1)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,f.prop,a,b,arguments.length>1)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(p);for(c=0,d=this.length;c-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.type]||f.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.type]||f.valHooks[g.nodeName.toLowerCase()];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h,i=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;i=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/(?:^|\s)hover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function( +a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")};f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler,g=p.selector),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;le&&j.push({elem:this,matches:d.slice(e)});for(k=0;k0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));o.match.globalPOS=p;var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/]","i"),bd=/checked\s*(?:[^=]|=\s*.checked.)/i,be=/\/(java|ecma)script/i,bf=/^\s*",""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div
","
"]),f.fn.extend({text:function(a){return f.access(this,function(a){return a===b?f.text(this):this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f +.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){return f.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>");try{for(;d1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||f.isXMLDoc(a)||!bc.test("<"+a.nodeName+">")?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g,h,i,j=[];b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);for(var k=0,l;(l=a[k])!=null;k++){typeof l=="number"&&(l+="");if(!l)continue;if(typeof l=="string")if(!_.test(l))l=b.createTextNode(l);else{l=l.replace(Y,"<$1>");var m=(Z.exec(l)||["",""])[1].toLowerCase(),n=bg[m]||bg._default,o=n[0],p=b.createElement("div"),q=bh.childNodes,r;b===c?bh.appendChild(p):U(b).appendChild(p),p.innerHTML=n[1]+l+n[2];while(o--)p=p.lastChild;if(!f.support.tbody){var s=$.test(l),t=m==="table"&&!s?p.firstChild&&p.firstChild.childNodes:n[1]===""&&!s?p.childNodes:[];for(i=t.length-1;i>=0;--i)f.nodeName(t[i],"tbody")&&!t[i].childNodes.length&&t[i].parentNode.removeChild(t[i])}!f.support.leadingWhitespace&&X.test(l)&&p.insertBefore(b.createTextNode(X.exec(l)[0]),p.firstChild),l=p.childNodes,p&&(p.parentNode.removeChild(p),q.length>0&&(r=q[q.length-1],r&&r.parentNode&&r.parentNode.removeChild(r)))}var u;if(!f.support.appendChecked)if(l[0]&&typeof (u=l.length)=="number")for(i=0;i1)},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=by(a,"opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d,h==="string"&&(g=bu.exec(d))&&(d=+(g[1]+1)*+g[2]+parseFloat(f.css(a,c)),h="number");if(d==null||h==="number"&&isNaN(d))return;h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(by)return by(a,c)},swap:function(a,b,c){var d={},e,f;for(f in b)d[f]=a.style[f],a.style[f]=b[f];e=c.call(a);for(f in b)a.style[f]=d[f];return e}}),f.curCSS=f.css,c.defaultView&&c.defaultView.getComputedStyle&&(bz=function(a,b){var c,d,e,g,h=a.style;b=b.replace(br,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b))),!f.support.pixelMargin&&e&&bv.test(b)&&bt.test(c)&&(g=h.width,h.width=c,c=e.width,h.width=g);return c}),c.documentElement.currentStyle&&(bA=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f==null&&g&&(e=g[b])&&(f=e),bt.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),by=bz||bA,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth!==0?bB(a,b,d):f.swap(a,bw,function(){return bB(a,b,d)})},set:function(a,b){return bs.test(b)?b+"px":b}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bq.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bp,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bp.test(g)?g.replace(bp,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){return f.swap(a,{display:"inline-block"},function(){return b?by(a,"margin-right"):a.style.marginRight})}})}),f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)}),f.each({margin:"",padding:"",border:"Width"},function(a,b){f.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bx[d]+b]=e[d]||e[d-2]||e[0];return f}}});var bC=/%20/g,bD=/\[\]$/,bE=/\r?\n/g,bF=/#.*$/,bG=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bH=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bI=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bJ=/^(?:GET|HEAD)$/,bK=/^\/\//,bL=/\?/,bM=/)<[^<]*)*<\/script>/gi,bN=/^(?:select|textarea)/i,bO=/\s+/,bP=/([?&])_=[^&]*/,bQ=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bR=f.fn.load,bS={},bT={},bU,bV,bW=["*/"]+["*"];try{bU=e.href}catch(bX){bU=c.createElement("a"),bU.href="",bU=bU.href}bV=bQ.exec(bU.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bR)return bR.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
").append(c.replace(bM,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bN.test(this.nodeName)||bH.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bE,"\r\n")}}):{name:b.name,value:c.replace(bE,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b$(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b$(a,b);return a},ajaxSettings:{url:bU,isLocal:bI.test(bV[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bW},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bY(bS),ajaxTransport:bY(bT),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?ca(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cb(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bG.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bF,"").replace(bK,bV[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bO),d.crossDomain==null&&(r=bQ.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bV[1]&&r[2]==bV[2]&&(r[3]||(r[1]==="http:"?80:443))==(bV[3]||(bV[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bZ(bS,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bJ.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bL.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bP,"$1_="+x);d.url=y+(y===d.url?(bL.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bW+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bZ(bT,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)b_(g,a[g],c,e);return d.join("&").replace(bC,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cc=f.now(),cd=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cc++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=typeof b.data=="string"&&/^application\/x\-www\-form\-urlencoded/.test(b.contentType);if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cd.test(b.url)||e&&cd.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cd,l),b.url===j&&(e&&(k=k.replace(cd,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var ce=a.ActiveXObject?function(){for(var a in cg)cg[a](0,1)}:!1,cf=0,cg;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ch()||ci()}:ch,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,ce&&delete cg[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n);try{m.text=h.responseText}catch(a){}try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cf,ce&&(cg||(cg={},f(a).unload(ce)),cg[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cj={},ck,cl,cm=/^(?:toggle|show|hide)$/,cn=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,co,cp=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cq;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(ct("show",3),a,b,c);for(var g=0,h=this.length;g=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);f.fn[a]=function(e){return f.access(this,function(a,e,g){var h=cy(a);if(g===b)return h?c in h?h[c]:f.support.boxModel&&h.document.documentElement[e]||h.document.body[e]:a[e];h?h.scrollTo(d?f(h).scrollLeft():g,d?g:f(h).scrollTop()):a[e]=g},a,e,arguments.length,null)}}),f.each({Height:"height",Width:"width"},function(a,c){var d="client"+a,e="scroll"+a,g="offset"+a;f.fn["inner"+a]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,c,"padding")):this[c]():null},f.fn["outer"+a]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,c,a?"margin":"border")):this[c]():null},f.fn[c]=function(a){return f.access(this,function(a,c,h){var i,j,k,l;if(f.isWindow(a)){i=a.document,j=i.documentElement[d];return f.support.boxModel&&j||i.body&&i.body[d]||j}if(a.nodeType===9){i=a.documentElement;if(i[d]>=i[e])return i[d];return Math.max(a.body[e],i[e],a.body[g],i[g])}if(h===b){k=f.css(a,c),l=parseFloat(k);return f.isNumeric(l)?l:k}f(a).css(c,h)},c,a,arguments.length,null)}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); \ No newline at end of file diff --git a/graphterm/www/jquery/js-plugins/rangy-core.js b/graphterm/www/jquery/js-plugins/rangy-core.js new file mode 100644 index 0000000..8549d10 --- /dev/null +++ b/graphterm/www/jquery/js-plugins/rangy-core.js @@ -0,0 +1,94 @@ +/* + Rangy, a cross-browser JavaScript range and selection library + http://code.google.com/p/rangy/ + + Copyright 2012, Tim Down + Licensed under the MIT license. + Version: 1.2.3 + Build date: 26 February 2012 +*/ +window.rangy=function(){function l(p,u){var w=typeof p[u];return w=="function"||!!(w=="object"&&p[u])||w=="unknown"}function K(p,u){return!!(typeof p[u]=="object"&&p[u])}function H(p,u){return typeof p[u]!="undefined"}function I(p){return function(u,w){for(var B=w.length;B--;)if(!p(u,w[B]))return false;return true}}function z(p){return p&&A(p,x)&&v(p,t)}function C(p){window.alert("Rangy not supported in your browser. Reason: "+p);c.initialized=true;c.supported=false}function N(){if(!c.initialized){var p, +u=false,w=false;if(l(document,"createRange")){p=document.createRange();if(A(p,n)&&v(p,i))u=true;p.detach()}if((p=K(document,"body")?document.body:document.getElementsByTagName("body")[0])&&l(p,"createTextRange")){p=p.createTextRange();if(z(p))w=true}!u&&!w&&C("Neither Range nor TextRange are implemented");c.initialized=true;c.features={implementsDomRange:u,implementsTextRange:w};u=k.concat(f);w=0;for(p=u.length;w["+c.childNodes.length+"]":c.nodeName}function n(c){this._next=this.root=c}function t(c,f){this.node=c;this.offset=f}function x(c){this.code=this[c]; +this.codeName=c;this.message="DOMException: "+this.codeName}var A=l.util;A.areHostMethods(document,["createDocumentFragment","createElement","createTextNode"])||K.fail("document missing a Node creation method");A.isHostMethod(document,"getElementsByTagName")||K.fail("document missing getElementsByTagName method");var q=document.createElement("div");A.areHostMethods(q,["insertBefore","appendChild","cloneNode"])||K.fail("Incomplete Element implementation");A.isHostProperty(q,"innerHTML")||K.fail("Element is missing innerHTML property"); +q=document.createTextNode("test");A.areHostMethods(q,["splitText","deleteData","insertData","appendData","cloneNode"])||K.fail("Incomplete Text Node implementation");var v=function(c,f){for(var k=c.length;k--;)if(c[k]===f)return true;return false};n.prototype={_current:null,hasNext:function(){return!!this._next},next:function(){var c=this._current=this._next,f;if(this._current)if(f=c.firstChild)this._next=f;else{for(f=null;c!==this.root&&!(f=c.nextSibling);)c=c.parentNode;this._next=f}return this._current}, +detach:function(){this._current=this._next=this.root=null}};t.prototype={equals:function(c){return this.node===c.node&this.offset==c.offset},inspect:function(){return"[DomPosition("+i(this.node)+":"+this.offset+")]"}};x.prototype={INDEX_SIZE_ERR:1,HIERARCHY_REQUEST_ERR:3,WRONG_DOCUMENT_ERR:4,NO_MODIFICATION_ALLOWED_ERR:7,NOT_FOUND_ERR:8,NOT_SUPPORTED_ERR:9,INVALID_STATE_ERR:11};x.prototype.toString=function(){return this.message};l.dom={arrayContains:v,isHtmlNamespace:function(c){var f;return typeof c.namespaceURI== +"undefined"||(f=c.namespaceURI)===null||f=="http://www.w3.org/1999/xhtml"},parentElement:function(c){c=c.parentNode;return c.nodeType==1?c:null},getNodeIndex:H,getNodeLength:function(c){var f;return C(c)?c.length:(f=c.childNodes)?f.length:0},getCommonAncestor:I,isAncestorOf:function(c,f,k){for(f=k?f:f.parentNode;f;)if(f===c)return true;else f=f.parentNode;return false},getClosestAncestorIn:z,isCharacterDataNode:C,insertAfter:N,splitDataNode:function(c,f){var k=c.cloneNode(false);k.deleteData(0,f); +c.deleteData(f,c.length-f);N(k,c);return k},getDocument:O,getWindow:function(c){c=O(c);if(typeof c.defaultView!="undefined")return c.defaultView;else if(typeof c.parentWindow!="undefined")return c.parentWindow;else throw Error("Cannot get a window object for node");},getIframeWindow:function(c){if(typeof c.contentWindow!="undefined")return c.contentWindow;else if(typeof c.contentDocument!="undefined")return c.contentDocument.defaultView;else throw Error("getIframeWindow: No Window object found for iframe element"); +},getIframeDocument:function(c){if(typeof c.contentDocument!="undefined")return c.contentDocument;else if(typeof c.contentWindow!="undefined")return c.contentWindow.document;else throw Error("getIframeWindow: No Document object found for iframe element");},getBody:function(c){return A.isHostObject(c,"body")?c.body:c.getElementsByTagName("body")[0]},getRootContainer:function(c){for(var f;f=c.parentNode;)c=f;return c},comparePoints:function(c,f,k,r){var L;if(c==k)return f===r?0:f=e.childNodes.length?e.appendChild(a):e.insertBefore(a,e.childNodes[j]);return o}function O(a){for(var e,j,o=H(a.range).createDocumentFragment();j=a.next();){e=a.isPartiallySelectedSubtree();j=j.cloneNode(!e);if(e){e=a.getSubtreeIterator();j.appendChild(O(e));e.detach(true)}if(j.nodeType==10)throw new S("HIERARCHY_REQUEST_ERR");o.appendChild(j)}return o}function i(a,e,j){var o,E;for(j=j||{stop:false};o=a.next();)if(a.isPartiallySelectedSubtree())if(e(o)=== +false){j.stop=true;return}else{o=a.getSubtreeIterator();i(o,e,j);o.detach(true);if(j.stop)return}else for(o=g.createIterator(o);E=o.next();)if(e(E)===false){j.stop=true;return}}function n(a){for(var e;a.next();)if(a.isPartiallySelectedSubtree()){e=a.getSubtreeIterator();n(e);e.detach(true)}else a.remove()}function t(a){for(var e,j=H(a.range).createDocumentFragment(),o;e=a.next();){if(a.isPartiallySelectedSubtree()){e=e.cloneNode(false);o=a.getSubtreeIterator();e.appendChild(t(o));o.detach(true)}else a.remove(); +if(e.nodeType==10)throw new S("HIERARCHY_REQUEST_ERR");j.appendChild(e)}return j}function x(a,e,j){var o=!!(e&&e.length),E,T=!!j;if(o)E=RegExp("^("+e.join("|")+")$");var m=[];i(new q(a,false),function(s){if((!o||E.test(s.nodeType))&&(!T||j(s)))m.push(s)});return m}function A(a){return"["+(typeof a.getName=="undefined"?"Range":a.getName())+"("+g.inspectNode(a.startContainer)+":"+a.startOffset+", "+g.inspectNode(a.endContainer)+":"+a.endOffset+")]"}function q(a,e){this.range=a;this.clonePartiallySelectedTextNodes= +e;if(!a.collapsed){this.sc=a.startContainer;this.so=a.startOffset;this.ec=a.endContainer;this.eo=a.endOffset;var j=a.commonAncestorContainer;if(this.sc===this.ec&&g.isCharacterDataNode(this.sc)){this.isSingleCharacterDataNode=true;this._first=this._last=this._next=this.sc}else{this._first=this._next=this.sc===j&&!g.isCharacterDataNode(this.sc)?this.sc.childNodes[this.so]:g.getClosestAncestorIn(this.sc,j,true);this._last=this.ec===j&&!g.isCharacterDataNode(this.ec)?this.ec.childNodes[this.eo-1]:g.getClosestAncestorIn(this.ec, +j,true)}}}function v(a){this.code=this[a];this.codeName=a;this.message="RangeException: "+this.codeName}function c(a,e,j){this.nodes=x(a,e,j);this._next=this.nodes[0];this._position=0}function f(a){return function(e,j){for(var o,E=j?e:e.parentNode;E;){o=E.nodeType;if(g.arrayContains(a,o))return E;E=E.parentNode}return null}}function k(a,e){if(G(a,e))throw new v("INVALID_NODE_TYPE_ERR");}function r(a){if(!a.startContainer)throw new S("INVALID_STATE_ERR");}function L(a,e){if(!g.arrayContains(e,a.nodeType))throw new v("INVALID_NODE_TYPE_ERR"); +}function p(a,e){if(e<0||e>(g.isCharacterDataNode(a)?a.length:a.childNodes.length))throw new S("INDEX_SIZE_ERR");}function u(a,e){if(h(a,true)!==h(e,true))throw new S("WRONG_DOCUMENT_ERR");}function w(a){if(D(a,true))throw new S("NO_MODIFICATION_ALLOWED_ERR");}function B(a,e){if(!a)throw new S(e);}function V(a){return!!a.startContainer&&!!a.endContainer&&!(!g.arrayContains(ba,a.startContainer.nodeType)&&!h(a.startContainer,true))&&!(!g.arrayContains(ba,a.endContainer.nodeType)&&!h(a.endContainer, +true))&&a.startOffset<=(g.isCharacterDataNode(a.startContainer)?a.startContainer.length:a.startContainer.childNodes.length)&&a.endOffset<=(g.isCharacterDataNode(a.endContainer)?a.endContainer.length:a.endContainer.childNodes.length)}function J(a){r(a);if(!V(a))throw Error("Range error: Range is no longer valid after DOM mutation ("+a.inspect()+")");}function ca(){}function Y(a){a.START_TO_START=ia;a.START_TO_END=la;a.END_TO_END=ra;a.END_TO_START=ma;a.NODE_BEFORE=na;a.NODE_AFTER=oa;a.NODE_BEFORE_AND_AFTER= +pa;a.NODE_INSIDE=ja}function W(a){Y(a);Y(a.prototype)}function da(a,e){return function(){J(this);var j=this.startContainer,o=this.startOffset,E=this.commonAncestorContainer,T=new q(this,true);if(j!==E){j=g.getClosestAncestorIn(j,E,true);o=C(j);j=o.node;o=o.offset}i(T,w);T.reset();E=a(T);T.detach();e(this,j,o,j,o);return E}}function fa(a,e,j){function o(m,s){return function(y){r(this);L(y,$);L(d(y),ba);y=(m?z:C)(y);(s?E:T)(this,y.node,y.offset)}}function E(m,s,y){var F=m.endContainer,Q=m.endOffset; +if(s!==m.startContainer||y!==m.startOffset){if(d(s)!=d(F)||g.comparePoints(s,y,F,Q)==1){F=s;Q=y}e(m,s,y,F,Q)}}function T(m,s,y){var F=m.startContainer,Q=m.startOffset;if(s!==m.endContainer||y!==m.endOffset){if(d(s)!=d(F)||g.comparePoints(s,y,F,Q)==-1){F=s;Q=y}e(m,F,Q,s,y)}}a.prototype=new ca;l.util.extend(a.prototype,{setStart:function(m,s){r(this);k(m,true);p(m,s);E(this,m,s)},setEnd:function(m,s){r(this);k(m,true);p(m,s);T(this,m,s)},setStartBefore:o(true,true),setStartAfter:o(false,true),setEndBefore:o(true, +false),setEndAfter:o(false,false),collapse:function(m){J(this);m?e(this,this.startContainer,this.startOffset,this.startContainer,this.startOffset):e(this,this.endContainer,this.endOffset,this.endContainer,this.endOffset)},selectNodeContents:function(m){r(this);k(m,true);e(this,m,0,m,g.getNodeLength(m))},selectNode:function(m){r(this);k(m,false);L(m,$);var s=z(m);m=C(m);e(this,s.node,s.offset,m.node,m.offset)},extractContents:da(t,e),deleteContents:da(n,e),canSurroundContents:function(){J(this);w(this.startContainer); +w(this.endContainer);var m=new q(this,true),s=m._first&&K(m._first,this)||m._last&&K(m._last,this);m.detach();return!s},detach:function(){j(this)},splitBoundaries:function(){J(this);var m=this.startContainer,s=this.startOffset,y=this.endContainer,F=this.endOffset,Q=m===y;g.isCharacterDataNode(y)&&F>0&&F0&&s=g.getNodeIndex(m)&&F++;s=0}e(this,m,s,y,F)},normalizeBoundaries:function(){J(this); +var m=this.startContainer,s=this.startOffset,y=this.endContainer,F=this.endOffset,Q=function(U){var R=U.nextSibling;if(R&&R.nodeType==U.nodeType){y=U;F=U.length;U.appendData(R.data);R.parentNode.removeChild(R)}},qa=function(U){var R=U.previousSibling;if(R&&R.nodeType==U.nodeType){m=U;var sa=U.length;s=R.length;U.insertData(0,R.data);R.parentNode.removeChild(R);if(m==y){F+=s;y=m}else if(y==U.parentNode){R=g.getNodeIndex(U);if(F==R){y=U;F=sa}else F>R&&F--}}},ga=true;if(g.isCharacterDataNode(y))y.length== +F&&Q(y);else{if(F>0)(ga=y.childNodes[F-1])&&g.isCharacterDataNode(ga)&&Q(ga);ga=!this.collapsed}if(ga)if(g.isCharacterDataNode(m))s==0&&qa(m);else{if(sx";X=P.firstChild.nodeType==3}catch(ta){}l.features.htmlParsingConforms=X;var ka=["startContainer","startOffset","endContainer","endOffset", +"collapsed","commonAncestorContainer"],ia=0,la=1,ra=2,ma=3,na=0,oa=1,pa=2,ja=3;ca.prototype={attachListener:function(a,e){this._listeners[a].push(e)},compareBoundaryPoints:function(a,e){J(this);u(this.startContainer,e.startContainer);var j=a==ma||a==ia?"start":"end",o=a==la||a==ia?"start":"end";return g.comparePoints(this[j+"Container"],this[j+"Offset"],e[o+"Container"],e[o+"Offset"])},insertNode:function(a){J(this);L(a,aa);w(this.startContainer);if(g.isAncestorOf(a,this.startContainer,true))throw new S("HIERARCHY_REQUEST_ERR"); +this.setStartBefore(N(a,this.startContainer,this.startOffset))},cloneContents:function(){J(this);var a,e;if(this.collapsed)return H(this).createDocumentFragment();else{if(this.startContainer===this.endContainer&&g.isCharacterDataNode(this.startContainer)){a=this.startContainer.cloneNode(true);a.data=a.data.slice(this.startOffset,this.endOffset);e=H(this).createDocumentFragment();e.appendChild(a);return e}else{e=new q(this,true);a=O(e);e.detach()}return a}},canSurroundContents:function(){J(this);w(this.startContainer); +w(this.endContainer);var a=new q(this,true),e=a._first&&K(a._first,this)||a._last&&K(a._last,this);a.detach();return!e},surroundContents:function(a){L(a,b);if(!this.canSurroundContents())throw new v("BAD_BOUNDARYPOINTS_ERR");var e=this.extractContents();if(a.hasChildNodes())for(;a.lastChild;)a.removeChild(a.lastChild);N(a,this.startContainer,this.startOffset);a.appendChild(e);this.selectNode(a)},cloneRange:function(){J(this);for(var a=new M(H(this)),e=ka.length,j;e--;){j=ka[e];a[j]=this[j]}return a}, +toString:function(){J(this);var a=this.startContainer;if(a===this.endContainer&&g.isCharacterDataNode(a))return a.nodeType==3||a.nodeType==4?a.data.slice(this.startOffset,this.endOffset):"";else{var e=[];a=new q(this,true);i(a,function(j){if(j.nodeType==3||j.nodeType==4)e.push(j.data)});a.detach();return e.join("")}},compareNode:function(a){J(this);var e=a.parentNode,j=g.getNodeIndex(a);if(!e)throw new S("NOT_FOUND_ERR");a=this.comparePoint(e,j);e=this.comparePoint(e,j+1);return a<0?e>0?pa:na:e>0? +oa:ja},comparePoint:function(a,e){J(this);B(a,"HIERARCHY_REQUEST_ERR");u(a,this.startContainer);if(g.comparePoints(a,e,this.startContainer,this.startOffset)<0)return-1;else if(g.comparePoints(a,e,this.endContainer,this.endOffset)>0)return 1;return 0},createContextualFragment:X?function(a){var e=this.startContainer,j=g.getDocument(e);if(!e)throw new S("INVALID_STATE_ERR");var o=null;if(e.nodeType==1)o=e;else if(g.isCharacterDataNode(e))o=g.parentElement(e);o=o===null||o.nodeName=="HTML"&&g.isHtmlNamespace(g.getDocument(o).documentElement)&& +g.isHtmlNamespace(o)?j.createElement("body"):o.cloneNode(false);o.innerHTML=a;return g.fragmentFromNodeChildren(o)}:function(a){r(this);var e=H(this).createElement("body");e.innerHTML=a;return g.fragmentFromNodeChildren(e)},toHtml:function(){J(this);var a=H(this).createElement("div");a.appendChild(this.cloneContents());return a.innerHTML},intersectsNode:function(a,e){J(this);B(a,"NOT_FOUND_ERR");if(g.getDocument(a)!==H(this))return false;var j=a.parentNode,o=g.getNodeIndex(a);B(j,"NOT_FOUND_ERR"); +var E=g.comparePoints(j,o,this.endContainer,this.endOffset);j=g.comparePoints(j,o+1,this.startContainer,this.startOffset);return e?E<=0&&j>=0:E<0&&j>0},isPointInRange:function(a,e){J(this);B(a,"HIERARCHY_REQUEST_ERR");u(a,this.startContainer);return g.comparePoints(a,e,this.startContainer,this.startOffset)>=0&&g.comparePoints(a,e,this.endContainer,this.endOffset)<=0},intersectsRange:function(a,e){J(this);if(H(a)!=H(this))throw new S("WRONG_DOCUMENT_ERR");var j=g.comparePoints(this.startContainer, +this.startOffset,a.endContainer,a.endOffset),o=g.comparePoints(this.endContainer,this.endOffset,a.startContainer,a.startOffset);return e?j<=0&&o>=0:j<0&&o>0},intersection:function(a){if(this.intersectsRange(a)){var e=g.comparePoints(this.startContainer,this.startOffset,a.startContainer,a.startOffset),j=g.comparePoints(this.endContainer,this.endOffset,a.endContainer,a.endOffset),o=this.cloneRange();e==-1&&o.setStart(a.startContainer,a.startOffset);j==1&&o.setEnd(a.endContainer,a.endOffset);return o}return null}, +union:function(a){if(this.intersectsRange(a,true)){var e=this.cloneRange();g.comparePoints(a.startContainer,a.startOffset,this.startContainer,this.startOffset)==-1&&e.setStart(a.startContainer,a.startOffset);g.comparePoints(a.endContainer,a.endOffset,this.endContainer,this.endOffset)==1&&e.setEnd(a.endContainer,a.endOffset);return e}else throw new v("Ranges do not intersect");},containsNode:function(a,e){return e?this.intersectsNode(a,false):this.compareNode(a)==ja},containsNodeContents:function(a){return this.comparePoint(a, +0)>=0&&this.comparePoint(a,g.getNodeLength(a))<=0},containsRange:function(a){return this.intersection(a).equals(a)},containsNodeText:function(a){var e=this.cloneRange();e.selectNode(a);var j=e.getNodes([3]);if(j.length>0){e.setStart(j[0],0);a=j.pop();e.setEnd(a,a.length);a=this.containsRange(e);e.detach();return a}else return this.containsNodeContents(a)},createNodeIterator:function(a,e){J(this);return new c(this,a,e)},getNodes:function(a,e){J(this);return x(this,a,e)},getDocument:function(){return H(this)}, +collapseBefore:function(a){r(this);this.setEndBefore(a);this.collapse(false)},collapseAfter:function(a){r(this);this.setStartAfter(a);this.collapse(true)},getName:function(){return"DomRange"},equals:function(a){return M.rangesEqual(this,a)},isValid:function(){return V(this)},inspect:function(){return A(this)}};fa(M,ha,function(a){r(a);a.startContainer=a.startOffset=a.endContainer=a.endOffset=null;a.collapsed=a.commonAncestorContainer=null;I(a,"detach",null);a._listeners=null});l.rangePrototype=ca.prototype; +M.rangeProperties=ka;M.RangeIterator=q;M.copyComparisonConstants=W;M.createPrototypeRange=fa;M.inspect=A;M.getRangeDocument=H;M.rangesEqual=function(a,e){return a.startContainer===e.startContainer&&a.startOffset===e.startOffset&&a.endContainer===e.endContainer&&a.endOffset===e.endOffset};l.DomRange=M;l.RangeException=v}); +rangy.createModule("WrappedRange",function(l){function K(i,n,t,x){var A=i.duplicate();A.collapse(t);var q=A.parentElement();z.isAncestorOf(n,q,true)||(q=n);if(!q.canHaveHTML)return new C(q.parentNode,z.getNodeIndex(q));n=z.getDocument(q).createElement("span");var v,c=t?"StartToStart":"StartToEnd";do{q.insertBefore(n,n.previousSibling);A.moveToElementText(n)}while((v=A.compareEndPoints(c,i))>0&&n.previousSibling);c=n.nextSibling;if(v==-1&&c&&z.isCharacterDataNode(c)){A.setEndPoint(t?"EndToStart":"EndToEnd", +i);if(/[\r\n]/.test(c.data)){q=A.duplicate();t=q.text.replace(/\r\n/g,"\r").length;for(t=q.moveStart("character",t);q.compareEndPoints("StartToEnd",q)==-1;){t++;q.moveStart("character",1)}}else t=A.text.length;q=new C(c,t)}else{c=(x||!t)&&n.previousSibling;q=(t=(x||t)&&n.nextSibling)&&z.isCharacterDataNode(t)?new C(t,0):c&&z.isCharacterDataNode(c)?new C(c,c.length):new C(q,z.getNodeIndex(n))}n.parentNode.removeChild(n);return q}function H(i,n){var t,x,A=i.offset,q=z.getDocument(i.node),v=q.body.createTextRange(), +c=z.isCharacterDataNode(i.node);if(c){t=i.node;x=t.parentNode}else{t=i.node.childNodes;t=A12");d.close();var h=c.getIframeWindow(b).getSelection(),D=d.documentElement.lastChild.firstChild;d=d.createRange();d.setStart(D,1);d.collapse(true);h.addRange(d);ha=h.rangeCount==1;h.removeAllRanges();var G=d.cloneRange();d.setStart(D,0);G.setEnd(D,2);h.addRange(d);h.addRange(G);ea=h.rangeCount==2;d.detach();G.detach();Y.removeChild(b)}();l.features.selectionSupportsMultipleRanges=ea; +l.features.collapsedNonEditableSelectionsSupported=ha;var M=false,g;if(Y&&f.isHostMethod(Y,"createControlRange")){g=Y.createControlRange();if(f.areHostProperties(g,["item","add"]))M=true}l.features.implementsControlRange=M;w=W?function(b){return b.anchorNode===b.focusNode&&b.anchorOffset===b.focusOffset}:function(b){return b.rangeCount?b.getRangeAt(b.rangeCount-1).collapsed:false};var Z;if(f.isHostMethod(B,"getRangeAt"))Z=function(b,d){try{return b.getRangeAt(d)}catch(h){return null}};else if(W)Z= +function(b){var d=c.getDocument(b.anchorNode);d=l.createRange(d);d.setStart(b.anchorNode,b.anchorOffset);d.setEnd(b.focusNode,b.focusOffset);if(d.collapsed!==this.isCollapsed){d.setStart(b.focusNode,b.focusOffset);d.setEnd(b.anchorNode,b.anchorOffset)}return d};l.getSelection=function(b){b=b||window;var d=b._rangySelection,h=u(b),D=V?I(b):null;if(d){d.nativeSelection=h;d.docSelection=D;d.refresh(b)}else{d=new x(h,D,b);b._rangySelection=d}return d};l.getIframeSelection=function(b){return l.getSelection(c.getIframeWindow(b))}; +g=x.prototype;if(!J&&W&&f.areHostMethods(B,["removeAllRanges","addRange"])){g.removeAllRanges=function(){this.nativeSelection.removeAllRanges();C(this)};var S=function(b,d){var h=k.getRangeDocument(d);h=l.createRange(h);h.collapseToPoint(d.endContainer,d.endOffset);b.nativeSelection.addRange(N(h));b.nativeSelection.extend(d.startContainer,d.startOffset);b.refresh()};g.addRange=fa?function(b,d){if(M&&V&&this.docSelection.type=="Control")t(this,b);else if(d&&da)S(this,b);else{var h;if(ea)h=this.rangeCount; +else{this.removeAllRanges();h=0}this.nativeSelection.addRange(N(b));this.rangeCount=this.nativeSelection.rangeCount;if(this.rangeCount==h+1){if(l.config.checkSelectionRanges)if((h=Z(this.nativeSelection,this.rangeCount-1))&&!k.rangesEqual(h,b))b=new r(h);this._ranges[this.rangeCount-1]=b;z(this,b,aa(this.nativeSelection));this.isCollapsed=w(this)}else this.refresh()}}:function(b,d){if(d&&da)S(this,b);else{this.nativeSelection.addRange(N(b));this.refresh()}};g.setRanges=function(b){if(M&&b.length> +1)A(this,b);else{this.removeAllRanges();for(var d=0,h=b.length;d1)A(this,b);else d&&this.addRange(b[0])}}else{K.fail("No means of selecting a Range or TextRange was found");return false}g.getRangeAt=function(b){if(b<0||b>=this.rangeCount)throw new L("INDEX_SIZE_ERR");else return this._ranges[b]}; +var $;if(J)$=function(b){var d;if(l.isSelectionValid(b.win))d=b.docSelection.createRange();else{d=c.getBody(b.win.document).createTextRange();d.collapse(true)}if(b.docSelection.type=="Control")n(b);else d&&typeof d.text!="undefined"?i(b,d):C(b)};else if(f.isHostMethod(B,"getRangeAt")&&typeof B.rangeCount=="number")$=function(b){if(M&&V&&b.docSelection.type=="Control")n(b);else{b._ranges.length=b.rangeCount=b.nativeSelection.rangeCount;if(b.rangeCount){for(var d=0,h=b.rangeCount;d + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/css', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/text', 'ace/tokenizer', 'ace/mode/css_highlight_rules', 'ace/mode/matching_brace_outdent', 'ace/worker/worker_client', 'ace/mode/folding/cstyle'], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var TextMode = require("./text").Mode; +var Tokenizer = require("../tokenizer").Tokenizer; +var CssHighlightRules = require("./css_highlight_rules").CssHighlightRules; +var MatchingBraceOutdent = require("./matching_brace_outdent").MatchingBraceOutdent; +var WorkerClient = require("../worker/worker_client").WorkerClient; +var CStyleFoldMode = require("./folding/cstyle").FoldMode; + +var Mode = function() { + this.$tokenizer = new Tokenizer(new CssHighlightRules().getRules(), "i"); + this.$outdent = new MatchingBraceOutdent(); + this.foldingRules = new CStyleFoldMode(); +}; +oop.inherits(Mode, TextMode); + +(function() { + + this.foldingRules = "cStyle"; + + this.getNextLineIndent = function(state, line, tab) { + var indent = this.$getIndent(line); + + // ignore braces in comments + var tokens = this.$tokenizer.getLineTokens(line, state).tokens; + if (tokens.length && tokens[tokens.length-1].type == "comment") { + return indent; + } + + var match = line.match(/^.*\{\s*$/); + if (match) { + indent += tab; + } + + return indent; + }; + + this.checkOutdent = function(state, line, input) { + return this.$outdent.checkOutdent(line, input); + }; + + this.autoOutdent = function(state, doc, row) { + this.$outdent.autoOutdent(doc, row); + }; + + this.createWorker = function(session) { + var worker = new WorkerClient(["ace"], "worker-css.js", "ace/mode/css_worker", "Worker"); + worker.attachToDocument(session.getDocument()); + + worker.on("csslint", function(e) { + var errors = []; + e.data.forEach(function(message) { + errors.push({ + row: message.line - 1, + column: message.col - 1, + text: message.message, + type: message.type, + lint: message + }); + }); + + session.setAnnotations(errors); + }); + return worker; + }; + +}).call(Mode.prototype); + +exports.Mode = Mode; + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/css_highlight_rules', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/lib/lang', 'ace/mode/text_highlight_rules'], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var lang = require("../lib/lang"); +var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; + +var CssHighlightRules = function() { + + var properties = lang.arrayToMap( + ("animation-fill-mode|alignment-adjust|alignment-baseline|animation-delay|animation-direction|animation-duration|animation-iteration-count|animation-name|animation-play-state|animation-timing-function|animation|appearance|azimuth|backface-visibility|background-attachment|background-break|background-clip|background-color|background-image|background-origin|background-position|background-repeat|background-size|background|baseline-shift|binding|bleed|bookmark-label|bookmark-level|bookmark-state|bookmark-target|border-bottom|border-bottom-color|border-bottom-left-radius|border-bottom-right-radius|border-bottom-style|border-bottom-width|border-collapse|border-color|border-image|border-image-outset|border-image-repeat|border-image-slice|border-image-source|border-image-width|border-left|border-left-color|border-left-style|border-left-width|border-radius|border-right|border-right-color|border-right-style|border-right-width|border-spacing|border-style|border-top|border-top-color|border-top-left-radius|border-top-right-radius|border-top-style|border-top-width|border-width|border|bottom|box-align|box-decoration-break|box-direction|box-flex-group|box-flex|box-lines|box-ordinal-group|box-orient|box-pack|box-shadow|box-sizing|break-after|break-before|break-inside|caption-side|clear|clip|color-profile|color|column-count|column-fill|column-gap|column-rule|column-rule-color|column-rule-style|column-rule-width|column-span|column-width|columns|content|counter-increment|counter-reset|crop|cue-after|cue-before|cue|cursor|direction|display|dominant-baseline|drop-initial-after-adjust|drop-initial-after-align|drop-initial-before-adjust|drop-initial-before-align|drop-initial-size|drop-initial-value|elevation|empty-cells|fit|fit-position|float-offset|float|font-family|font-size|font-size-adjust|font-stretch|font-style|font-variant|font-weight|font|grid-columns|grid-rows|hanging-punctuation|height|hyphenate-after|hyphenate-before|hyphenate-character|hyphenate-lines|hyphenate-resource|hyphens|icon|image-orientation|image-rendering|image-resolution|inline-box-align|left|letter-spacing|line-height|line-stacking-ruby|line-stacking-shift|line-stacking-strategy|line-stacking|list-style-image|list-style-position|list-style-type|list-style|margin-bottom|margin-left|margin-right|margin-top|margin|mark-after|mark-before|mark|marks|marquee-direction|marquee-play-count|marquee-speed|marquee-style|max-height|max-width|min-height|min-width|move-to|nav-down|nav-index|nav-left|nav-right|nav-up|opacity|orphans|outline-color|outline-offset|outline-style|outline-width|outline|overflow-style|overflow-x|overflow-y|overflow|padding-bottom|padding-left|padding-right|padding-top|padding|page-break-after|page-break-before|page-break-inside|page-policy|page|pause-after|pause-before|pause|perspective-origin|perspective|phonemes|pitch-range|pitch|play-during|position|presentation-level|punctuation-trim|quotes|rendering-intent|resize|rest-after|rest-before|rest|richness|right|rotation-point|rotation|ruby-align|ruby-overhang|ruby-position|ruby-span|size|speak-header|speak-numeral|speak-punctuation|speak|speech-rate|stress|string-set|table-layout|target-name|target-new|target-position|target|text-align-last|text-align|text-decoration|text-emphasis|text-height|text-indent|text-justify|text-outline|text-shadow|text-transform|text-wrap|top|transform-origin|transform-style|transform|transition-delay|transition-duration|transition-property|transition-timing-function|transition|unicode-bidi|vertical-align|visibility|voice-balance|voice-duration|voice-family|voice-pitch-range|voice-pitch|voice-rate|voice-stress|voice-volume|volume|white-space-collapse|white-space|widows|width|word-break|word-spacing|word-wrap|z-index").split("|") + ); + + var functions = lang.arrayToMap( + ("rgb|rgba|url|attr|counter|counters").split("|") + ); + + var constants = lang.arrayToMap( + ("absolute|after-edge|after|all-scroll|all|alphabetic|always|antialiased|armenian|auto|avoid-column|avoid-page|avoid|balance|baseline|before-edge|before|below|bidi-override|block-line-height|block|bold|bolder|border-box|both|bottom|box|break-all|break-word|capitalize|caps-height|caption|center|central|char|circle|cjk-ideographic|clone|close-quote|col-resize|collapse|column|consider-shifts|contain|content-box|cover|crosshair|cubic-bezier|dashed|decimal-leading-zero|decimal|default|disabled|disc|disregard-shifts|distribute-all-lines|distribute-letter|distribute-space|distribute|dotted|double|e-resize|ease-in|ease-in-out|ease-out|ease|ellipsis|end|exclude-ruby|fill|fixed|font-size|font|georgian|glyphs|grid-height|groove|hand|hanging|hebrew|help|hidden|hiragana-iroha|hiragana|horizontal|icon|ideograph-alpha|ideograph-numeric|ideograph-parenthesis|ideograph-space|ideographic|inactive|include-ruby|inherit|initial|inline-block|inline-box|inline-line-height|inline-table|inline|inset|inside|inter-ideograph|inter-word|invert|italic|justify|katakana-iroha|katakana|keep-all|last|left|lighter|line-edge|line-through|line|linear|list-item|local|loose|lower-alpha|lower-greek|lower-latin|lower-roman|lowercase|lr-tb|ltr|mathematical|max-height|max-size|medium|menu|message-box|middle|move|n-resize|ne-resize|newspaper|no-change|no-close-quote|no-drop|no-open-quote|no-repeat|none|normal|not-allowed|nowrap|nw-resize|oblique|open-quote|outset|outside|overline|padding-box|page|pointer|pre-line|pre-wrap|pre|preserve-3d|progress|relative|repeat-x|repeat-y|repeat|replaced|reset-size|ridge|right|round|row-resize|rtl|s-resize|scroll|se-resize|separate|slice|small-caps|small-caption|solid|space|square|start|static|status-bar|step-end|step-start|steps|stretch|strict|sub|super|sw-resize|table-caption|table-cell|table-column-group|table-column|table-footer-group|table-header-group|table-row-group|table-row|table|tb-rl|text-after-edge|text-before-edge|text-bottom|text-size|text-top|text|thick|thin|top|transparent|underline|upper-alpha|upper-latin|upper-roman|uppercase|use-script|vertical-ideographic|vertical-text|visible|w-resize|wait|whitespace|z-index|zero").split("|") + ); + + var colors = lang.arrayToMap( + ("aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|" + + "purple|red|silver|teal|white|yellow").split("|") + ); + + var fonts = lang.arrayToMap( + ("arial|century|comic|courier|garamond|georgia|helvetica|impact|lucida|" + + "symbol|system|tahoma|times|trebuchet|utopia|verdana|webdings|sans-serif|" + + "serif|monospace").split("|") + ); + + // regexp must not have capturing parentheses. Use (?:) instead. + // regexps are ordered -> the first match is used + + var numRe = "\\-?(?:(?:[0-9]+)|(?:[0-9]*\\.[0-9]+))"; + var pseudoElements = "(\\:+)\\b(after|before|first-letter|first-line|moz-selection|selection)\\b"; + var pseudoClasses = "(:)\\b(active|checked|disabled|empty|enabled|first-child|first-of-type|focus|hover|indeterminate|invalid|last-child|last-of-type|link|not|nth-child|nth-last-child|nth-last-of-type|nth-of-type|only-child|only-of-type|required|root|target|valid|visited)\\b"; + + var base_ruleset = [ + { + token : "comment", // multi line comment + merge : true, + regex : "\\/\\*", + next : "ruleset_comment" + }, { + token : "string", // single line + regex : '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]' + }, { + token : "string", // single line + regex : "['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']" + }, { + token : ["constant.numeric", "keyword"], + regex : "(" + numRe + ")(ch|cm|deg|em|ex|fr|gd|grad|Hz|in|kHz|mm|ms|pc|pt|px|rad|rem|s|turn|vh|vm|vw|%)" + }, { + token : ["constant.numeric"], + regex : "([0-9]+)" + }, { + token : "constant.numeric", // hex6 color + regex : "#[a-f0-9]{6}" + }, { + token : "constant.numeric", // hex3 color + regex : "#[a-f0-9]{3}" + }, { + token : ["punctuation", "entity.other.attribute-name.pseudo-element.css"], + regex : pseudoElements + }, { + token : ["punctuation", "entity.other.attribute-name.pseudo-class.css"], + regex : pseudoClasses + }, { + token : function(value) { + if (properties.hasOwnProperty(value.toLowerCase())) { + return "support.type"; + } + else if (functions.hasOwnProperty(value.toLowerCase())) { + return "support.function"; + } + else if (constants.hasOwnProperty(value.toLowerCase())) { + return "support.constant"; + } + else if (colors.hasOwnProperty(value.toLowerCase())) { + return "support.constant.color"; + } + else if (fonts.hasOwnProperty(value.toLowerCase())) { + return "support.constant.fonts"; + } + else { + return "text"; + } + }, + regex : "\\-?[a-zA-Z_][a-zA-Z0-9_\\-]*" + } + ]; + + var ruleset = lang.copyArray(base_ruleset); + ruleset.unshift({ + token : "paren.rparen", + regex : "\\}", + next: "start" + }); + + var media_ruleset = lang.copyArray( base_ruleset ); + media_ruleset.unshift({ + token : "paren.rparen", + regex : "\\}", + next: "media" + }); + + var base_comment = [{ + token : "comment", // comment spanning whole line + merge : true, + regex : ".+" + }]; + + var comment = lang.copyArray(base_comment); + comment.unshift({ + token : "comment", // closing comment + regex : ".*?\\*\\/", + next : "start" + }); + + var media_comment = lang.copyArray(base_comment); + media_comment.unshift({ + token : "comment", // closing comment + regex : ".*?\\*\\/", + next : "media" + }); + + var ruleset_comment = lang.copyArray(base_comment); + ruleset_comment.unshift({ + token : "comment", // closing comment + regex : ".*?\\*\\/", + next : "ruleset" + }); + + this.$rules = { + "start" : [{ + token : "comment", // multi line comment + merge : true, + regex : "\\/\\*", + next : "comment" + }, { + token: "paren.lparen", + regex: "\\{", + next: "ruleset" + }, { + token: "string", + regex: "@.*?{", + next: "media" + },{ + token: "keyword", + regex: "#[a-z0-9-_]+" + },{ + token: "variable", + regex: "\\.[a-z0-9-_]+" + },{ + token: "string", + regex: ":[a-z0-9-_]+" + },{ + token: "constant", + regex: "[a-z0-9-_]+" + }], + + "media" : [ { + token : "comment", // multi line comment + merge : true, + regex : "\\/\\*", + next : "media_comment" + }, { + token: "paren.lparen", + regex: "\\{", + next: "media_ruleset" + },{ + token: "string", + regex: "\\}", + next: "start" + },{ + token: "keyword", + regex: "#[a-z0-9-_]+" + },{ + token: "variable", + regex: "\\.[a-z0-9-_]+" + },{ + token: "string", + regex: ":[a-z0-9-_]+" + },{ + token: "constant", + regex: "[a-z0-9-_]+" + }], + + "comment" : comment, + + "ruleset" : ruleset, + "ruleset_comment" : ruleset_comment, + + "media_ruleset" : media_ruleset, + "media_comment" : media_comment + }; +}; + +oop.inherits(CssHighlightRules, TextHighlightRules); + +exports.CssHighlightRules = CssHighlightRules; + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/matching_brace_outdent', ['require', 'exports', 'module' , 'ace/range'], function(require, exports, module) { +"use strict"; + +var Range = require("../range").Range; + +var MatchingBraceOutdent = function() {}; + +(function() { + + this.checkOutdent = function(line, input) { + if (! /^\s+$/.test(line)) + return false; + + return /^\s*\}/.test(input); + }; + + this.autoOutdent = function(doc, row) { + var line = doc.getLine(row); + var match = line.match(/^(\s*\})/); + + if (!match) return 0; + + var column = match[1].length; + var openBracePos = doc.findMatchingBracket({row: row, column: column}); + + if (!openBracePos || openBracePos.row == row) return 0; + + var indent = this.$getIndent(doc.getLine(openBracePos.row)); + doc.replace(new Range(row, 0, row, column-1), indent); + }; + + this.$getIndent = function(line) { + var match = line.match(/^(\s+)/); + if (match) { + return match[1]; + } + + return ""; + }; + +}).call(MatchingBraceOutdent.prototype); + +exports.MatchingBraceOutdent = MatchingBraceOutdent; +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/folding/cstyle', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/range', 'ace/mode/folding/fold_mode'], function(require, exports, module) { +"use strict"; + +var oop = require("../../lib/oop"); +var Range = require("../../range").Range; +var BaseFoldMode = require("./fold_mode").FoldMode; + +var FoldMode = exports.FoldMode = function() {}; +oop.inherits(FoldMode, BaseFoldMode); + +(function() { + + this.foldingStartMarker = /(\{|\[)[^\}\]]*$|^\s*(\/\*)/; + this.foldingStopMarker = /^[^\[\{]*(\}|\])|^[\s\*]*(\*\/)/; + + this.getFoldWidgetRange = function(session, foldStyle, row) { + var line = session.getLine(row); + var match = line.match(this.foldingStartMarker); + if (match) { + var i = match.index; + + if (match[1]) + return this.openingBracketBlock(session, match[1], row, i); + + var range = session.getCommentFoldRange(row, i + match[0].length); + range.end.column -= 2; + return range; + } + + if (foldStyle !== "markbeginend") + return; + + var match = line.match(this.foldingStopMarker); + if (match) { + var i = match.index + match[0].length; + + if (match[2]) { + var range = session.getCommentFoldRange(row, i); + range.end.column -= 2; + return range; + } + + var end = {row: row, column: i}; + var start = session.$findOpeningBracket(match[1], end); + + if (!start) + return; + + start.column++; + end.column--; + + return Range.fromPoints(start, end); + } + }; + +}).call(FoldMode.prototype); + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/folding/fold_mode', ['require', 'exports', 'module' , 'ace/range'], function(require, exports, module) { +"use strict"; + +var Range = require("../../range").Range; + +var FoldMode = exports.FoldMode = function() {}; + +(function() { + + this.foldingStartMarker = null; + this.foldingStopMarker = null; + + // must return "" if there's no fold, to enable caching + this.getFoldWidget = function(session, foldStyle, row) { + var line = session.getLine(row); + if (this.foldingStartMarker.test(line)) + return "start"; + if (foldStyle == "markbeginend" + && this.foldingStopMarker + && this.foldingStopMarker.test(line)) + return "end"; + return ""; + }; + + this.getFoldWidgetRange = function(session, foldStyle, row) { + return null; + }; + + this.indentationBlock = function(session, row, column) { + var re = /^\s*/; + var startRow = row; + var endRow = row; + var line = session.getLine(row); + var startColumn = column || line.length; + var startLevel = line.match(re)[0].length; + var maxRow = session.getLength() + + while (++row < maxRow) { + line = session.getLine(row); + var level = line.match(re)[0].length; + + if (level == line.length) + continue; + + if (level <= startLevel) + break; + + endRow = row; + } + + if (endRow > startRow) { + var endColumn = session.getLine(endRow).length; + return new Range(startRow, startColumn, endRow, endColumn); + } + }; + + this.openingBracketBlock = function(session, bracket, row, column) { + var start = {row: row, column: column + 1}; + var end = session.$findClosingBracket(bracket, start); + if (!end) + return; + + var fw = session.foldWidgets[end.row]; + if (fw == null) + fw = this.getFoldWidget(session, end.row); + + if (fw == "start") { + end.row --; + end.column = session.getLine(end.row).length; + } + return Range.fromPoints(start, end); + }; + +}).call(FoldMode.prototype); + +}); diff --git a/graphterm/www/mode-html.js b/graphterm/www/mode-html.js new file mode 100644 index 0000000..9e0ac85 --- /dev/null +++ b/graphterm/www/mode-html.js @@ -0,0 +1,2475 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/html', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/text', 'ace/mode/javascript', 'ace/mode/css', 'ace/tokenizer', 'ace/mode/html_highlight_rules', 'ace/mode/behaviour/xml', 'ace/mode/folding/html'], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var TextMode = require("./text").Mode; +var JavaScriptMode = require("./javascript").Mode; +var CssMode = require("./css").Mode; +var Tokenizer = require("../tokenizer").Tokenizer; +var HtmlHighlightRules = require("./html_highlight_rules").HtmlHighlightRules; +var XmlBehaviour = require("./behaviour/xml").XmlBehaviour; +var HtmlFoldMode = require("./folding/html").FoldMode; + +var Mode = function() { + var highlighter = new HtmlHighlightRules(); + this.$tokenizer = new Tokenizer(highlighter.getRules()); + this.$behaviour = new XmlBehaviour(); + + this.$embeds = highlighter.getEmbeds(); + this.createModeDelegates({ + "js-": JavaScriptMode, + "css-": CssMode + }); + + this.foldingRules = new HtmlFoldMode(); +}; +oop.inherits(Mode, TextMode); + +(function() { + + + this.toggleCommentLines = function(state, doc, startRow, endRow) { + return 0; + }; + + this.getNextLineIndent = function(state, line, tab) { + return this.$getIndent(line); + }; + + this.checkOutdent = function(state, line, input) { + return false; + }; + +}).call(Mode.prototype); + +exports.Mode = Mode; +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/javascript', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/text', 'ace/tokenizer', 'ace/mode/javascript_highlight_rules', 'ace/mode/matching_brace_outdent', 'ace/range', 'ace/worker/worker_client', 'ace/mode/behaviour/cstyle', 'ace/mode/folding/cstyle'], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var TextMode = require("./text").Mode; +var Tokenizer = require("../tokenizer").Tokenizer; +var JavaScriptHighlightRules = require("./javascript_highlight_rules").JavaScriptHighlightRules; +var MatchingBraceOutdent = require("./matching_brace_outdent").MatchingBraceOutdent; +var Range = require("../range").Range; +var WorkerClient = require("../worker/worker_client").WorkerClient; +var CstyleBehaviour = require("./behaviour/cstyle").CstyleBehaviour; +var CStyleFoldMode = require("./folding/cstyle").FoldMode; + +var Mode = function() { + this.$tokenizer = new Tokenizer(new JavaScriptHighlightRules().getRules()); + this.$outdent = new MatchingBraceOutdent(); + this.$behaviour = new CstyleBehaviour(); + this.foldingRules = new CStyleFoldMode(); +}; +oop.inherits(Mode, TextMode); + +(function() { + + + this.toggleCommentLines = function(state, doc, startRow, endRow) { + var outdent = true; + var re = /^(\s*)\/\//; + + for (var i=startRow; i<= endRow; i++) { + if (!re.test(doc.getLine(i))) { + outdent = false; + break; + } + } + + if (outdent) { + var deleteRange = new Range(0, 0, 0, 0); + for (var i=startRow; i<= endRow; i++) + { + var line = doc.getLine(i); + var m = line.match(re); + deleteRange.start.row = i; + deleteRange.end.row = i; + deleteRange.end.column = m[0].length; + doc.replace(deleteRange, m[1]); + } + } + else { + doc.indentRows(startRow, endRow, "//"); + } + }; + + this.getNextLineIndent = function(state, line, tab) { + var indent = this.$getIndent(line); + + var tokenizedLine = this.$tokenizer.getLineTokens(line, state); + var tokens = tokenizedLine.tokens; + var endState = tokenizedLine.state; + + if (tokens.length && tokens[tokens.length-1].type == "comment") { + return indent; + } + + if (state == "start" || state == "regex_allowed") { + var match = line.match(/^.*(?:\bcase\b.*\:|[\{\(\[])\s*$/); + if (match) { + indent += tab; + } + } else if (state == "doc-start") { + if (endState == "start" || state == "regex_allowed") { + return ""; + } + var match = line.match(/^\s*(\/?)\*/); + if (match) { + if (match[1]) { + indent += " "; + } + indent += "* "; + } + } + + return indent; + }; + + this.checkOutdent = function(state, line, input) { + return this.$outdent.checkOutdent(line, input); + }; + + this.autoOutdent = function(state, doc, row) { + this.$outdent.autoOutdent(doc, row); + }; + + this.createWorker = function(session) { + var worker = new WorkerClient(["ace"], "worker-javascript.js", "ace/mode/javascript_worker", "JavaScriptWorker"); + worker.attachToDocument(session.getDocument()); + + worker.on("jslint", function(results) { + var errors = []; + for (var i=0; i + * Mihai Sucan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/javascript_highlight_rules', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/lib/lang', 'ace/unicode', 'ace/mode/doc_comment_highlight_rules', 'ace/mode/text_highlight_rules'], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var lang = require("../lib/lang"); +var unicode = require("../unicode"); +var DocCommentHighlightRules = require("./doc_comment_highlight_rules").DocCommentHighlightRules; +var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; + +var JavaScriptHighlightRules = function() { + + // see: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects + var globals = lang.arrayToMap( + // Constructors + ("Array|Boolean|Date|Function|Iterator|Number|Object|RegExp|String|Proxy|" + + // E4X + "Namespace|QName|XML|XMLList|" + + "ArrayBuffer|Float32Array|Float64Array|Int16Array|Int32Array|Int8Array|" + + "Uint16Array|Uint32Array|Uint8Array|Uint8ClampedArray|" + + // Errors + "Error|EvalError|InternalError|RangeError|ReferenceError|StopIteration|" + + "SyntaxError|TypeError|URIError|" + + // Non-constructor functions + "decodeURI|decodeURIComponent|encodeURI|encodeURIComponent|eval|isFinite|" + + "isNaN|parseFloat|parseInt|" + + // Other + "JSON|Math|" + + // Pseudo + "this|arguments|prototype|window|document" + ).split("|") + ); + + var keywords = lang.arrayToMap( + ("break|case|catch|continue|default|delete|do|else|finally|for|function|" + + "if|in|instanceof|new|return|switch|throw|try|typeof|let|var|while|with|" + + "const|yield|import|get|set").split("|") + ); + + // keywords which can be followed by regular expressions + var kwBeforeRe = "case|do|else|finally|in|instanceof|return|throw|try|typeof|yield"; + + var deprecated = lang.arrayToMap( + ("__parent__|__count__|escape|unescape|with|__proto__").split("|") + ); + + var definitions = lang.arrayToMap(("const|let|var|function").split("|")); + + var buildinConstants = lang.arrayToMap( + ("null|Infinity|NaN|undefined").split("|") + ); + + var futureReserved = lang.arrayToMap( + ("class|enum|extends|super|export|implements|private|" + + "public|interface|package|protected|static").split("|") + ); + + // TODO: Unicode escape sequences + var identifierRe = "[" + unicode.packages.L + "\\$_][" + + unicode.packages.L + + unicode.packages.Mn + unicode.packages.Mc + + unicode.packages.Nd + + unicode.packages.Pc + "\\$_]*\\b"; + + var escapedRe = "\\\\(?:x[0-9a-fA-F]{2}|" + // hex + "u[0-9a-fA-F]{4}|" + // unicode + "[0-2][0-7]{0,2}|" + // oct + "3[0-6][0-7]?|" + // oct + "37[0-7]?|" + // oct + "[4-7][0-7]?|" + //oct + ".)"; + + // regexp must not have capturing parentheses. Use (?:) instead. + // regexps are ordered -> the first match is used + + this.$rules = { + "start" : [ + { + token : "comment", + regex : /\/\/.*$/ + }, + DocCommentHighlightRules.getStartRule("doc-start"), + { + token : "comment", // multi line comment + merge : true, + regex : /\/\*/, + next : "comment" + }, { + token : "string", + regex : "'(?=.)", + next : "qstring" + }, { + token : "string", + regex : '"(?=.)', + next : "qqstring" + }, { + token : "constant.numeric", // hex + regex : /0[xX][0-9a-fA-F]+\b/ + }, { + token : "constant.numeric", // float + regex : /[+-]?\d+(?:(?:\.\d*)?(?:[eE][+-]?\d+)?)?\b/ + }, { // match stuff like: Sound.prototype.play = function() { } + token : [ + "storage.type", + "punctuation.operator", + "support.function", + "punctuation.operator", + "entity.name.function", + "text", + "keyword.operator", + "text", + "storage.type", + "text", + "paren.lparen" + ], + regex : "(" + identifierRe + ")(\\.)(prototype)(\\.)(" + identifierRe +")(\\s*)(=)(\\s*)(function)(\\s*)(\\()", + next: "function_arguments" + }, { // match stuff like: Sound.prototype.play = myfunc + token : [ + "storage.type", + "punctuation.operator", + "support.function", + "punctuation.operator", + "entity.name.function", + "text", + "keyword.operator", + "text" + ], + regex : "(" + identifierRe + ")(\\.)(prototype)(\\.)(" + identifierRe +")(\\s*)(=)(\\s*)", + next: "function_arguments" + }, { // match stuff like: Sound.play = function() { } + token : [ + "storage.type", + "punctuation.operator", + "entity.name.function", + "text", + "keyword.operator", + "text", + "storage.type", + "text", + "paren.lparen" + ], + regex : "(" + identifierRe + ")(\\.)(" + identifierRe +")(\\s*)(=)(\\s*)(function)(\\s*)(\\()", + next: "function_arguments" + }, { // match stuff like: play = function() { } + token : [ + "entity.name.function", + "text", + "keyword.operator", + "text", + "storage.type", + "text", + "paren.lparen" + ], + regex : "(" + identifierRe +")(\\s*)(=)(\\s*)(function)(\\s*)(\\()", + next: "function_arguments" + }, { // match regular function like: function myFunc(arg) { } + token : [ + "storage.type", + "text", + "entity.name.function", + "text", + "paren.lparen" + ], + regex : "(function)(\\s+)(" + identifierRe + ")(\\s*)(\\()", + next: "function_arguments" + }, { // match stuff like: foobar: function() { } + token : [ + "entity.name.function", + "text", + "punctuation.operator", + "text", + "storage.type", + "text", + "paren.lparen" + ], + regex : "(" + identifierRe + ")(\\s*)(:)(\\s*)(function)(\\s*)(\\()", + next: "function_arguments" + }, { // Attempt to match : function() { } (this is for issues with 'foo': function() { }) + token : [ + "text", + "text", + "storage.type", + "text", + "paren.lparen" + ], + regex : "(:)(\\s*)(function)(\\s*)(\\()", + next: "function_arguments" + }, { + token : "constant.language.boolean", + regex : /(?:true|false)\b/ + }, { + token : "keyword", + regex : "(?:" + kwBeforeRe + ")\\b", + next : "regex_allowed" + }, { + token : ["punctuation.operator", "support.function"], + regex : /(\.)(s(?:h(?:ift|ow(?:Mod(?:elessDialog|alDialog)|Help))|croll(?:X|By(?:Pages|Lines)?|Y|To)?|t(?:opzzzz|rike)|i(?:n|zeToContent|debar|gnText)|ort|u(?:p|b(?:str(?:ing)?)?)|pli(?:ce|t)|e(?:nd|t(?:Re(?:sizable|questHeader)|M(?:i(?:nutes|lliseconds)|onth)|Seconds|Ho(?:tKeys|urs)|Year|Cursor|Time(?:out)?|Interval|ZOptions|Date|UTC(?:M(?:i(?:nutes|lliseconds)|onth)|Seconds|Hours|Date|FullYear)|FullYear|Active)|arch)|qrt|lice|avePreferences|mall)|h(?:ome|andleEvent)|navigate|c(?:har(?:CodeAt|At)|o(?:s|n(?:cat|textual|firm)|mpile)|eil|lear(?:Timeout|Interval)?|a(?:ptureEvents|ll)|reate(?:StyleSheet|Popup|EventObject))|t(?:o(?:GMTString|S(?:tring|ource)|U(?:TCString|pperCase)|Lo(?:caleString|werCase))|est|a(?:n|int(?:Enabled)?))|i(?:s(?:NaN|Finite)|ndexOf|talics)|d(?:isableExternalCapture|ump|etachEvent)|u(?:n(?:shift|taint|escape|watch)|pdateCommands)|j(?:oin|avaEnabled)|p(?:o(?:p|w)|ush|lugins.refresh|a(?:ddings|rse(?:Int|Float)?)|r(?:int|ompt|eference))|e(?:scape|nableExternalCapture|val|lementFromPoint|x(?:p|ec(?:Script|Command)?))|valueOf|UTC|queryCommand(?:State|Indeterm|Enabled|Value)|f(?:i(?:nd|le(?:ModifiedDate|Size|CreatedDate|UpdatedDate)|xed)|o(?:nt(?:size|color)|rward)|loor|romCharCode)|watch|l(?:ink|o(?:ad|g)|astIndexOf)|a(?:sin|nchor|cos|t(?:tachEvent|ob|an(?:2)?)|pply|lert|b(?:s|ort))|r(?:ou(?:nd|teEvents)|e(?:size(?:By|To)|calc|turnValue|place|verse|l(?:oad|ease(?:Capture|Events)))|andom)|g(?:o|et(?:ResponseHeader|M(?:i(?:nutes|lliseconds)|onth)|Se(?:conds|lection)|Hours|Year|Time(?:zoneOffset)?|Da(?:y|te)|UTC(?:M(?:i(?:nutes|lliseconds)|onth)|Seconds|Hours|Da(?:y|te)|FullYear)|FullYear|A(?:ttention|llResponseHeaders)))|m(?:in|ove(?:B(?:y|elow)|To(?:Absolute)?|Above)|ergeAttributes|a(?:tch|rgins|x))|b(?:toa|ig|o(?:ld|rderWidths)|link|ack))\b(?=\()/ + }, { + token : ["punctuation.operator", "support.function.dom"], + regex : /(\.)(s(?:ub(?:stringData|mit)|plitText|e(?:t(?:NamedItem|Attribute(?:Node)?)|lect))|has(?:ChildNodes|Feature)|namedItem|c(?:l(?:ick|o(?:se|neNode))|reate(?:C(?:omment|DATASection|aption)|T(?:Head|extNode|Foot)|DocumentFragment|ProcessingInstruction|E(?:ntityReference|lement)|Attribute))|tabIndex|i(?:nsert(?:Row|Before|Cell|Data)|tem)|open|delete(?:Row|C(?:ell|aption)|T(?:Head|Foot)|Data)|focus|write(?:ln)?|a(?:dd|ppend(?:Child|Data))|re(?:set|place(?:Child|Data)|move(?:NamedItem|Child|Attribute(?:Node)?)?)|get(?:NamedItem|Element(?:sBy(?:Name|TagName)|ById)|Attribute(?:Node)?)|blur)\b(?=\()/ + }, { + token : ["punctuation.operator", "support.constant"], + regex : /(\.)(s(?:ystemLanguage|cr(?:ipts|ollbars|een(?:X|Y|Top|Left))|t(?:yle(?:Sheets)?|atus(?:Text|bar)?)|ibling(?:Below|Above)|ource|uffixes|e(?:curity(?:Policy)?|l(?:ection|f)))|h(?:istory|ost(?:name)?|as(?:h|Focus))|y|X(?:MLDocument|SLDocument)|n(?:ext|ame(?:space(?:s|URI)|Prop))|M(?:IN_VALUE|AX_VALUE)|c(?:haracterSet|o(?:n(?:structor|trollers)|okieEnabled|lorDepth|mp(?:onents|lete))|urrent|puClass|l(?:i(?:p(?:boardData)?|entInformation)|osed|asses)|alle(?:e|r)|rypto)|t(?:o(?:olbar|p)|ext(?:Transform|Indent|Decoration|Align)|ags)|SQRT(?:1_2|2)|i(?:n(?:ner(?:Height|Width)|put)|ds|gnoreCase)|zIndex|o(?:scpu|n(?:readystatechange|Line)|uter(?:Height|Width)|p(?:sProfile|ener)|ffscreenBuffering)|NEGATIVE_INFINITY|d(?:i(?:splay|alog(?:Height|Top|Width|Left|Arguments)|rectories)|e(?:scription|fault(?:Status|Ch(?:ecked|arset)|View)))|u(?:ser(?:Profile|Language|Agent)|n(?:iqueID|defined)|pdateInterval)|_content|p(?:ixelDepth|ort|ersonalbar|kcs11|l(?:ugins|atform)|a(?:thname|dding(?:Right|Bottom|Top|Left)|rent(?:Window|Layer)?|ge(?:X(?:Offset)?|Y(?:Offset)?))|r(?:o(?:to(?:col|type)|duct(?:Sub)?|mpter)|e(?:vious|fix)))|e(?:n(?:coding|abledPlugin)|x(?:ternal|pando)|mbeds)|v(?:isibility|endor(?:Sub)?|Linkcolor)|URLUnencoded|P(?:I|OSITIVE_INFINITY)|f(?:ilename|o(?:nt(?:Size|Family|Weight)|rmName)|rame(?:s|Element)|gColor)|E|whiteSpace|l(?:i(?:stStyleType|n(?:eHeight|kColor))|o(?:ca(?:tion(?:bar)?|lName)|wsrc)|e(?:ngth|ft(?:Context)?)|a(?:st(?:M(?:odified|atch)|Index|Paren)|yer(?:s|X)|nguage))|a(?:pp(?:MinorVersion|Name|Co(?:deName|re)|Version)|vail(?:Height|Top|Width|Left)|ll|r(?:ity|guments)|Linkcolor|bove)|r(?:ight(?:Context)?|e(?:sponse(?:XML|Text)|adyState))|global|x|m(?:imeTypes|ultiline|enubar|argin(?:Right|Bottom|Top|Left))|L(?:N(?:10|2)|OG(?:10E|2E))|b(?:o(?:ttom|rder(?:Width|RightWidth|BottomWidth|Style|Color|TopWidth|LeftWidth))|ufferDepth|elow|ackground(?:Color|Image)))\b/ + }, { + token : ["storage.type", "punctuation.operator", "support.function.firebug"], + regex : /(console)(\.)(warn|info|log|error|time|timeEnd|assert)\b/ + }, { + token : function(value) { + if (globals.hasOwnProperty(value)) + return "variable.language"; + else if (deprecated.hasOwnProperty(value)) + return "invalid.deprecated"; + else if (definitions.hasOwnProperty(value)) + return "storage.type"; + else if (keywords.hasOwnProperty(value)) + return "keyword"; + else if (buildinConstants.hasOwnProperty(value)) + return "constant.language"; + else if (futureReserved.hasOwnProperty(value)) + return "invalid.illegal"; + else if (value == "debugger") + return "invalid.deprecated"; + else + return "identifier"; + }, + regex : identifierRe + }, { + token : "keyword.operator", + regex : /!|\$|%|&|\*|\-\-|\-|\+\+|\+|~|===|==|=|!=|!==|<=|>=|<<=|>>=|>>>=|<>|<|>|!|&&|\|\||\?\:|\*=|%=|\+=|\-=|&=|\^=|\b(?:in|instanceof|new|delete|typeof|void)/, + next : "regex_allowed" + }, { + token : "punctuation.operator", + regex : /\?|\:|\,|\;|\./, + next : "regex_allowed" + }, { + token : "paren.lparen", + regex : /[\[({]/, + next : "regex_allowed" + }, { + token : "paren.rparen", + regex : /[\])}]/ + }, { + token : "keyword.operator", + regex : /\/=?/, + next : "regex_allowed" + }, { + token: "comment", + regex: /^#!.*$/ + }, { + token : "text", + regex : /\s+/ + } + ], + // regular expressions are only allowed after certain tokens. This + // makes sure we don't mix up regexps with the divison operator + "regex_allowed": [ + DocCommentHighlightRules.getStartRule("doc-start"), + { + token : "comment", // multi line comment + merge : true, + regex : "\\/\\*", + next : "comment_regex_allowed" + }, { + token : "comment", + regex : "\\/\\/.*$" + }, { + token: "string.regexp", + regex: "\\/", + next: "regex", + merge: true + }, { + token : "text", + regex : "\\s+" + }, { + // immediately return to the start mode without matching + // anything + token: "empty", + regex: "", + next: "start" + } + ], + "regex": [ + { + token: "regexp.keyword.operator", + regex: "\\\\(?:u[\\da-fA-F]{4}|x[\\da-fA-F]{2}|.)" + }, { + // flag + token: "string.regexp", + regex: "/\\w*", + next: "start", + merge: true + }, { + token: "string.regexp", + regex: "[^\\\\/\\[]+", + merge: true + }, { + token: "string.regexp.charachterclass", + regex: "\\[", + next: "regex_character_class", + merge: true + }, { + token: "empty", + regex: "", + next: "start" + } + ], + "regex_character_class": [ + { + token: "regexp.keyword.operator", + regex: "\\\\(?:u[\\da-fA-F]{4}|x[\\da-fA-F]{2}|.)" + }, { + token: "string.regexp.charachterclass", + regex: "]", + next: "regex", + merge: true + }, { + token: "string.regexp.charachterclass", + regex: "[^\\\\\\]]+", + merge: true + }, { + token: "empty", + regex: "", + next: "start" + } + ], + "function_arguments": [ + { + token: "variable.parameter", + regex: identifierRe, + }, { + token: "punctuation.operator", + regex: "[, ]+", + merge: true + }, { + token: "punctuation.operator", + regex: "$", + merge: true + }, { + token: "empty", + regex: "", + next: "start" + } + ], + "comment_regex_allowed" : [ + { + token : "comment", // closing comment + regex : ".*?\\*\\/", + merge : true, + next : "regex_allowed" + }, { + token : "comment", // comment spanning whole line + merge : true, + regex : ".+" + } + ], + "comment" : [ + { + token : "comment", // closing comment + regex : ".*?\\*\\/", + merge : true, + next : "start" + }, { + token : "comment", // comment spanning whole line + merge : true, + regex : ".+" + } + ], + "qqstring" : [ + { + token : "constant.language.escape", + regex : escapedRe + }, { + token : "string", + regex : '[^"\\\\]+', + merge : true + }, { + token : "string", + regex : "\\\\$", + next : "qqstring", + merge : true + }, { + token : "string", + regex : '"|$', + next : "start", + merge : true + } + ], + "qstring" : [ + { + token : "constant.language.escape", + regex : escapedRe + }, { + token : "string", + regex : "[^'\\\\]+", + merge : true + }, { + token : "string", + regex : "\\\\$", + next : "qstring", + merge : true + }, { + token : "string", + regex : "'|$", + next : "start", + merge : true + } + ] + }; + + this.embedRules(DocCommentHighlightRules, "doc-", + [ DocCommentHighlightRules.getEndRule("start") ]); +}; + +oop.inherits(JavaScriptHighlightRules, TextHighlightRules); + +exports.JavaScriptHighlightRules = JavaScriptHighlightRules; +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/doc_comment_highlight_rules', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/text_highlight_rules'], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; + +var DocCommentHighlightRules = function() { + + this.$rules = { + "start" : [ { + token : "comment.doc.tag", + regex : "@[\\w\\d_]+" // TODO: fix email addresses + }, { + token : "comment.doc", + merge : true, + regex : "\\s+" + }, { + token : "comment.doc", + merge : true, + regex : "TODO" + }, { + token : "comment.doc", + merge : true, + regex : "[^@\\*]+" + }, { + token : "comment.doc", + merge : true, + regex : "." + }] + }; +}; + +oop.inherits(DocCommentHighlightRules, TextHighlightRules); + +DocCommentHighlightRules.getStartRule = function(start) { + return { + token : "comment.doc", // doc comment + merge : true, + regex : "\\/\\*(?=\\*)", + next : start + }; +}; + +DocCommentHighlightRules.getEndRule = function (start) { + return { + token : "comment.doc", // closing comment + merge : true, + regex : "\\*\\/", + next : start + }; +}; + + +exports.DocCommentHighlightRules = DocCommentHighlightRules; + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/matching_brace_outdent', ['require', 'exports', 'module' , 'ace/range'], function(require, exports, module) { +"use strict"; + +var Range = require("../range").Range; + +var MatchingBraceOutdent = function() {}; + +(function() { + + this.checkOutdent = function(line, input) { + if (! /^\s+$/.test(line)) + return false; + + return /^\s*\}/.test(input); + }; + + this.autoOutdent = function(doc, row) { + var line = doc.getLine(row); + var match = line.match(/^(\s*\})/); + + if (!match) return 0; + + var column = match[1].length; + var openBracePos = doc.findMatchingBracket({row: row, column: column}); + + if (!openBracePos || openBracePos.row == row) return 0; + + var indent = this.$getIndent(doc.getLine(openBracePos.row)); + doc.replace(new Range(row, 0, row, column-1), indent); + }; + + this.$getIndent = function(line) { + var match = line.match(/^(\s+)/); + if (match) { + return match[1]; + } + + return ""; + }; + +}).call(MatchingBraceOutdent.prototype); + +exports.MatchingBraceOutdent = MatchingBraceOutdent; +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Chris Spencer + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/behaviour/cstyle', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/behaviour'], function(require, exports, module) { +"use strict"; + +var oop = require("../../lib/oop"); +var Behaviour = require('../behaviour').Behaviour; + +var CstyleBehaviour = function () { + + this.add("braces", "insertion", function (state, action, editor, session, text) { + if (text == '{') { + var selection = editor.getSelectionRange(); + var selected = session.doc.getTextRange(selection); + if (selected !== "") { + return { + text: '{' + selected + '}', + selection: false + }; + } else { + return { + text: '{}', + selection: [1, 1] + }; + } + } else if (text == '}') { + var cursor = editor.getCursorPosition(); + var line = session.doc.getLine(cursor.row); + var rightChar = line.substring(cursor.column, cursor.column + 1); + if (rightChar == '}') { + var matching = session.$findOpeningBracket('}', {column: cursor.column + 1, row: cursor.row}); + if (matching !== null) { + return { + text: '', + selection: [1, 1] + }; + } + } + } else if (text == "\n") { + var cursor = editor.getCursorPosition(); + var line = session.doc.getLine(cursor.row); + var rightChar = line.substring(cursor.column, cursor.column + 1); + if (rightChar == '}') { + var openBracePos = session.findMatchingBracket({row: cursor.row, column: cursor.column + 1}); + if (!openBracePos) + return null; + + var indent = this.getNextLineIndent(state, line.substring(0, line.length - 1), session.getTabString()); + var next_indent = this.$getIndent(session.doc.getLine(openBracePos.row)); + + return { + text: '\n' + indent + '\n' + next_indent, + selection: [1, indent.length, 1, indent.length] + }; + } + } + }); + + this.add("braces", "deletion", function (state, action, editor, session, range) { + var selected = session.doc.getTextRange(range); + if (!range.isMultiLine() && selected == '{') { + var line = session.doc.getLine(range.start.row); + var rightChar = line.substring(range.end.column, range.end.column + 1); + if (rightChar == '}') { + range.end.column++; + return range; + } + } + }); + + this.add("parens", "insertion", function (state, action, editor, session, text) { + if (text == '(') { + var selection = editor.getSelectionRange(); + var selected = session.doc.getTextRange(selection); + if (selected !== "") { + return { + text: '(' + selected + ')', + selection: false + }; + } else { + return { + text: '()', + selection: [1, 1] + }; + } + } else if (text == ')') { + var cursor = editor.getCursorPosition(); + var line = session.doc.getLine(cursor.row); + var rightChar = line.substring(cursor.column, cursor.column + 1); + if (rightChar == ')') { + var matching = session.$findOpeningBracket(')', {column: cursor.column + 1, row: cursor.row}); + if (matching !== null) { + return { + text: '', + selection: [1, 1] + }; + } + } + } + }); + + this.add("parens", "deletion", function (state, action, editor, session, range) { + var selected = session.doc.getTextRange(range); + if (!range.isMultiLine() && selected == '(') { + var line = session.doc.getLine(range.start.row); + var rightChar = line.substring(range.start.column + 1, range.start.column + 2); + if (rightChar == ')') { + range.end.column++; + return range; + } + } + }); + + this.add("string_dquotes", "insertion", function (state, action, editor, session, text) { + if (text == '"' || text == "'") { + var quote = text; + var selection = editor.getSelectionRange(); + var selected = session.doc.getTextRange(selection); + if (selected !== "") { + return { + text: quote + selected + quote, + selection: false + }; + } else { + var cursor = editor.getCursorPosition(); + var line = session.doc.getLine(cursor.row); + var leftChar = line.substring(cursor.column-1, cursor.column); + + // We're escaped. + if (leftChar == '\\') { + return null; + } + + // Find what token we're inside. + var tokens = session.getTokens(selection.start.row, selection.start.row)[0].tokens; + var col = 0, token; + var quotepos = -1; // Track whether we're inside an open quote. + + for (var x = 0; x < tokens.length; x++) { + token = tokens[x]; + if (token.type == "string") { + quotepos = -1; + } else if (quotepos < 0) { + quotepos = token.value.indexOf(quote); + } + if ((token.value.length + col) > selection.start.column) { + break; + } + col += tokens[x].value.length; + } + + // Try and be smart about when we auto insert. + if (!token || (quotepos < 0 && token.type !== "comment" && (token.type !== "string" || ((selection.start.column !== token.value.length+col-1) && token.value.lastIndexOf(quote) === token.value.length-1)))) { + return { + text: quote + quote, + selection: [1,1] + }; + } else if (token && token.type === "string") { + // Ignore input and move right one if we're typing over the closing quote. + var rightChar = line.substring(cursor.column, cursor.column + 1); + if (rightChar == quote) { + return { + text: '', + selection: [1, 1] + }; + } + } + } + } + }); + + this.add("string_dquotes", "deletion", function (state, action, editor, session, range) { + var selected = session.doc.getTextRange(range); + if (!range.isMultiLine() && (selected == '"' || selected == "'")) { + var line = session.doc.getLine(range.start.row); + var rightChar = line.substring(range.start.column + 1, range.start.column + 2); + if (rightChar == '"') { + range.end.column++; + return range; + } + } + }); + +}; + +oop.inherits(CstyleBehaviour, Behaviour); + +exports.CstyleBehaviour = CstyleBehaviour; +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/folding/cstyle', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/range', 'ace/mode/folding/fold_mode'], function(require, exports, module) { +"use strict"; + +var oop = require("../../lib/oop"); +var Range = require("../../range").Range; +var BaseFoldMode = require("./fold_mode").FoldMode; + +var FoldMode = exports.FoldMode = function() {}; +oop.inherits(FoldMode, BaseFoldMode); + +(function() { + + this.foldingStartMarker = /(\{|\[)[^\}\]]*$|^\s*(\/\*)/; + this.foldingStopMarker = /^[^\[\{]*(\}|\])|^[\s\*]*(\*\/)/; + + this.getFoldWidgetRange = function(session, foldStyle, row) { + var line = session.getLine(row); + var match = line.match(this.foldingStartMarker); + if (match) { + var i = match.index; + + if (match[1]) + return this.openingBracketBlock(session, match[1], row, i); + + var range = session.getCommentFoldRange(row, i + match[0].length); + range.end.column -= 2; + return range; + } + + if (foldStyle !== "markbeginend") + return; + + var match = line.match(this.foldingStopMarker); + if (match) { + var i = match.index + match[0].length; + + if (match[2]) { + var range = session.getCommentFoldRange(row, i); + range.end.column -= 2; + return range; + } + + var end = {row: row, column: i}; + var start = session.$findOpeningBracket(match[1], end); + + if (!start) + return; + + start.column++; + end.column--; + + return Range.fromPoints(start, end); + } + }; + +}).call(FoldMode.prototype); + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/folding/fold_mode', ['require', 'exports', 'module' , 'ace/range'], function(require, exports, module) { +"use strict"; + +var Range = require("../../range").Range; + +var FoldMode = exports.FoldMode = function() {}; + +(function() { + + this.foldingStartMarker = null; + this.foldingStopMarker = null; + + // must return "" if there's no fold, to enable caching + this.getFoldWidget = function(session, foldStyle, row) { + var line = session.getLine(row); + if (this.foldingStartMarker.test(line)) + return "start"; + if (foldStyle == "markbeginend" + && this.foldingStopMarker + && this.foldingStopMarker.test(line)) + return "end"; + return ""; + }; + + this.getFoldWidgetRange = function(session, foldStyle, row) { + return null; + }; + + this.indentationBlock = function(session, row, column) { + var re = /^\s*/; + var startRow = row; + var endRow = row; + var line = session.getLine(row); + var startColumn = column || line.length; + var startLevel = line.match(re)[0].length; + var maxRow = session.getLength() + + while (++row < maxRow) { + line = session.getLine(row); + var level = line.match(re)[0].length; + + if (level == line.length) + continue; + + if (level <= startLevel) + break; + + endRow = row; + } + + if (endRow > startRow) { + var endColumn = session.getLine(endRow).length; + return new Range(startRow, startColumn, endRow, endColumn); + } + }; + + this.openingBracketBlock = function(session, bracket, row, column) { + var start = {row: row, column: column + 1}; + var end = session.$findClosingBracket(bracket, start); + if (!end) + return; + + var fw = session.foldWidgets[end.row]; + if (fw == null) + fw = this.getFoldWidget(session, end.row); + + if (fw == "start") { + end.row --; + end.column = session.getLine(end.row).length; + } + return Range.fromPoints(start, end); + }; + +}).call(FoldMode.prototype); + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/css', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/text', 'ace/tokenizer', 'ace/mode/css_highlight_rules', 'ace/mode/matching_brace_outdent', 'ace/worker/worker_client', 'ace/mode/folding/cstyle'], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var TextMode = require("./text").Mode; +var Tokenizer = require("../tokenizer").Tokenizer; +var CssHighlightRules = require("./css_highlight_rules").CssHighlightRules; +var MatchingBraceOutdent = require("./matching_brace_outdent").MatchingBraceOutdent; +var WorkerClient = require("../worker/worker_client").WorkerClient; +var CStyleFoldMode = require("./folding/cstyle").FoldMode; + +var Mode = function() { + this.$tokenizer = new Tokenizer(new CssHighlightRules().getRules(), "i"); + this.$outdent = new MatchingBraceOutdent(); + this.foldingRules = new CStyleFoldMode(); +}; +oop.inherits(Mode, TextMode); + +(function() { + + this.foldingRules = "cStyle"; + + this.getNextLineIndent = function(state, line, tab) { + var indent = this.$getIndent(line); + + // ignore braces in comments + var tokens = this.$tokenizer.getLineTokens(line, state).tokens; + if (tokens.length && tokens[tokens.length-1].type == "comment") { + return indent; + } + + var match = line.match(/^.*\{\s*$/); + if (match) { + indent += tab; + } + + return indent; + }; + + this.checkOutdent = function(state, line, input) { + return this.$outdent.checkOutdent(line, input); + }; + + this.autoOutdent = function(state, doc, row) { + this.$outdent.autoOutdent(doc, row); + }; + + this.createWorker = function(session) { + var worker = new WorkerClient(["ace"], "worker-css.js", "ace/mode/css_worker", "Worker"); + worker.attachToDocument(session.getDocument()); + + worker.on("csslint", function(e) { + var errors = []; + e.data.forEach(function(message) { + errors.push({ + row: message.line - 1, + column: message.col - 1, + text: message.message, + type: message.type, + lint: message + }); + }); + + session.setAnnotations(errors); + }); + return worker; + }; + +}).call(Mode.prototype); + +exports.Mode = Mode; + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/css_highlight_rules', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/lib/lang', 'ace/mode/text_highlight_rules'], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var lang = require("../lib/lang"); +var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; + +var CssHighlightRules = function() { + + var properties = lang.arrayToMap( + ("animation-fill-mode|alignment-adjust|alignment-baseline|animation-delay|animation-direction|animation-duration|animation-iteration-count|animation-name|animation-play-state|animation-timing-function|animation|appearance|azimuth|backface-visibility|background-attachment|background-break|background-clip|background-color|background-image|background-origin|background-position|background-repeat|background-size|background|baseline-shift|binding|bleed|bookmark-label|bookmark-level|bookmark-state|bookmark-target|border-bottom|border-bottom-color|border-bottom-left-radius|border-bottom-right-radius|border-bottom-style|border-bottom-width|border-collapse|border-color|border-image|border-image-outset|border-image-repeat|border-image-slice|border-image-source|border-image-width|border-left|border-left-color|border-left-style|border-left-width|border-radius|border-right|border-right-color|border-right-style|border-right-width|border-spacing|border-style|border-top|border-top-color|border-top-left-radius|border-top-right-radius|border-top-style|border-top-width|border-width|border|bottom|box-align|box-decoration-break|box-direction|box-flex-group|box-flex|box-lines|box-ordinal-group|box-orient|box-pack|box-shadow|box-sizing|break-after|break-before|break-inside|caption-side|clear|clip|color-profile|color|column-count|column-fill|column-gap|column-rule|column-rule-color|column-rule-style|column-rule-width|column-span|column-width|columns|content|counter-increment|counter-reset|crop|cue-after|cue-before|cue|cursor|direction|display|dominant-baseline|drop-initial-after-adjust|drop-initial-after-align|drop-initial-before-adjust|drop-initial-before-align|drop-initial-size|drop-initial-value|elevation|empty-cells|fit|fit-position|float-offset|float|font-family|font-size|font-size-adjust|font-stretch|font-style|font-variant|font-weight|font|grid-columns|grid-rows|hanging-punctuation|height|hyphenate-after|hyphenate-before|hyphenate-character|hyphenate-lines|hyphenate-resource|hyphens|icon|image-orientation|image-rendering|image-resolution|inline-box-align|left|letter-spacing|line-height|line-stacking-ruby|line-stacking-shift|line-stacking-strategy|line-stacking|list-style-image|list-style-position|list-style-type|list-style|margin-bottom|margin-left|margin-right|margin-top|margin|mark-after|mark-before|mark|marks|marquee-direction|marquee-play-count|marquee-speed|marquee-style|max-height|max-width|min-height|min-width|move-to|nav-down|nav-index|nav-left|nav-right|nav-up|opacity|orphans|outline-color|outline-offset|outline-style|outline-width|outline|overflow-style|overflow-x|overflow-y|overflow|padding-bottom|padding-left|padding-right|padding-top|padding|page-break-after|page-break-before|page-break-inside|page-policy|page|pause-after|pause-before|pause|perspective-origin|perspective|phonemes|pitch-range|pitch|play-during|position|presentation-level|punctuation-trim|quotes|rendering-intent|resize|rest-after|rest-before|rest|richness|right|rotation-point|rotation|ruby-align|ruby-overhang|ruby-position|ruby-span|size|speak-header|speak-numeral|speak-punctuation|speak|speech-rate|stress|string-set|table-layout|target-name|target-new|target-position|target|text-align-last|text-align|text-decoration|text-emphasis|text-height|text-indent|text-justify|text-outline|text-shadow|text-transform|text-wrap|top|transform-origin|transform-style|transform|transition-delay|transition-duration|transition-property|transition-timing-function|transition|unicode-bidi|vertical-align|visibility|voice-balance|voice-duration|voice-family|voice-pitch-range|voice-pitch|voice-rate|voice-stress|voice-volume|volume|white-space-collapse|white-space|widows|width|word-break|word-spacing|word-wrap|z-index").split("|") + ); + + var functions = lang.arrayToMap( + ("rgb|rgba|url|attr|counter|counters").split("|") + ); + + var constants = lang.arrayToMap( + ("absolute|after-edge|after|all-scroll|all|alphabetic|always|antialiased|armenian|auto|avoid-column|avoid-page|avoid|balance|baseline|before-edge|before|below|bidi-override|block-line-height|block|bold|bolder|border-box|both|bottom|box|break-all|break-word|capitalize|caps-height|caption|center|central|char|circle|cjk-ideographic|clone|close-quote|col-resize|collapse|column|consider-shifts|contain|content-box|cover|crosshair|cubic-bezier|dashed|decimal-leading-zero|decimal|default|disabled|disc|disregard-shifts|distribute-all-lines|distribute-letter|distribute-space|distribute|dotted|double|e-resize|ease-in|ease-in-out|ease-out|ease|ellipsis|end|exclude-ruby|fill|fixed|font-size|font|georgian|glyphs|grid-height|groove|hand|hanging|hebrew|help|hidden|hiragana-iroha|hiragana|horizontal|icon|ideograph-alpha|ideograph-numeric|ideograph-parenthesis|ideograph-space|ideographic|inactive|include-ruby|inherit|initial|inline-block|inline-box|inline-line-height|inline-table|inline|inset|inside|inter-ideograph|inter-word|invert|italic|justify|katakana-iroha|katakana|keep-all|last|left|lighter|line-edge|line-through|line|linear|list-item|local|loose|lower-alpha|lower-greek|lower-latin|lower-roman|lowercase|lr-tb|ltr|mathematical|max-height|max-size|medium|menu|message-box|middle|move|n-resize|ne-resize|newspaper|no-change|no-close-quote|no-drop|no-open-quote|no-repeat|none|normal|not-allowed|nowrap|nw-resize|oblique|open-quote|outset|outside|overline|padding-box|page|pointer|pre-line|pre-wrap|pre|preserve-3d|progress|relative|repeat-x|repeat-y|repeat|replaced|reset-size|ridge|right|round|row-resize|rtl|s-resize|scroll|se-resize|separate|slice|small-caps|small-caption|solid|space|square|start|static|status-bar|step-end|step-start|steps|stretch|strict|sub|super|sw-resize|table-caption|table-cell|table-column-group|table-column|table-footer-group|table-header-group|table-row-group|table-row|table|tb-rl|text-after-edge|text-before-edge|text-bottom|text-size|text-top|text|thick|thin|top|transparent|underline|upper-alpha|upper-latin|upper-roman|uppercase|use-script|vertical-ideographic|vertical-text|visible|w-resize|wait|whitespace|z-index|zero").split("|") + ); + + var colors = lang.arrayToMap( + ("aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|" + + "purple|red|silver|teal|white|yellow").split("|") + ); + + var fonts = lang.arrayToMap( + ("arial|century|comic|courier|garamond|georgia|helvetica|impact|lucida|" + + "symbol|system|tahoma|times|trebuchet|utopia|verdana|webdings|sans-serif|" + + "serif|monospace").split("|") + ); + + // regexp must not have capturing parentheses. Use (?:) instead. + // regexps are ordered -> the first match is used + + var numRe = "\\-?(?:(?:[0-9]+)|(?:[0-9]*\\.[0-9]+))"; + var pseudoElements = "(\\:+)\\b(after|before|first-letter|first-line|moz-selection|selection)\\b"; + var pseudoClasses = "(:)\\b(active|checked|disabled|empty|enabled|first-child|first-of-type|focus|hover|indeterminate|invalid|last-child|last-of-type|link|not|nth-child|nth-last-child|nth-last-of-type|nth-of-type|only-child|only-of-type|required|root|target|valid|visited)\\b"; + + var base_ruleset = [ + { + token : "comment", // multi line comment + merge : true, + regex : "\\/\\*", + next : "ruleset_comment" + }, { + token : "string", // single line + regex : '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]' + }, { + token : "string", // single line + regex : "['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']" + }, { + token : ["constant.numeric", "keyword"], + regex : "(" + numRe + ")(ch|cm|deg|em|ex|fr|gd|grad|Hz|in|kHz|mm|ms|pc|pt|px|rad|rem|s|turn|vh|vm|vw|%)" + }, { + token : ["constant.numeric"], + regex : "([0-9]+)" + }, { + token : "constant.numeric", // hex6 color + regex : "#[a-f0-9]{6}" + }, { + token : "constant.numeric", // hex3 color + regex : "#[a-f0-9]{3}" + }, { + token : ["punctuation", "entity.other.attribute-name.pseudo-element.css"], + regex : pseudoElements + }, { + token : ["punctuation", "entity.other.attribute-name.pseudo-class.css"], + regex : pseudoClasses + }, { + token : function(value) { + if (properties.hasOwnProperty(value.toLowerCase())) { + return "support.type"; + } + else if (functions.hasOwnProperty(value.toLowerCase())) { + return "support.function"; + } + else if (constants.hasOwnProperty(value.toLowerCase())) { + return "support.constant"; + } + else if (colors.hasOwnProperty(value.toLowerCase())) { + return "support.constant.color"; + } + else if (fonts.hasOwnProperty(value.toLowerCase())) { + return "support.constant.fonts"; + } + else { + return "text"; + } + }, + regex : "\\-?[a-zA-Z_][a-zA-Z0-9_\\-]*" + } + ]; + + var ruleset = lang.copyArray(base_ruleset); + ruleset.unshift({ + token : "paren.rparen", + regex : "\\}", + next: "start" + }); + + var media_ruleset = lang.copyArray( base_ruleset ); + media_ruleset.unshift({ + token : "paren.rparen", + regex : "\\}", + next: "media" + }); + + var base_comment = [{ + token : "comment", // comment spanning whole line + merge : true, + regex : ".+" + }]; + + var comment = lang.copyArray(base_comment); + comment.unshift({ + token : "comment", // closing comment + regex : ".*?\\*\\/", + next : "start" + }); + + var media_comment = lang.copyArray(base_comment); + media_comment.unshift({ + token : "comment", // closing comment + regex : ".*?\\*\\/", + next : "media" + }); + + var ruleset_comment = lang.copyArray(base_comment); + ruleset_comment.unshift({ + token : "comment", // closing comment + regex : ".*?\\*\\/", + next : "ruleset" + }); + + this.$rules = { + "start" : [{ + token : "comment", // multi line comment + merge : true, + regex : "\\/\\*", + next : "comment" + }, { + token: "paren.lparen", + regex: "\\{", + next: "ruleset" + }, { + token: "string", + regex: "@.*?{", + next: "media" + },{ + token: "keyword", + regex: "#[a-z0-9-_]+" + },{ + token: "variable", + regex: "\\.[a-z0-9-_]+" + },{ + token: "string", + regex: ":[a-z0-9-_]+" + },{ + token: "constant", + regex: "[a-z0-9-_]+" + }], + + "media" : [ { + token : "comment", // multi line comment + merge : true, + regex : "\\/\\*", + next : "media_comment" + }, { + token: "paren.lparen", + regex: "\\{", + next: "media_ruleset" + },{ + token: "string", + regex: "\\}", + next: "start" + },{ + token: "keyword", + regex: "#[a-z0-9-_]+" + },{ + token: "variable", + regex: "\\.[a-z0-9-_]+" + },{ + token: "string", + regex: ":[a-z0-9-_]+" + },{ + token: "constant", + regex: "[a-z0-9-_]+" + }], + + "comment" : comment, + + "ruleset" : ruleset, + "ruleset_comment" : ruleset_comment, + + "media_ruleset" : media_ruleset, + "media_comment" : media_comment + }; +}; + +oop.inherits(CssHighlightRules, TextHighlightRules); + +exports.CssHighlightRules = CssHighlightRules; + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/html_highlight_rules', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/css_highlight_rules', 'ace/mode/javascript_highlight_rules', 'ace/mode/xml_util', 'ace/mode/text_highlight_rules'], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var CssHighlightRules = require("./css_highlight_rules").CssHighlightRules; +var JavaScriptHighlightRules = require("./javascript_highlight_rules").JavaScriptHighlightRules; +var xmlUtil = require("./xml_util"); +var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; + +var HtmlHighlightRules = function() { + + // regexp must not have capturing parentheses + // regexps are ordered -> the first match is used + this.$rules = { + start : [{ + token : "text", + merge : true, + regex : "<\\!\\[CDATA\\[", + next : "cdata" + }, { + token : "xml_pe", + regex : "<\\?.*?\\?>" + }, { + token : "comment", + merge : true, + regex : "<\\!--", + next : "comment" + }, { + token : "xml_pe", + regex : "<\\!.*?>" + }, { + token : "meta.tag", + regex : "<(?=\s*script\\b)", + next : "script" + }, { + token : "meta.tag", + regex : "<(?=\s*style\\b)", + next : "style" + }, { + token : "meta.tag", // opening tag + regex : "<\\/?", + next : "tag" + }, { + token : "text", + regex : "\\s+" + }, { + token : "constant.character.entity", + regex : "(?:&#[0-9]+;)|(?:&#x[0-9a-fA-F]+;)|(?:&[a-zA-Z0-9_:\\.-]+;)" + }, { + token : "text", + regex : "[^<]+" + } ], + + cdata : [ { + token : "text", + regex : "\\]\\]>", + next : "start" + }, { + token : "text", + merge : true, + regex : "\\s+" + }, { + token : "text", + merge : true, + regex : ".+" + } ], + + comment : [ { + token : "comment", + regex : ".*?-->", + next : "start" + }, { + token : "comment", + merge : true, + regex : ".+" + } ] + }; + + xmlUtil.tag(this.$rules, "tag", "start"); + xmlUtil.tag(this.$rules, "style", "css-start"); + xmlUtil.tag(this.$rules, "script", "js-start"); + + this.embedRules(JavaScriptHighlightRules, "js-", [{ + token: "comment", + regex: "\\/\\/.*(?=<\\/script>)", + next: "tag" + }, { + token: "meta.tag", + regex: "<\\/(?=script)", + next: "tag" + }]); + + this.embedRules(CssHighlightRules, "css-", [{ + token: "meta.tag", + regex: "<\\/(?=style)", + next: "tag" + }]); +}; + +oop.inherits(HtmlHighlightRules, TextHighlightRules); + +exports.HtmlHighlightRules = HtmlHighlightRules; +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/xml_util', ['require', 'exports', 'module' , 'ace/lib/lang'], function(require, exports, module) { +"use strict"; + +var lang = require("../lib/lang"); + +var formTags = lang.arrayToMap( + ("button|form|input|label|select|textarea").split("|") +); + +var tableTags = lang.arrayToMap( + ("table|tbody|td|tfoot|th|tr").split("|") +); + +function string(state) { + return [{ + token : "string", + regex : '".*?"' + }, { + token : "string", // multi line string start + merge : true, + regex : '["].*', + next : state + "_qqstring" + }, { + token : "string", + regex : "'.*?'" + }, { + token : "string", // multi line string start + merge : true, + regex : "['].*", + next : state + "_qstring" + }]; +} + +function multiLineString(quote, state) { + return [{ + token : "string", + merge : true, + regex : ".*?" + quote, + next : state + }, { + token : "string", + merge : true, + regex : '.+' + }]; +} + +exports.tag = function(states, name, nextState) { + states[name] = [{ + token : "text", + regex : "\\s+" + }, { + //token : "meta.tag", + + token : function(value) { + if ( value==='a' ) { + return "meta.tag.anchor"; + } + else if ( value==='img' ) { + return "meta.tag.image"; + } + else if ( value==='script' ) { + return "meta.tag.script"; + } + else if ( value==='style' ) { + return "meta.tag.style"; + } + else if (formTags.hasOwnProperty(value.toLowerCase())) { + return "meta.tag.form"; + } + else if (tableTags.hasOwnProperty(value.toLowerCase())) { + return "meta.tag.table"; + } + else { + return "meta.tag"; + } + }, + merge : true, + regex : "[-_a-zA-Z0-9:]+", + next : name + "_embed_attribute_list" + }, { + token: "empty", + regex: "", + next : name + "_embed_attribute_list" + }]; + + states[name + "_qstring"] = multiLineString("'", name + "_embed_attribute_list"); + states[name + "_qqstring"] = multiLineString("\"", name + "_embed_attribute_list"); + + states[name + "_embed_attribute_list"] = [{ + token : "meta.tag", + merge : true, + regex : "\/?>", + next : nextState + }, { + token : "keyword.operator", + regex : "=" + }, { + token : "entity.other.attribute-name", + regex : "[-_a-zA-Z0-9:]+" + }, { + token : "constant.numeric", // float + regex : "[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b" + }, { + token : "text", + regex : "\\s+" + }].concat(string(name)); +}; + +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Chris Spencer + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/behaviour/xml', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/behaviour', 'ace/mode/behaviour/cstyle'], function(require, exports, module) { +"use strict"; + +var oop = require("../../lib/oop"); +var Behaviour = require("../behaviour").Behaviour; +var CstyleBehaviour = require("./cstyle").CstyleBehaviour; + +var XmlBehaviour = function () { + + this.inherit(CstyleBehaviour, ["string_dquotes"]); // Get string behaviour + + this.add("brackets", "insertion", function (state, action, editor, session, text) { + if (text == '<') { + var selection = editor.getSelectionRange(); + var selected = session.doc.getTextRange(selection); + if (selected !== "") { + return false; + } else { + return { + text: '<>', + selection: [1, 1] + } + } + } else if (text == '>') { + var cursor = editor.getCursorPosition(); + var line = session.doc.getLine(cursor.row); + var rightChar = line.substring(cursor.column, cursor.column + 1); + if (rightChar == '>') { // need some kind of matching check here + return { + text: '', + selection: [1, 1] + } + } + } else if (text == "\n") { + var cursor = editor.getCursorPosition(); + var line = session.doc.getLine(cursor.row); + var rightChars = line.substring(cursor.column, cursor.column + 2); + if (rightChars == ' + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/folding/html', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/folding/mixed', 'ace/mode/folding/xml', 'ace/mode/folding/cstyle'], function(require, exports, module) { +"use strict"; + +var oop = require("../../lib/oop"); +var MixedFoldMode = require("./mixed").FoldMode; +var XmlFoldMode = require("./xml").FoldMode; +var CStyleFoldMode = require("./cstyle").FoldMode; + +var FoldMode = exports.FoldMode = function() { + MixedFoldMode.call(this, new XmlFoldMode({ + // void elements + "area": 1, + "base": 1, + "br": 1, + "col": 1, + "command": 1, + "embed": 1, + "hr": 1, + "img": 1, + "input": 1, + "keygen": 1, + "link": 1, + "meta": 1, + "param": 1, + "source": 1, + "track": 1, + "wbr": 1, + + // optional tags + "li": 1, + "dt": 1, + "dd": 1, + "p": 1, + "rt": 1, + "rp": 1, + "optgroup": 1, + "option": 1, + "colgroup": 1, + "td": 1, + "th": 1 + }), { + "js-": new CStyleFoldMode(), + "css-": new CStyleFoldMode() + }); +}; + +oop.inherits(FoldMode, MixedFoldMode); + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/folding/mixed', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/folding/fold_mode'], function(require, exports, module) { +"use strict"; + +var oop = require("../../lib/oop"); +var BaseFoldMode = require("./fold_mode").FoldMode; + +var FoldMode = exports.FoldMode = function(defaultMode, subModes) { + this.defaultMode = defaultMode; + this.subModes = subModes; +}; +oop.inherits(FoldMode, BaseFoldMode); + +(function() { + + + this.$getMode = function(state) { + for (var key in this.subModes) { + if (state.indexOf(key) === 0) + return this.subModes[key]; + } + return null; + }; + + this.$tryMode = function(state, session, foldStyle, row) { + var mode = this.$getMode(state); + return (mode ? mode.getFoldWidget(session, foldStyle, row) : ""); + }; + + this.getFoldWidget = function(session, foldStyle, row) { + return ( + this.$tryMode(session.getState(row-1), session, foldStyle, row) || + this.$tryMode(session.getState(row), session, foldStyle, row) || + this.defaultMode.getFoldWidget(session, foldStyle, row) + ); + }; + + this.getFoldWidgetRange = function(session, foldStyle, row) { + var mode = this.$getMode(session.getState(row-1)); + + if (!mode || !mode.getFoldWidget(session, foldStyle, row)) + mode = this.$getMode(session.getState(row)); + + if (!mode || !mode.getFoldWidget(session, foldStyle, row)) + mode = this.defaultMode; + + return mode.getFoldWidgetRange(session, foldStyle, row); + }; + +}).call(FoldMode.prototype); + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/folding/xml', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/lib/lang', 'ace/range', 'ace/mode/folding/fold_mode', 'ace/token_iterator'], function(require, exports, module) { +"use strict"; + +var oop = require("../../lib/oop"); +var lang = require("../../lib/lang"); +var Range = require("../../range").Range; +var BaseFoldMode = require("./fold_mode").FoldMode; +var TokenIterator = require("../../token_iterator").TokenIterator; + +var FoldMode = exports.FoldMode = function(voidElements) { + BaseFoldMode.call(this); + this.voidElements = voidElements || {}; +}; +oop.inherits(FoldMode, BaseFoldMode); + +(function() { + + this.getFoldWidget = function(session, foldStyle, row) { + var tag = this._getFirstTagInLine(session, row); + + if (tag.closing) + return foldStyle == "markbeginend" ? "end" : ""; + + if (!tag.tagName || this.voidElements[tag.tagName.toLowerCase()]) + return ""; + + if (tag.selfClosing) + return ""; + + if (tag.value.indexOf("/" + tag.tagName) !== -1) + return ""; + + return "start"; + }; + + this._getFirstTagInLine = function(session, row) { + var tokens = session.getTokens(row, row)[0].tokens; + var value = ""; + for (var i = 0; i < tokens.length; i++) { + var token = tokens[i]; + if (token.type.indexOf("meta.tag") === 0) + value += token.value; + else + value += lang.stringRepeat(" ", token.value.length); + } + + return this._parseTag(value); + }; + + this.tagRe = /^(\s*)(?)/; + this._parseTag = function(tag) { + + var match = this.tagRe.exec(tag); + var column = this.tagRe.lastIndex || 0; + this.tagRe.lastIndex = 0; + + return { + value: tag, + match: match ? match[2] : "", + closing: match ? !!match[3] : false, + selfClosing: match ? !!match[5] || match[2] == "/>" : false, + tagName: match ? match[4] : "", + column: match[1] ? column + match[1].length : column + }; + }; + + /* + * reads a full tag and places the iterator after the tag + */ + this._readTagForward = function(iterator) { + var token = iterator.getCurrentToken(); + if (!token) + return null; + + var value = ""; + var start; + + do { + if (token.type.indexOf("meta.tag") === 0) { + if (!start) { + var start = { + row: iterator.getCurrentTokenRow(), + column: iterator.getCurrentTokenColumn() + }; + } + value += token.value; + if (value.indexOf(">") !== -1) { + var tag = this._parseTag(value); + tag.start = start; + tag.end = { + row: iterator.getCurrentTokenRow(), + column: iterator.getCurrentTokenColumn() + token.value.length + }; + iterator.stepForward(); + return tag; + } + } + } while(token = iterator.stepForward()); + + return null; + }; + + this._readTagBackward = function(iterator) { + var token = iterator.getCurrentToken(); + if (!token) + return null; + + var value = ""; + var end; + + do { + if (token.type.indexOf("meta.tag") === 0) { + if (!end) { + end = { + row: iterator.getCurrentTokenRow(), + column: iterator.getCurrentTokenColumn() + token.value.length + }; + } + value = token.value + value; + if (value.indexOf("<") !== -1) { + var tag = this._parseTag(value); + tag.end = end; + tag.start = { + row: iterator.getCurrentTokenRow(), + column: iterator.getCurrentTokenColumn() + }; + iterator.stepBackward(); + return tag; + } + } + } while(token = iterator.stepBackward()); + + return null; + }; + + this._pop = function(stack, tag) { + while (stack.length) { + + var top = stack[stack.length-1]; + if (!tag || top.tagName == tag.tagName) { + return stack.pop(); + } + else if (this.voidElements[tag.tagName]) { + return; + } + else if (this.voidElements[top.tagName]) { + stack.pop(); + continue; + } else { + return null; + } + } + }; + + this.getFoldWidgetRange = function(session, foldStyle, row) { + var firstTag = this._getFirstTagInLine(session, row); + + if (!firstTag.match) + return null; + + var isBackward = firstTag.closing || firstTag.selfClosing; + var stack = []; + var tag; + + if (!isBackward) { + var iterator = new TokenIterator(session, row, firstTag.column); + var start = { + row: row, + column: firstTag.column + firstTag.tagName.length + 2 + }; + while (tag = this._readTagForward(iterator)) { + if (tag.selfClosing) { + if (!stack.length) { + tag.start.column += tag.tagName.length + 2; + tag.end.column -= 2; + return Range.fromPoints(tag.start, tag.end); + } else + continue; + } + + if (tag.closing) { + this._pop(stack, tag); + if (stack.length == 0) + return Range.fromPoints(start, tag.start); + } + else { + stack.push(tag) + } + } + } + else { + var iterator = new TokenIterator(session, row, firstTag.column + firstTag.match.length); + var end = { + row: row, + column: firstTag.column + }; + + while (tag = this._readTagBackward(iterator)) { + if (tag.selfClosing) { + if (!stack.length) { + tag.start.column += tag.tagName.length + 2; + tag.end.column -= 2; + return Range.fromPoints(tag.start, tag.end); + } else + continue; + } + + if (!tag.closing) { + this._pop(stack, tag); + if (stack.length == 0) { + tag.start.column += tag.tagName.length + 2; + return Range.fromPoints(tag.start, end); + } + } + else { + stack.push(tag) + } + } + } + + }; + +}).call(FoldMode.prototype); + +}); diff --git a/graphterm/www/mode-javascript.js b/graphterm/www/mode-javascript.js new file mode 100644 index 0000000..0645d3b --- /dev/null +++ b/graphterm/www/mode-javascript.js @@ -0,0 +1,1226 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/javascript', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/text', 'ace/tokenizer', 'ace/mode/javascript_highlight_rules', 'ace/mode/matching_brace_outdent', 'ace/range', 'ace/worker/worker_client', 'ace/mode/behaviour/cstyle', 'ace/mode/folding/cstyle'], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var TextMode = require("./text").Mode; +var Tokenizer = require("../tokenizer").Tokenizer; +var JavaScriptHighlightRules = require("./javascript_highlight_rules").JavaScriptHighlightRules; +var MatchingBraceOutdent = require("./matching_brace_outdent").MatchingBraceOutdent; +var Range = require("../range").Range; +var WorkerClient = require("../worker/worker_client").WorkerClient; +var CstyleBehaviour = require("./behaviour/cstyle").CstyleBehaviour; +var CStyleFoldMode = require("./folding/cstyle").FoldMode; + +var Mode = function() { + this.$tokenizer = new Tokenizer(new JavaScriptHighlightRules().getRules()); + this.$outdent = new MatchingBraceOutdent(); + this.$behaviour = new CstyleBehaviour(); + this.foldingRules = new CStyleFoldMode(); +}; +oop.inherits(Mode, TextMode); + +(function() { + + + this.toggleCommentLines = function(state, doc, startRow, endRow) { + var outdent = true; + var re = /^(\s*)\/\//; + + for (var i=startRow; i<= endRow; i++) { + if (!re.test(doc.getLine(i))) { + outdent = false; + break; + } + } + + if (outdent) { + var deleteRange = new Range(0, 0, 0, 0); + for (var i=startRow; i<= endRow; i++) + { + var line = doc.getLine(i); + var m = line.match(re); + deleteRange.start.row = i; + deleteRange.end.row = i; + deleteRange.end.column = m[0].length; + doc.replace(deleteRange, m[1]); + } + } + else { + doc.indentRows(startRow, endRow, "//"); + } + }; + + this.getNextLineIndent = function(state, line, tab) { + var indent = this.$getIndent(line); + + var tokenizedLine = this.$tokenizer.getLineTokens(line, state); + var tokens = tokenizedLine.tokens; + var endState = tokenizedLine.state; + + if (tokens.length && tokens[tokens.length-1].type == "comment") { + return indent; + } + + if (state == "start" || state == "regex_allowed") { + var match = line.match(/^.*(?:\bcase\b.*\:|[\{\(\[])\s*$/); + if (match) { + indent += tab; + } + } else if (state == "doc-start") { + if (endState == "start" || state == "regex_allowed") { + return ""; + } + var match = line.match(/^\s*(\/?)\*/); + if (match) { + if (match[1]) { + indent += " "; + } + indent += "* "; + } + } + + return indent; + }; + + this.checkOutdent = function(state, line, input) { + return this.$outdent.checkOutdent(line, input); + }; + + this.autoOutdent = function(state, doc, row) { + this.$outdent.autoOutdent(doc, row); + }; + + this.createWorker = function(session) { + var worker = new WorkerClient(["ace"], "worker-javascript.js", "ace/mode/javascript_worker", "JavaScriptWorker"); + worker.attachToDocument(session.getDocument()); + + worker.on("jslint", function(results) { + var errors = []; + for (var i=0; i + * Mihai Sucan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/javascript_highlight_rules', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/lib/lang', 'ace/unicode', 'ace/mode/doc_comment_highlight_rules', 'ace/mode/text_highlight_rules'], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var lang = require("../lib/lang"); +var unicode = require("../unicode"); +var DocCommentHighlightRules = require("./doc_comment_highlight_rules").DocCommentHighlightRules; +var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; + +var JavaScriptHighlightRules = function() { + + // see: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects + var globals = lang.arrayToMap( + // Constructors + ("Array|Boolean|Date|Function|Iterator|Number|Object|RegExp|String|Proxy|" + + // E4X + "Namespace|QName|XML|XMLList|" + + "ArrayBuffer|Float32Array|Float64Array|Int16Array|Int32Array|Int8Array|" + + "Uint16Array|Uint32Array|Uint8Array|Uint8ClampedArray|" + + // Errors + "Error|EvalError|InternalError|RangeError|ReferenceError|StopIteration|" + + "SyntaxError|TypeError|URIError|" + + // Non-constructor functions + "decodeURI|decodeURIComponent|encodeURI|encodeURIComponent|eval|isFinite|" + + "isNaN|parseFloat|parseInt|" + + // Other + "JSON|Math|" + + // Pseudo + "this|arguments|prototype|window|document" + ).split("|") + ); + + var keywords = lang.arrayToMap( + ("break|case|catch|continue|default|delete|do|else|finally|for|function|" + + "if|in|instanceof|new|return|switch|throw|try|typeof|let|var|while|with|" + + "const|yield|import|get|set").split("|") + ); + + // keywords which can be followed by regular expressions + var kwBeforeRe = "case|do|else|finally|in|instanceof|return|throw|try|typeof|yield"; + + var deprecated = lang.arrayToMap( + ("__parent__|__count__|escape|unescape|with|__proto__").split("|") + ); + + var definitions = lang.arrayToMap(("const|let|var|function").split("|")); + + var buildinConstants = lang.arrayToMap( + ("null|Infinity|NaN|undefined").split("|") + ); + + var futureReserved = lang.arrayToMap( + ("class|enum|extends|super|export|implements|private|" + + "public|interface|package|protected|static").split("|") + ); + + // TODO: Unicode escape sequences + var identifierRe = "[" + unicode.packages.L + "\\$_][" + + unicode.packages.L + + unicode.packages.Mn + unicode.packages.Mc + + unicode.packages.Nd + + unicode.packages.Pc + "\\$_]*\\b"; + + var escapedRe = "\\\\(?:x[0-9a-fA-F]{2}|" + // hex + "u[0-9a-fA-F]{4}|" + // unicode + "[0-2][0-7]{0,2}|" + // oct + "3[0-6][0-7]?|" + // oct + "37[0-7]?|" + // oct + "[4-7][0-7]?|" + //oct + ".)"; + + // regexp must not have capturing parentheses. Use (?:) instead. + // regexps are ordered -> the first match is used + + this.$rules = { + "start" : [ + { + token : "comment", + regex : /\/\/.*$/ + }, + DocCommentHighlightRules.getStartRule("doc-start"), + { + token : "comment", // multi line comment + merge : true, + regex : /\/\*/, + next : "comment" + }, { + token : "string", + regex : "'(?=.)", + next : "qstring" + }, { + token : "string", + regex : '"(?=.)', + next : "qqstring" + }, { + token : "constant.numeric", // hex + regex : /0[xX][0-9a-fA-F]+\b/ + }, { + token : "constant.numeric", // float + regex : /[+-]?\d+(?:(?:\.\d*)?(?:[eE][+-]?\d+)?)?\b/ + }, { // match stuff like: Sound.prototype.play = function() { } + token : [ + "storage.type", + "punctuation.operator", + "support.function", + "punctuation.operator", + "entity.name.function", + "text", + "keyword.operator", + "text", + "storage.type", + "text", + "paren.lparen" + ], + regex : "(" + identifierRe + ")(\\.)(prototype)(\\.)(" + identifierRe +")(\\s*)(=)(\\s*)(function)(\\s*)(\\()", + next: "function_arguments" + }, { // match stuff like: Sound.prototype.play = myfunc + token : [ + "storage.type", + "punctuation.operator", + "support.function", + "punctuation.operator", + "entity.name.function", + "text", + "keyword.operator", + "text" + ], + regex : "(" + identifierRe + ")(\\.)(prototype)(\\.)(" + identifierRe +")(\\s*)(=)(\\s*)", + next: "function_arguments" + }, { // match stuff like: Sound.play = function() { } + token : [ + "storage.type", + "punctuation.operator", + "entity.name.function", + "text", + "keyword.operator", + "text", + "storage.type", + "text", + "paren.lparen" + ], + regex : "(" + identifierRe + ")(\\.)(" + identifierRe +")(\\s*)(=)(\\s*)(function)(\\s*)(\\()", + next: "function_arguments" + }, { // match stuff like: play = function() { } + token : [ + "entity.name.function", + "text", + "keyword.operator", + "text", + "storage.type", + "text", + "paren.lparen" + ], + regex : "(" + identifierRe +")(\\s*)(=)(\\s*)(function)(\\s*)(\\()", + next: "function_arguments" + }, { // match regular function like: function myFunc(arg) { } + token : [ + "storage.type", + "text", + "entity.name.function", + "text", + "paren.lparen" + ], + regex : "(function)(\\s+)(" + identifierRe + ")(\\s*)(\\()", + next: "function_arguments" + }, { // match stuff like: foobar: function() { } + token : [ + "entity.name.function", + "text", + "punctuation.operator", + "text", + "storage.type", + "text", + "paren.lparen" + ], + regex : "(" + identifierRe + ")(\\s*)(:)(\\s*)(function)(\\s*)(\\()", + next: "function_arguments" + }, { // Attempt to match : function() { } (this is for issues with 'foo': function() { }) + token : [ + "text", + "text", + "storage.type", + "text", + "paren.lparen" + ], + regex : "(:)(\\s*)(function)(\\s*)(\\()", + next: "function_arguments" + }, { + token : "constant.language.boolean", + regex : /(?:true|false)\b/ + }, { + token : "keyword", + regex : "(?:" + kwBeforeRe + ")\\b", + next : "regex_allowed" + }, { + token : ["punctuation.operator", "support.function"], + regex : /(\.)(s(?:h(?:ift|ow(?:Mod(?:elessDialog|alDialog)|Help))|croll(?:X|By(?:Pages|Lines)?|Y|To)?|t(?:opzzzz|rike)|i(?:n|zeToContent|debar|gnText)|ort|u(?:p|b(?:str(?:ing)?)?)|pli(?:ce|t)|e(?:nd|t(?:Re(?:sizable|questHeader)|M(?:i(?:nutes|lliseconds)|onth)|Seconds|Ho(?:tKeys|urs)|Year|Cursor|Time(?:out)?|Interval|ZOptions|Date|UTC(?:M(?:i(?:nutes|lliseconds)|onth)|Seconds|Hours|Date|FullYear)|FullYear|Active)|arch)|qrt|lice|avePreferences|mall)|h(?:ome|andleEvent)|navigate|c(?:har(?:CodeAt|At)|o(?:s|n(?:cat|textual|firm)|mpile)|eil|lear(?:Timeout|Interval)?|a(?:ptureEvents|ll)|reate(?:StyleSheet|Popup|EventObject))|t(?:o(?:GMTString|S(?:tring|ource)|U(?:TCString|pperCase)|Lo(?:caleString|werCase))|est|a(?:n|int(?:Enabled)?))|i(?:s(?:NaN|Finite)|ndexOf|talics)|d(?:isableExternalCapture|ump|etachEvent)|u(?:n(?:shift|taint|escape|watch)|pdateCommands)|j(?:oin|avaEnabled)|p(?:o(?:p|w)|ush|lugins.refresh|a(?:ddings|rse(?:Int|Float)?)|r(?:int|ompt|eference))|e(?:scape|nableExternalCapture|val|lementFromPoint|x(?:p|ec(?:Script|Command)?))|valueOf|UTC|queryCommand(?:State|Indeterm|Enabled|Value)|f(?:i(?:nd|le(?:ModifiedDate|Size|CreatedDate|UpdatedDate)|xed)|o(?:nt(?:size|color)|rward)|loor|romCharCode)|watch|l(?:ink|o(?:ad|g)|astIndexOf)|a(?:sin|nchor|cos|t(?:tachEvent|ob|an(?:2)?)|pply|lert|b(?:s|ort))|r(?:ou(?:nd|teEvents)|e(?:size(?:By|To)|calc|turnValue|place|verse|l(?:oad|ease(?:Capture|Events)))|andom)|g(?:o|et(?:ResponseHeader|M(?:i(?:nutes|lliseconds)|onth)|Se(?:conds|lection)|Hours|Year|Time(?:zoneOffset)?|Da(?:y|te)|UTC(?:M(?:i(?:nutes|lliseconds)|onth)|Seconds|Hours|Da(?:y|te)|FullYear)|FullYear|A(?:ttention|llResponseHeaders)))|m(?:in|ove(?:B(?:y|elow)|To(?:Absolute)?|Above)|ergeAttributes|a(?:tch|rgins|x))|b(?:toa|ig|o(?:ld|rderWidths)|link|ack))\b(?=\()/ + }, { + token : ["punctuation.operator", "support.function.dom"], + regex : /(\.)(s(?:ub(?:stringData|mit)|plitText|e(?:t(?:NamedItem|Attribute(?:Node)?)|lect))|has(?:ChildNodes|Feature)|namedItem|c(?:l(?:ick|o(?:se|neNode))|reate(?:C(?:omment|DATASection|aption)|T(?:Head|extNode|Foot)|DocumentFragment|ProcessingInstruction|E(?:ntityReference|lement)|Attribute))|tabIndex|i(?:nsert(?:Row|Before|Cell|Data)|tem)|open|delete(?:Row|C(?:ell|aption)|T(?:Head|Foot)|Data)|focus|write(?:ln)?|a(?:dd|ppend(?:Child|Data))|re(?:set|place(?:Child|Data)|move(?:NamedItem|Child|Attribute(?:Node)?)?)|get(?:NamedItem|Element(?:sBy(?:Name|TagName)|ById)|Attribute(?:Node)?)|blur)\b(?=\()/ + }, { + token : ["punctuation.operator", "support.constant"], + regex : /(\.)(s(?:ystemLanguage|cr(?:ipts|ollbars|een(?:X|Y|Top|Left))|t(?:yle(?:Sheets)?|atus(?:Text|bar)?)|ibling(?:Below|Above)|ource|uffixes|e(?:curity(?:Policy)?|l(?:ection|f)))|h(?:istory|ost(?:name)?|as(?:h|Focus))|y|X(?:MLDocument|SLDocument)|n(?:ext|ame(?:space(?:s|URI)|Prop))|M(?:IN_VALUE|AX_VALUE)|c(?:haracterSet|o(?:n(?:structor|trollers)|okieEnabled|lorDepth|mp(?:onents|lete))|urrent|puClass|l(?:i(?:p(?:boardData)?|entInformation)|osed|asses)|alle(?:e|r)|rypto)|t(?:o(?:olbar|p)|ext(?:Transform|Indent|Decoration|Align)|ags)|SQRT(?:1_2|2)|i(?:n(?:ner(?:Height|Width)|put)|ds|gnoreCase)|zIndex|o(?:scpu|n(?:readystatechange|Line)|uter(?:Height|Width)|p(?:sProfile|ener)|ffscreenBuffering)|NEGATIVE_INFINITY|d(?:i(?:splay|alog(?:Height|Top|Width|Left|Arguments)|rectories)|e(?:scription|fault(?:Status|Ch(?:ecked|arset)|View)))|u(?:ser(?:Profile|Language|Agent)|n(?:iqueID|defined)|pdateInterval)|_content|p(?:ixelDepth|ort|ersonalbar|kcs11|l(?:ugins|atform)|a(?:thname|dding(?:Right|Bottom|Top|Left)|rent(?:Window|Layer)?|ge(?:X(?:Offset)?|Y(?:Offset)?))|r(?:o(?:to(?:col|type)|duct(?:Sub)?|mpter)|e(?:vious|fix)))|e(?:n(?:coding|abledPlugin)|x(?:ternal|pando)|mbeds)|v(?:isibility|endor(?:Sub)?|Linkcolor)|URLUnencoded|P(?:I|OSITIVE_INFINITY)|f(?:ilename|o(?:nt(?:Size|Family|Weight)|rmName)|rame(?:s|Element)|gColor)|E|whiteSpace|l(?:i(?:stStyleType|n(?:eHeight|kColor))|o(?:ca(?:tion(?:bar)?|lName)|wsrc)|e(?:ngth|ft(?:Context)?)|a(?:st(?:M(?:odified|atch)|Index|Paren)|yer(?:s|X)|nguage))|a(?:pp(?:MinorVersion|Name|Co(?:deName|re)|Version)|vail(?:Height|Top|Width|Left)|ll|r(?:ity|guments)|Linkcolor|bove)|r(?:ight(?:Context)?|e(?:sponse(?:XML|Text)|adyState))|global|x|m(?:imeTypes|ultiline|enubar|argin(?:Right|Bottom|Top|Left))|L(?:N(?:10|2)|OG(?:10E|2E))|b(?:o(?:ttom|rder(?:Width|RightWidth|BottomWidth|Style|Color|TopWidth|LeftWidth))|ufferDepth|elow|ackground(?:Color|Image)))\b/ + }, { + token : ["storage.type", "punctuation.operator", "support.function.firebug"], + regex : /(console)(\.)(warn|info|log|error|time|timeEnd|assert)\b/ + }, { + token : function(value) { + if (globals.hasOwnProperty(value)) + return "variable.language"; + else if (deprecated.hasOwnProperty(value)) + return "invalid.deprecated"; + else if (definitions.hasOwnProperty(value)) + return "storage.type"; + else if (keywords.hasOwnProperty(value)) + return "keyword"; + else if (buildinConstants.hasOwnProperty(value)) + return "constant.language"; + else if (futureReserved.hasOwnProperty(value)) + return "invalid.illegal"; + else if (value == "debugger") + return "invalid.deprecated"; + else + return "identifier"; + }, + regex : identifierRe + }, { + token : "keyword.operator", + regex : /!|\$|%|&|\*|\-\-|\-|\+\+|\+|~|===|==|=|!=|!==|<=|>=|<<=|>>=|>>>=|<>|<|>|!|&&|\|\||\?\:|\*=|%=|\+=|\-=|&=|\^=|\b(?:in|instanceof|new|delete|typeof|void)/, + next : "regex_allowed" + }, { + token : "punctuation.operator", + regex : /\?|\:|\,|\;|\./, + next : "regex_allowed" + }, { + token : "paren.lparen", + regex : /[\[({]/, + next : "regex_allowed" + }, { + token : "paren.rparen", + regex : /[\])}]/ + }, { + token : "keyword.operator", + regex : /\/=?/, + next : "regex_allowed" + }, { + token: "comment", + regex: /^#!.*$/ + }, { + token : "text", + regex : /\s+/ + } + ], + // regular expressions are only allowed after certain tokens. This + // makes sure we don't mix up regexps with the divison operator + "regex_allowed": [ + DocCommentHighlightRules.getStartRule("doc-start"), + { + token : "comment", // multi line comment + merge : true, + regex : "\\/\\*", + next : "comment_regex_allowed" + }, { + token : "comment", + regex : "\\/\\/.*$" + }, { + token: "string.regexp", + regex: "\\/", + next: "regex", + merge: true + }, { + token : "text", + regex : "\\s+" + }, { + // immediately return to the start mode without matching + // anything + token: "empty", + regex: "", + next: "start" + } + ], + "regex": [ + { + token: "regexp.keyword.operator", + regex: "\\\\(?:u[\\da-fA-F]{4}|x[\\da-fA-F]{2}|.)" + }, { + // flag + token: "string.regexp", + regex: "/\\w*", + next: "start", + merge: true + }, { + token: "string.regexp", + regex: "[^\\\\/\\[]+", + merge: true + }, { + token: "string.regexp.charachterclass", + regex: "\\[", + next: "regex_character_class", + merge: true + }, { + token: "empty", + regex: "", + next: "start" + } + ], + "regex_character_class": [ + { + token: "regexp.keyword.operator", + regex: "\\\\(?:u[\\da-fA-F]{4}|x[\\da-fA-F]{2}|.)" + }, { + token: "string.regexp.charachterclass", + regex: "]", + next: "regex", + merge: true + }, { + token: "string.regexp.charachterclass", + regex: "[^\\\\\\]]+", + merge: true + }, { + token: "empty", + regex: "", + next: "start" + } + ], + "function_arguments": [ + { + token: "variable.parameter", + regex: identifierRe, + }, { + token: "punctuation.operator", + regex: "[, ]+", + merge: true + }, { + token: "punctuation.operator", + regex: "$", + merge: true + }, { + token: "empty", + regex: "", + next: "start" + } + ], + "comment_regex_allowed" : [ + { + token : "comment", // closing comment + regex : ".*?\\*\\/", + merge : true, + next : "regex_allowed" + }, { + token : "comment", // comment spanning whole line + merge : true, + regex : ".+" + } + ], + "comment" : [ + { + token : "comment", // closing comment + regex : ".*?\\*\\/", + merge : true, + next : "start" + }, { + token : "comment", // comment spanning whole line + merge : true, + regex : ".+" + } + ], + "qqstring" : [ + { + token : "constant.language.escape", + regex : escapedRe + }, { + token : "string", + regex : '[^"\\\\]+', + merge : true + }, { + token : "string", + regex : "\\\\$", + next : "qqstring", + merge : true + }, { + token : "string", + regex : '"|$', + next : "start", + merge : true + } + ], + "qstring" : [ + { + token : "constant.language.escape", + regex : escapedRe + }, { + token : "string", + regex : "[^'\\\\]+", + merge : true + }, { + token : "string", + regex : "\\\\$", + next : "qstring", + merge : true + }, { + token : "string", + regex : "'|$", + next : "start", + merge : true + } + ] + }; + + this.embedRules(DocCommentHighlightRules, "doc-", + [ DocCommentHighlightRules.getEndRule("start") ]); +}; + +oop.inherits(JavaScriptHighlightRules, TextHighlightRules); + +exports.JavaScriptHighlightRules = JavaScriptHighlightRules; +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/doc_comment_highlight_rules', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/text_highlight_rules'], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; + +var DocCommentHighlightRules = function() { + + this.$rules = { + "start" : [ { + token : "comment.doc.tag", + regex : "@[\\w\\d_]+" // TODO: fix email addresses + }, { + token : "comment.doc", + merge : true, + regex : "\\s+" + }, { + token : "comment.doc", + merge : true, + regex : "TODO" + }, { + token : "comment.doc", + merge : true, + regex : "[^@\\*]+" + }, { + token : "comment.doc", + merge : true, + regex : "." + }] + }; +}; + +oop.inherits(DocCommentHighlightRules, TextHighlightRules); + +DocCommentHighlightRules.getStartRule = function(start) { + return { + token : "comment.doc", // doc comment + merge : true, + regex : "\\/\\*(?=\\*)", + next : start + }; +}; + +DocCommentHighlightRules.getEndRule = function (start) { + return { + token : "comment.doc", // closing comment + merge : true, + regex : "\\*\\/", + next : start + }; +}; + + +exports.DocCommentHighlightRules = DocCommentHighlightRules; + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/matching_brace_outdent', ['require', 'exports', 'module' , 'ace/range'], function(require, exports, module) { +"use strict"; + +var Range = require("../range").Range; + +var MatchingBraceOutdent = function() {}; + +(function() { + + this.checkOutdent = function(line, input) { + if (! /^\s+$/.test(line)) + return false; + + return /^\s*\}/.test(input); + }; + + this.autoOutdent = function(doc, row) { + var line = doc.getLine(row); + var match = line.match(/^(\s*\})/); + + if (!match) return 0; + + var column = match[1].length; + var openBracePos = doc.findMatchingBracket({row: row, column: column}); + + if (!openBracePos || openBracePos.row == row) return 0; + + var indent = this.$getIndent(doc.getLine(openBracePos.row)); + doc.replace(new Range(row, 0, row, column-1), indent); + }; + + this.$getIndent = function(line) { + var match = line.match(/^(\s+)/); + if (match) { + return match[1]; + } + + return ""; + }; + +}).call(MatchingBraceOutdent.prototype); + +exports.MatchingBraceOutdent = MatchingBraceOutdent; +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Chris Spencer + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/behaviour/cstyle', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/behaviour'], function(require, exports, module) { +"use strict"; + +var oop = require("../../lib/oop"); +var Behaviour = require('../behaviour').Behaviour; + +var CstyleBehaviour = function () { + + this.add("braces", "insertion", function (state, action, editor, session, text) { + if (text == '{') { + var selection = editor.getSelectionRange(); + var selected = session.doc.getTextRange(selection); + if (selected !== "") { + return { + text: '{' + selected + '}', + selection: false + }; + } else { + return { + text: '{}', + selection: [1, 1] + }; + } + } else if (text == '}') { + var cursor = editor.getCursorPosition(); + var line = session.doc.getLine(cursor.row); + var rightChar = line.substring(cursor.column, cursor.column + 1); + if (rightChar == '}') { + var matching = session.$findOpeningBracket('}', {column: cursor.column + 1, row: cursor.row}); + if (matching !== null) { + return { + text: '', + selection: [1, 1] + }; + } + } + } else if (text == "\n") { + var cursor = editor.getCursorPosition(); + var line = session.doc.getLine(cursor.row); + var rightChar = line.substring(cursor.column, cursor.column + 1); + if (rightChar == '}') { + var openBracePos = session.findMatchingBracket({row: cursor.row, column: cursor.column + 1}); + if (!openBracePos) + return null; + + var indent = this.getNextLineIndent(state, line.substring(0, line.length - 1), session.getTabString()); + var next_indent = this.$getIndent(session.doc.getLine(openBracePos.row)); + + return { + text: '\n' + indent + '\n' + next_indent, + selection: [1, indent.length, 1, indent.length] + }; + } + } + }); + + this.add("braces", "deletion", function (state, action, editor, session, range) { + var selected = session.doc.getTextRange(range); + if (!range.isMultiLine() && selected == '{') { + var line = session.doc.getLine(range.start.row); + var rightChar = line.substring(range.end.column, range.end.column + 1); + if (rightChar == '}') { + range.end.column++; + return range; + } + } + }); + + this.add("parens", "insertion", function (state, action, editor, session, text) { + if (text == '(') { + var selection = editor.getSelectionRange(); + var selected = session.doc.getTextRange(selection); + if (selected !== "") { + return { + text: '(' + selected + ')', + selection: false + }; + } else { + return { + text: '()', + selection: [1, 1] + }; + } + } else if (text == ')') { + var cursor = editor.getCursorPosition(); + var line = session.doc.getLine(cursor.row); + var rightChar = line.substring(cursor.column, cursor.column + 1); + if (rightChar == ')') { + var matching = session.$findOpeningBracket(')', {column: cursor.column + 1, row: cursor.row}); + if (matching !== null) { + return { + text: '', + selection: [1, 1] + }; + } + } + } + }); + + this.add("parens", "deletion", function (state, action, editor, session, range) { + var selected = session.doc.getTextRange(range); + if (!range.isMultiLine() && selected == '(') { + var line = session.doc.getLine(range.start.row); + var rightChar = line.substring(range.start.column + 1, range.start.column + 2); + if (rightChar == ')') { + range.end.column++; + return range; + } + } + }); + + this.add("string_dquotes", "insertion", function (state, action, editor, session, text) { + if (text == '"' || text == "'") { + var quote = text; + var selection = editor.getSelectionRange(); + var selected = session.doc.getTextRange(selection); + if (selected !== "") { + return { + text: quote + selected + quote, + selection: false + }; + } else { + var cursor = editor.getCursorPosition(); + var line = session.doc.getLine(cursor.row); + var leftChar = line.substring(cursor.column-1, cursor.column); + + // We're escaped. + if (leftChar == '\\') { + return null; + } + + // Find what token we're inside. + var tokens = session.getTokens(selection.start.row, selection.start.row)[0].tokens; + var col = 0, token; + var quotepos = -1; // Track whether we're inside an open quote. + + for (var x = 0; x < tokens.length; x++) { + token = tokens[x]; + if (token.type == "string") { + quotepos = -1; + } else if (quotepos < 0) { + quotepos = token.value.indexOf(quote); + } + if ((token.value.length + col) > selection.start.column) { + break; + } + col += tokens[x].value.length; + } + + // Try and be smart about when we auto insert. + if (!token || (quotepos < 0 && token.type !== "comment" && (token.type !== "string" || ((selection.start.column !== token.value.length+col-1) && token.value.lastIndexOf(quote) === token.value.length-1)))) { + return { + text: quote + quote, + selection: [1,1] + }; + } else if (token && token.type === "string") { + // Ignore input and move right one if we're typing over the closing quote. + var rightChar = line.substring(cursor.column, cursor.column + 1); + if (rightChar == quote) { + return { + text: '', + selection: [1, 1] + }; + } + } + } + } + }); + + this.add("string_dquotes", "deletion", function (state, action, editor, session, range) { + var selected = session.doc.getTextRange(range); + if (!range.isMultiLine() && (selected == '"' || selected == "'")) { + var line = session.doc.getLine(range.start.row); + var rightChar = line.substring(range.start.column + 1, range.start.column + 2); + if (rightChar == '"') { + range.end.column++; + return range; + } + } + }); + +}; + +oop.inherits(CstyleBehaviour, Behaviour); + +exports.CstyleBehaviour = CstyleBehaviour; +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/folding/cstyle', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/range', 'ace/mode/folding/fold_mode'], function(require, exports, module) { +"use strict"; + +var oop = require("../../lib/oop"); +var Range = require("../../range").Range; +var BaseFoldMode = require("./fold_mode").FoldMode; + +var FoldMode = exports.FoldMode = function() {}; +oop.inherits(FoldMode, BaseFoldMode); + +(function() { + + this.foldingStartMarker = /(\{|\[)[^\}\]]*$|^\s*(\/\*)/; + this.foldingStopMarker = /^[^\[\{]*(\}|\])|^[\s\*]*(\*\/)/; + + this.getFoldWidgetRange = function(session, foldStyle, row) { + var line = session.getLine(row); + var match = line.match(this.foldingStartMarker); + if (match) { + var i = match.index; + + if (match[1]) + return this.openingBracketBlock(session, match[1], row, i); + + var range = session.getCommentFoldRange(row, i + match[0].length); + range.end.column -= 2; + return range; + } + + if (foldStyle !== "markbeginend") + return; + + var match = line.match(this.foldingStopMarker); + if (match) { + var i = match.index + match[0].length; + + if (match[2]) { + var range = session.getCommentFoldRange(row, i); + range.end.column -= 2; + return range; + } + + var end = {row: row, column: i}; + var start = session.$findOpeningBracket(match[1], end); + + if (!start) + return; + + start.column++; + end.column--; + + return Range.fromPoints(start, end); + } + }; + +}).call(FoldMode.prototype); + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/folding/fold_mode', ['require', 'exports', 'module' , 'ace/range'], function(require, exports, module) { +"use strict"; + +var Range = require("../../range").Range; + +var FoldMode = exports.FoldMode = function() {}; + +(function() { + + this.foldingStartMarker = null; + this.foldingStopMarker = null; + + // must return "" if there's no fold, to enable caching + this.getFoldWidget = function(session, foldStyle, row) { + var line = session.getLine(row); + if (this.foldingStartMarker.test(line)) + return "start"; + if (foldStyle == "markbeginend" + && this.foldingStopMarker + && this.foldingStopMarker.test(line)) + return "end"; + return ""; + }; + + this.getFoldWidgetRange = function(session, foldStyle, row) { + return null; + }; + + this.indentationBlock = function(session, row, column) { + var re = /^\s*/; + var startRow = row; + var endRow = row; + var line = session.getLine(row); + var startColumn = column || line.length; + var startLevel = line.match(re)[0].length; + var maxRow = session.getLength() + + while (++row < maxRow) { + line = session.getLine(row); + var level = line.match(re)[0].length; + + if (level == line.length) + continue; + + if (level <= startLevel) + break; + + endRow = row; + } + + if (endRow > startRow) { + var endColumn = session.getLine(endRow).length; + return new Range(startRow, startColumn, endRow, endColumn); + } + }; + + this.openingBracketBlock = function(session, bracket, row, column) { + var start = {row: row, column: column + 1}; + var end = session.$findClosingBracket(bracket, start); + if (!end) + return; + + var fw = session.foldWidgets[end.row]; + if (fw == null) + fw = this.getFoldWidget(session, end.row); + + if (fw == "start") { + end.row --; + end.column = session.getLine(end.row).length; + } + return Range.fromPoints(start, end); + }; + +}).call(FoldMode.prototype); + +}); diff --git a/graphterm/www/mode-python.js b/graphterm/www/mode-python.js new file mode 100644 index 0000000..1564ba0 --- /dev/null +++ b/graphterm/www/mode-python.js @@ -0,0 +1,507 @@ +/* ***** BEGIN LICENSE BLOCK ***** +* Version: MPL 1.1/GPL 2.0/LGPL 2.1 +* +* The contents of this file are subject to the Mozilla Public License Version +* 1.1 (the "License"); you may not use this file except in compliance with +* the License. You may obtain a copy of the License at +* http://www.mozilla.org/MPL/ +* +* Software distributed under the License is distributed on an "AS IS" basis, +* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +* for the specific language governing rights and limitations under the +* License. +* +* The Original Code is Ajax.org Code Editor (ACE). +* +* The Initial Developer of the Original Code is +* Ajax.org B.V. +* Portions created by the Initial Developer are Copyright (C) 2010 +* the Initial Developer. All Rights Reserved. +* +* Contributor(s): +* Fabian Jakobs +* Colin Gourlay +* +* Alternatively, the contents of this file may be used under the terms of +* either the GNU General Public License Version 2 or later (the "GPL"), or +* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +* in which case the provisions of the GPL or the LGPL are applicable instead +* of those above. If you wish to allow use of your version of this file only +* under the terms of either the GPL or the LGPL, and not to allow others to +* use your version of this file under the terms of the MPL, indicate your +* decision by deleting the provisions above and replace them with the notice +* and other provisions required by the GPL or the LGPL. If you do not delete +* the provisions above, a recipient may use your version of this file under +* the terms of any one of the MPL, the GPL or the LGPL. +* +* ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/python', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/text', 'ace/tokenizer', 'ace/mode/python_highlight_rules', 'ace/mode/folding/pythonic', 'ace/range'], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var TextMode = require("./text").Mode; +var Tokenizer = require("../tokenizer").Tokenizer; +var PythonHighlightRules = require("./python_highlight_rules").PythonHighlightRules; +var PythonFoldMode = require("./folding/pythonic").FoldMode; +var Range = require("../range").Range; + +var Mode = function() { + this.$tokenizer = new Tokenizer(new PythonHighlightRules().getRules()); + this.foldingRules = new PythonFoldMode("\\:"); +}; +oop.inherits(Mode, TextMode); + +(function() { + + this.toggleCommentLines = function(state, doc, startRow, endRow) { + var outdent = true; + var re = /^(\s*)#/; + + for (var i=startRow; i<= endRow; i++) { + if (!re.test(doc.getLine(i))) { + outdent = false; + break; + } + } + + if (outdent) { + var deleteRange = new Range(0, 0, 0, 0); + for (var i=startRow; i<= endRow; i++) + { + var line = doc.getLine(i); + var m = line.match(re); + deleteRange.start.row = i; + deleteRange.end.row = i; + deleteRange.end.column = m[0].length; + doc.replace(deleteRange, m[1]); + } + } + else { + doc.indentRows(startRow, endRow, "#"); + } + }; + + this.getNextLineIndent = function(state, line, tab) { + var indent = this.$getIndent(line); + + var tokenizedLine = this.$tokenizer.getLineTokens(line, state); + var tokens = tokenizedLine.tokens; + + if (tokens.length && tokens[tokens.length-1].type == "comment") { + return indent; + } + + if (state == "start") { + var match = line.match(/^.*[\{\(\[\:]\s*$/); + if (match) { + indent += tab; + } + } + + return indent; + }; + + var outdents = { + "pass": 1, + "return": 1, + "raise": 1, + "break": 1, + "continue": 1 + }; + + this.checkOutdent = function(state, line, input) { + if (input !== "\r\n" && input !== "\r" && input !== "\n") + return false; + + var tokens = this.$tokenizer.getLineTokens(line.trim(), state).tokens; + + if (!tokens) + return false; + + // ignore trailing comments + do { + var last = tokens.pop(); + } while (last && (last.type == "comment" || (last.type == "text" && last.value.match(/^\s+$/)))); + + if (!last) + return false; + + return (last.type == "keyword" && outdents[last.value]); + }; + + this.autoOutdent = function(state, doc, row) { + // outdenting in python is slightly different because it always applies + // to the next line and only of a new line is inserted + + row += 1; + var indent = this.$getIndent(doc.getLine(row)); + var tab = doc.getTabString(); + if (indent.slice(-tab.length) == tab) + doc.remove(new Range(row, indent.length-tab.length, row, indent.length)); + }; + +}).call(Mode.prototype); + +exports.Mode = Mode; +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * Colin Gourlay + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** + * + * TODO: python delimiters + */ + +ace.define('ace/mode/python_highlight_rules', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/lib/lang', 'ace/mode/text_highlight_rules'], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var lang = require("../lib/lang"); +var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; + +var PythonHighlightRules = function() { + + var keywords = lang.arrayToMap( + ("and|as|assert|break|class|continue|def|del|elif|else|except|exec|" + + "finally|for|from|global|if|import|in|is|lambda|not|or|pass|print|" + + "raise|return|try|while|with|yield").split("|") + ); + + var builtinConstants = lang.arrayToMap( + ("True|False|None|NotImplemented|Ellipsis|__debug__").split("|") + ); + + var builtinFunctions = lang.arrayToMap( + ("abs|divmod|input|open|staticmethod|all|enumerate|int|ord|str|any|" + + "eval|isinstance|pow|sum|basestring|execfile|issubclass|print|super|" + + "binfile|iter|property|tuple|bool|filter|len|range|type|bytearray|" + + "float|list|raw_input|unichr|callable|format|locals|reduce|unicode|" + + "chr|frozenset|long|reload|vars|classmethod|getattr|map|repr|xrange|" + + "cmp|globals|max|reversed|zip|compile|hasattr|memoryview|round|" + + "__import__|complex|hash|min|set|apply|delattr|help|next|setattr|" + + "buffer|dict|hex|object|slice|coerce|dir|id|oct|sorted|intern").split("|") + ); + + var futureReserved = lang.arrayToMap( + ("").split("|") + ); + + var strPre = "(?:r|u|ur|R|U|UR|Ur|uR)?"; + + var decimalInteger = "(?:(?:[1-9]\\d*)|(?:0))"; + var octInteger = "(?:0[oO]?[0-7]+)"; + var hexInteger = "(?:0[xX][\\dA-Fa-f]+)"; + var binInteger = "(?:0[bB][01]+)"; + var integer = "(?:" + decimalInteger + "|" + octInteger + "|" + hexInteger + "|" + binInteger + ")"; + + var exponent = "(?:[eE][+-]?\\d+)"; + var fraction = "(?:\\.\\d+)"; + var intPart = "(?:\\d+)"; + var pointFloat = "(?:(?:" + intPart + "?" + fraction + ")|(?:" + intPart + "\\.))"; + var exponentFloat = "(?:(?:" + pointFloat + "|" + intPart + ")" + exponent + ")"; + var floatNumber = "(?:" + exponentFloat + "|" + pointFloat + ")"; + + this.$rules = { + "start" : [ { + token : "comment", + regex : "#.*$" + }, { + token : "string", // """ string + regex : strPre + '"{3}(?:[^\\\\]|\\\\.)*?"{3}' + }, { + token : "string", // multi line """ string start + merge : true, + regex : strPre + '"{3}.*$', + next : "qqstring" + }, { + token : "string", // " string + regex : strPre + '"(?:[^\\\\]|\\\\.)*?"' + }, { + token : "string", // ''' string + regex : strPre + "'{3}(?:[^\\\\]|\\\\.)*?'{3}" + }, { + token : "string", // multi line ''' string start + merge : true, + regex : strPre + "'{3}.*$", + next : "qstring" + }, { + token : "string", // ' string + regex : strPre + "'(?:[^\\\\]|\\\\.)*?'" + }, { + token : "constant.numeric", // imaginary + regex : "(?:" + floatNumber + "|\\d+)[jJ]\\b" + }, { + token : "constant.numeric", // float + regex : floatNumber + }, { + token : "constant.numeric", // long integer + regex : integer + "[lL]\\b" + }, { + token : "constant.numeric", // integer + regex : integer + "\\b" + }, { + token : function(value) { + if (keywords.hasOwnProperty(value)) + return "keyword"; + else if (builtinConstants.hasOwnProperty(value)) + return "constant.language"; + else if (futureReserved.hasOwnProperty(value)) + return "invalid.illegal"; + else if (builtinFunctions.hasOwnProperty(value)) + return "support.function"; + else if (value == "debugger") + return "invalid.deprecated"; + else + return "identifier"; + }, + regex : "[a-zA-Z_$][a-zA-Z0-9_$]*\\b" + }, { + token : "keyword.operator", + regex : "\\+|\\-|\\*|\\*\\*|\\/|\\/\\/|%|<<|>>|&|\\||\\^|~|<|>|<=|=>|==|!=|<>|=" + }, { + token : "lparen.paren", + regex : "[\\[\\(\\{]" + }, { + token : "paren.rparen", + regex : "[\\]\\)\\}]" + }, { + token : "text", + regex : "\\s+" + } ], + "qqstring" : [ { + token : "string", // multi line """ string end + regex : '(?:[^\\\\]|\\\\.)*?"{3}', + next : "start" + }, { + token : "string", + merge : true, + regex : '.+' + } ], + "qstring" : [ { + token : "string", // multi line ''' string end + regex : "(?:[^\\\\]|\\\\.)*?'{3}", + next : "start" + }, { + token : "string", + merge : true, + regex : '.+' + } ] + }; +}; + +oop.inherits(PythonHighlightRules, TextHighlightRules); + +exports.PythonHighlightRules = PythonHighlightRules; +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/folding/pythonic', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/folding/fold_mode'], function(require, exports, module) { +"use strict"; + +var oop = require("../../lib/oop"); +var BaseFoldMode = require("./fold_mode").FoldMode; + +var FoldMode = exports.FoldMode = function(markers) { + this.foldingStartMarker = new RegExp("(?:([\\[{])|(" + markers + "))(?:\\s*)(?:#.*)?$"); +}; +oop.inherits(FoldMode, BaseFoldMode); + +(function() { + + this.getFoldWidgetRange = function(session, foldStyle, row) { + var line = session.getLine(row); + var match = line.match(this.foldingStartMarker); + if (match) { + if (match[1]) + return this.openingBracketBlock(session, match[1], row, match.index); + if (match[2]) + return this.indentationBlock(session, row, match.index + match[2].length); + return this.indentationBlock(session, row); + } + } + +}).call(FoldMode.prototype); + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/folding/fold_mode', ['require', 'exports', 'module' , 'ace/range'], function(require, exports, module) { +"use strict"; + +var Range = require("../../range").Range; + +var FoldMode = exports.FoldMode = function() {}; + +(function() { + + this.foldingStartMarker = null; + this.foldingStopMarker = null; + + // must return "" if there's no fold, to enable caching + this.getFoldWidget = function(session, foldStyle, row) { + var line = session.getLine(row); + if (this.foldingStartMarker.test(line)) + return "start"; + if (foldStyle == "markbeginend" + && this.foldingStopMarker + && this.foldingStopMarker.test(line)) + return "end"; + return ""; + }; + + this.getFoldWidgetRange = function(session, foldStyle, row) { + return null; + }; + + this.indentationBlock = function(session, row, column) { + var re = /^\s*/; + var startRow = row; + var endRow = row; + var line = session.getLine(row); + var startColumn = column || line.length; + var startLevel = line.match(re)[0].length; + var maxRow = session.getLength() + + while (++row < maxRow) { + line = session.getLine(row); + var level = line.match(re)[0].length; + + if (level == line.length) + continue; + + if (level <= startLevel) + break; + + endRow = row; + } + + if (endRow > startRow) { + var endColumn = session.getLine(endRow).length; + return new Range(startRow, startColumn, endRow, endColumn); + } + }; + + this.openingBracketBlock = function(session, bracket, row, column) { + var start = {row: row, column: column + 1}; + var end = session.$findClosingBracket(bracket, start); + if (!end) + return; + + var fw = session.foldWidgets[end.row]; + if (fw == null) + fw = this.getFoldWidget(session, end.row); + + if (fw == "start") { + end.row --; + end.column = session.getLine(end.row).length; + } + return Range.fromPoints(start, end); + }; + +}).call(FoldMode.prototype); + +}); diff --git a/graphterm/www/mode-xml.js b/graphterm/www/mode-xml.js new file mode 100644 index 0000000..f1c96a4 --- /dev/null +++ b/graphterm/www/mode-xml.js @@ -0,0 +1,1012 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/xml', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/text', 'ace/tokenizer', 'ace/mode/xml_highlight_rules', 'ace/mode/behaviour/xml', 'ace/mode/folding/xml'], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var TextMode = require("./text").Mode; +var Tokenizer = require("../tokenizer").Tokenizer; +var XmlHighlightRules = require("./xml_highlight_rules").XmlHighlightRules; +var XmlBehaviour = require("./behaviour/xml").XmlBehaviour; +var XmlFoldMode = require("./folding/xml").FoldMode; + +var Mode = function() { + this.$tokenizer = new Tokenizer(new XmlHighlightRules().getRules()); + this.$behaviour = new XmlBehaviour(); + this.foldingRules = new XmlFoldMode(); +}; + +oop.inherits(Mode, TextMode); + +(function() { + + this.getNextLineIndent = function(state, line, tab) { + return this.$getIndent(line); + }; + +}).call(Mode.prototype); + +exports.Mode = Mode; +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/xml_highlight_rules', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/xml_util', 'ace/mode/text_highlight_rules'], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var xmlUtil = require("./xml_util"); +var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; + +var XmlHighlightRules = function() { + + // regexp must not have capturing parentheses + // regexps are ordered -> the first match is used + this.$rules = { + start : [{ + token : "text", + regex : "<\\!\\[CDATA\\[", + next : "cdata" + }, { + token : "xml_pe", + regex : "<\\?.*?\\?>" + }, { + token : "comment", + merge : true, + regex : "<\\!--", + next : "comment" + }, { + token : "xml_pe", + regex : "<\\!.*?>" + }, { + token : "meta.tag", // opening tag + regex : "<\\/?", + next : "tag" + }, { + token : "text", + regex : "\\s+" + }, { + token : "constant.character.entity", + regex : "(?:&#[0-9]+;)|(?:&#x[0-9a-fA-F]+;)|(?:&[a-zA-Z0-9_:\\.-]+;)" + }, { + token : "text", + regex : "[^<]+" + }], + + cdata : [{ + token : "text", + regex : "\\]\\]>", + next : "start" + }, { + token : "text", + regex : "\\s+" + }, { + token : "text", + regex : "(?:[^\\]]|\\](?!\\]>))+" + }], + + comment : [{ + token : "comment", + regex : ".*?-->", + next : "start" + }, { + token : "comment", + merge : true, + regex : ".+" + }] + }; + + xmlUtil.tag(this.$rules, "tag", "start"); +}; + +oop.inherits(XmlHighlightRules, TextHighlightRules); + +exports.XmlHighlightRules = XmlHighlightRules; +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/xml_util', ['require', 'exports', 'module' , 'ace/lib/lang'], function(require, exports, module) { +"use strict"; + +var lang = require("../lib/lang"); + +var formTags = lang.arrayToMap( + ("button|form|input|label|select|textarea").split("|") +); + +var tableTags = lang.arrayToMap( + ("table|tbody|td|tfoot|th|tr").split("|") +); + +function string(state) { + return [{ + token : "string", + regex : '".*?"' + }, { + token : "string", // multi line string start + merge : true, + regex : '["].*', + next : state + "_qqstring" + }, { + token : "string", + regex : "'.*?'" + }, { + token : "string", // multi line string start + merge : true, + regex : "['].*", + next : state + "_qstring" + }]; +} + +function multiLineString(quote, state) { + return [{ + token : "string", + merge : true, + regex : ".*?" + quote, + next : state + }, { + token : "string", + merge : true, + regex : '.+' + }]; +} + +exports.tag = function(states, name, nextState) { + states[name] = [{ + token : "text", + regex : "\\s+" + }, { + //token : "meta.tag", + + token : function(value) { + if ( value==='a' ) { + return "meta.tag.anchor"; + } + else if ( value==='img' ) { + return "meta.tag.image"; + } + else if ( value==='script' ) { + return "meta.tag.script"; + } + else if ( value==='style' ) { + return "meta.tag.style"; + } + else if (formTags.hasOwnProperty(value.toLowerCase())) { + return "meta.tag.form"; + } + else if (tableTags.hasOwnProperty(value.toLowerCase())) { + return "meta.tag.table"; + } + else { + return "meta.tag"; + } + }, + merge : true, + regex : "[-_a-zA-Z0-9:]+", + next : name + "_embed_attribute_list" + }, { + token: "empty", + regex: "", + next : name + "_embed_attribute_list" + }]; + + states[name + "_qstring"] = multiLineString("'", name + "_embed_attribute_list"); + states[name + "_qqstring"] = multiLineString("\"", name + "_embed_attribute_list"); + + states[name + "_embed_attribute_list"] = [{ + token : "meta.tag", + merge : true, + regex : "\/?>", + next : nextState + }, { + token : "keyword.operator", + regex : "=" + }, { + token : "entity.other.attribute-name", + regex : "[-_a-zA-Z0-9:]+" + }, { + token : "constant.numeric", // float + regex : "[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b" + }, { + token : "text", + regex : "\\s+" + }].concat(string(name)); +}; + +}); +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Chris Spencer + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/behaviour/xml', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/behaviour', 'ace/mode/behaviour/cstyle'], function(require, exports, module) { +"use strict"; + +var oop = require("../../lib/oop"); +var Behaviour = require("../behaviour").Behaviour; +var CstyleBehaviour = require("./cstyle").CstyleBehaviour; + +var XmlBehaviour = function () { + + this.inherit(CstyleBehaviour, ["string_dquotes"]); // Get string behaviour + + this.add("brackets", "insertion", function (state, action, editor, session, text) { + if (text == '<') { + var selection = editor.getSelectionRange(); + var selected = session.doc.getTextRange(selection); + if (selected !== "") { + return false; + } else { + return { + text: '<>', + selection: [1, 1] + } + } + } else if (text == '>') { + var cursor = editor.getCursorPosition(); + var line = session.doc.getLine(cursor.row); + var rightChar = line.substring(cursor.column, cursor.column + 1); + if (rightChar == '>') { // need some kind of matching check here + return { + text: '', + selection: [1, 1] + } + } + } else if (text == "\n") { + var cursor = editor.getCursorPosition(); + var line = session.doc.getLine(cursor.row); + var rightChars = line.substring(cursor.column, cursor.column + 2); + if (rightChars == ' + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/behaviour/cstyle', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/behaviour'], function(require, exports, module) { +"use strict"; + +var oop = require("../../lib/oop"); +var Behaviour = require('../behaviour').Behaviour; + +var CstyleBehaviour = function () { + + this.add("braces", "insertion", function (state, action, editor, session, text) { + if (text == '{') { + var selection = editor.getSelectionRange(); + var selected = session.doc.getTextRange(selection); + if (selected !== "") { + return { + text: '{' + selected + '}', + selection: false + }; + } else { + return { + text: '{}', + selection: [1, 1] + }; + } + } else if (text == '}') { + var cursor = editor.getCursorPosition(); + var line = session.doc.getLine(cursor.row); + var rightChar = line.substring(cursor.column, cursor.column + 1); + if (rightChar == '}') { + var matching = session.$findOpeningBracket('}', {column: cursor.column + 1, row: cursor.row}); + if (matching !== null) { + return { + text: '', + selection: [1, 1] + }; + } + } + } else if (text == "\n") { + var cursor = editor.getCursorPosition(); + var line = session.doc.getLine(cursor.row); + var rightChar = line.substring(cursor.column, cursor.column + 1); + if (rightChar == '}') { + var openBracePos = session.findMatchingBracket({row: cursor.row, column: cursor.column + 1}); + if (!openBracePos) + return null; + + var indent = this.getNextLineIndent(state, line.substring(0, line.length - 1), session.getTabString()); + var next_indent = this.$getIndent(session.doc.getLine(openBracePos.row)); + + return { + text: '\n' + indent + '\n' + next_indent, + selection: [1, indent.length, 1, indent.length] + }; + } + } + }); + + this.add("braces", "deletion", function (state, action, editor, session, range) { + var selected = session.doc.getTextRange(range); + if (!range.isMultiLine() && selected == '{') { + var line = session.doc.getLine(range.start.row); + var rightChar = line.substring(range.end.column, range.end.column + 1); + if (rightChar == '}') { + range.end.column++; + return range; + } + } + }); + + this.add("parens", "insertion", function (state, action, editor, session, text) { + if (text == '(') { + var selection = editor.getSelectionRange(); + var selected = session.doc.getTextRange(selection); + if (selected !== "") { + return { + text: '(' + selected + ')', + selection: false + }; + } else { + return { + text: '()', + selection: [1, 1] + }; + } + } else if (text == ')') { + var cursor = editor.getCursorPosition(); + var line = session.doc.getLine(cursor.row); + var rightChar = line.substring(cursor.column, cursor.column + 1); + if (rightChar == ')') { + var matching = session.$findOpeningBracket(')', {column: cursor.column + 1, row: cursor.row}); + if (matching !== null) { + return { + text: '', + selection: [1, 1] + }; + } + } + } + }); + + this.add("parens", "deletion", function (state, action, editor, session, range) { + var selected = session.doc.getTextRange(range); + if (!range.isMultiLine() && selected == '(') { + var line = session.doc.getLine(range.start.row); + var rightChar = line.substring(range.start.column + 1, range.start.column + 2); + if (rightChar == ')') { + range.end.column++; + return range; + } + } + }); + + this.add("string_dquotes", "insertion", function (state, action, editor, session, text) { + if (text == '"' || text == "'") { + var quote = text; + var selection = editor.getSelectionRange(); + var selected = session.doc.getTextRange(selection); + if (selected !== "") { + return { + text: quote + selected + quote, + selection: false + }; + } else { + var cursor = editor.getCursorPosition(); + var line = session.doc.getLine(cursor.row); + var leftChar = line.substring(cursor.column-1, cursor.column); + + // We're escaped. + if (leftChar == '\\') { + return null; + } + + // Find what token we're inside. + var tokens = session.getTokens(selection.start.row, selection.start.row)[0].tokens; + var col = 0, token; + var quotepos = -1; // Track whether we're inside an open quote. + + for (var x = 0; x < tokens.length; x++) { + token = tokens[x]; + if (token.type == "string") { + quotepos = -1; + } else if (quotepos < 0) { + quotepos = token.value.indexOf(quote); + } + if ((token.value.length + col) > selection.start.column) { + break; + } + col += tokens[x].value.length; + } + + // Try and be smart about when we auto insert. + if (!token || (quotepos < 0 && token.type !== "comment" && (token.type !== "string" || ((selection.start.column !== token.value.length+col-1) && token.value.lastIndexOf(quote) === token.value.length-1)))) { + return { + text: quote + quote, + selection: [1,1] + }; + } else if (token && token.type === "string") { + // Ignore input and move right one if we're typing over the closing quote. + var rightChar = line.substring(cursor.column, cursor.column + 1); + if (rightChar == quote) { + return { + text: '', + selection: [1, 1] + }; + } + } + } + } + }); + + this.add("string_dquotes", "deletion", function (state, action, editor, session, range) { + var selected = session.doc.getTextRange(range); + if (!range.isMultiLine() && (selected == '"' || selected == "'")) { + var line = session.doc.getLine(range.start.row); + var rightChar = line.substring(range.start.column + 1, range.start.column + 2); + if (rightChar == '"') { + range.end.column++; + return range; + } + } + }); + +}; + +oop.inherits(CstyleBehaviour, Behaviour); + +exports.CstyleBehaviour = CstyleBehaviour; +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/folding/xml', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/lib/lang', 'ace/range', 'ace/mode/folding/fold_mode', 'ace/token_iterator'], function(require, exports, module) { +"use strict"; + +var oop = require("../../lib/oop"); +var lang = require("../../lib/lang"); +var Range = require("../../range").Range; +var BaseFoldMode = require("./fold_mode").FoldMode; +var TokenIterator = require("../../token_iterator").TokenIterator; + +var FoldMode = exports.FoldMode = function(voidElements) { + BaseFoldMode.call(this); + this.voidElements = voidElements || {}; +}; +oop.inherits(FoldMode, BaseFoldMode); + +(function() { + + this.getFoldWidget = function(session, foldStyle, row) { + var tag = this._getFirstTagInLine(session, row); + + if (tag.closing) + return foldStyle == "markbeginend" ? "end" : ""; + + if (!tag.tagName || this.voidElements[tag.tagName.toLowerCase()]) + return ""; + + if (tag.selfClosing) + return ""; + + if (tag.value.indexOf("/" + tag.tagName) !== -1) + return ""; + + return "start"; + }; + + this._getFirstTagInLine = function(session, row) { + var tokens = session.getTokens(row, row)[0].tokens; + var value = ""; + for (var i = 0; i < tokens.length; i++) { + var token = tokens[i]; + if (token.type.indexOf("meta.tag") === 0) + value += token.value; + else + value += lang.stringRepeat(" ", token.value.length); + } + + return this._parseTag(value); + }; + + this.tagRe = /^(\s*)(?)/; + this._parseTag = function(tag) { + + var match = this.tagRe.exec(tag); + var column = this.tagRe.lastIndex || 0; + this.tagRe.lastIndex = 0; + + return { + value: tag, + match: match ? match[2] : "", + closing: match ? !!match[3] : false, + selfClosing: match ? !!match[5] || match[2] == "/>" : false, + tagName: match ? match[4] : "", + column: match[1] ? column + match[1].length : column + }; + }; + + /* + * reads a full tag and places the iterator after the tag + */ + this._readTagForward = function(iterator) { + var token = iterator.getCurrentToken(); + if (!token) + return null; + + var value = ""; + var start; + + do { + if (token.type.indexOf("meta.tag") === 0) { + if (!start) { + var start = { + row: iterator.getCurrentTokenRow(), + column: iterator.getCurrentTokenColumn() + }; + } + value += token.value; + if (value.indexOf(">") !== -1) { + var tag = this._parseTag(value); + tag.start = start; + tag.end = { + row: iterator.getCurrentTokenRow(), + column: iterator.getCurrentTokenColumn() + token.value.length + }; + iterator.stepForward(); + return tag; + } + } + } while(token = iterator.stepForward()); + + return null; + }; + + this._readTagBackward = function(iterator) { + var token = iterator.getCurrentToken(); + if (!token) + return null; + + var value = ""; + var end; + + do { + if (token.type.indexOf("meta.tag") === 0) { + if (!end) { + end = { + row: iterator.getCurrentTokenRow(), + column: iterator.getCurrentTokenColumn() + token.value.length + }; + } + value = token.value + value; + if (value.indexOf("<") !== -1) { + var tag = this._parseTag(value); + tag.end = end; + tag.start = { + row: iterator.getCurrentTokenRow(), + column: iterator.getCurrentTokenColumn() + }; + iterator.stepBackward(); + return tag; + } + } + } while(token = iterator.stepBackward()); + + return null; + }; + + this._pop = function(stack, tag) { + while (stack.length) { + + var top = stack[stack.length-1]; + if (!tag || top.tagName == tag.tagName) { + return stack.pop(); + } + else if (this.voidElements[tag.tagName]) { + return; + } + else if (this.voidElements[top.tagName]) { + stack.pop(); + continue; + } else { + return null; + } + } + }; + + this.getFoldWidgetRange = function(session, foldStyle, row) { + var firstTag = this._getFirstTagInLine(session, row); + + if (!firstTag.match) + return null; + + var isBackward = firstTag.closing || firstTag.selfClosing; + var stack = []; + var tag; + + if (!isBackward) { + var iterator = new TokenIterator(session, row, firstTag.column); + var start = { + row: row, + column: firstTag.column + firstTag.tagName.length + 2 + }; + while (tag = this._readTagForward(iterator)) { + if (tag.selfClosing) { + if (!stack.length) { + tag.start.column += tag.tagName.length + 2; + tag.end.column -= 2; + return Range.fromPoints(tag.start, tag.end); + } else + continue; + } + + if (tag.closing) { + this._pop(stack, tag); + if (stack.length == 0) + return Range.fromPoints(start, tag.start); + } + else { + stack.push(tag) + } + } + } + else { + var iterator = new TokenIterator(session, row, firstTag.column + firstTag.match.length); + var end = { + row: row, + column: firstTag.column + }; + + while (tag = this._readTagBackward(iterator)) { + if (tag.selfClosing) { + if (!stack.length) { + tag.start.column += tag.tagName.length + 2; + tag.end.column -= 2; + return Range.fromPoints(tag.start, tag.end); + } else + continue; + } + + if (!tag.closing) { + this._pop(stack, tag); + if (stack.length == 0) { + tag.start.column += tag.tagName.length + 2; + return Range.fromPoints(tag.start, end); + } + } + else { + stack.push(tag) + } + } + } + + }; + +}).call(FoldMode.prototype); + +}); +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/mode/folding/fold_mode', ['require', 'exports', 'module' , 'ace/range'], function(require, exports, module) { +"use strict"; + +var Range = require("../../range").Range; + +var FoldMode = exports.FoldMode = function() {}; + +(function() { + + this.foldingStartMarker = null; + this.foldingStopMarker = null; + + // must return "" if there's no fold, to enable caching + this.getFoldWidget = function(session, foldStyle, row) { + var line = session.getLine(row); + if (this.foldingStartMarker.test(line)) + return "start"; + if (foldStyle == "markbeginend" + && this.foldingStopMarker + && this.foldingStopMarker.test(line)) + return "end"; + return ""; + }; + + this.getFoldWidgetRange = function(session, foldStyle, row) { + return null; + }; + + this.indentationBlock = function(session, row, column) { + var re = /^\s*/; + var startRow = row; + var endRow = row; + var line = session.getLine(row); + var startColumn = column || line.length; + var startLevel = line.match(re)[0].length; + var maxRow = session.getLength() + + while (++row < maxRow) { + line = session.getLine(row); + var level = line.match(re)[0].length; + + if (level == line.length) + continue; + + if (level <= startLevel) + break; + + endRow = row; + } + + if (endRow > startRow) { + var endColumn = session.getLine(endRow).length; + return new Range(startRow, startColumn, endRow, endColumn); + } + }; + + this.openingBracketBlock = function(session, bracket, row, column) { + var start = {row: row, column: column + 1}; + var end = session.$findClosingBracket(bracket, start); + if (!end) + return; + + var fw = session.foldWidgets[end.row]; + if (fw == null) + fw = this.getFoldWidget(session, end.row); + + if (fw == "start") { + end.row --; + end.column = session.getLine(end.row).length; + } + return Range.fromPoints(start, end); + }; + +}).call(FoldMode.prototype); + +}); diff --git a/graphterm/www/perspective.css b/graphterm/www/perspective.css new file mode 100644 index 0000000..e5cbd16 --- /dev/null +++ b/graphterm/www/perspective.css @@ -0,0 +1,27 @@ +/* Inspired by http://www.seanslinsky.com/star-wars-crawl-with-css3 */ +.perspective-container { + background: url(images/starry-night-PublicDomain-VeraKratochvil.jpg); + z-index:1; + width:100%; + height:500px; + background-size: 100%; +} +.perspective { position:absolute; + z-index:1; + top:0; + left:50%; + margin-left:-300px; + width:800px; + height:500px; + overflow: scroll; + -webkit-transform: perspective(300) rotateX(20deg); + -webkit-transform-style: preserve-3d; + -moz-perspective: 800px; + -moz-transform: rotateX(45deg); + -moz-transform-style: preserve-3d; + } + +/* Input components */ +.perspective SPAN.prompt { color: red } +.perspective SPAN.command { color: red } +.perspective SPAN {color: yellow; } diff --git a/graphterm/www/test-ace.html b/graphterm/www/test-ace.html new file mode 100644 index 0000000..1431c64 --- /dev/null +++ b/graphterm/www/test-ace.html @@ -0,0 +1,41 @@ + + + + + + Editor + + + + +
function foo(items) {
+    var i;
+    for (i = 0; i < items.length; i++) {
+        alert("Ace Rocks " + items[i]);
+    }
+}
+ + + + + + diff --git a/graphterm/www/theme-twilight.js b/graphterm/www/theme-twilight.js new file mode 100644 index 0000000..894ce62 --- /dev/null +++ b/graphterm/www/theme-twilight.js @@ -0,0 +1,204 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/theme/twilight', ['require', 'exports', 'module' , 'ace/lib/dom'], function(require, exports, module) { + +exports.isDark = true; +exports.cssClass = "ace-twilight"; +exports.cssText = "\ +.ace-twilight .ace_editor {\ + border: 2px solid rgb(159, 159, 159);\ +}\ +\ +.ace-twilight .ace_editor.ace_focus {\ + border: 2px solid #327fbd;\ +}\ +\ +.ace-twilight .ace_gutter {\ + background: #e8e8e8;\ + color: #333;\ +}\ +\ +.ace-twilight .ace_print_margin {\ + width: 1px;\ + background: #e8e8e8;\ +}\ +\ +.ace-twilight .ace_scroller {\ + background-color: #141414;\ +}\ +\ +.ace-twilight .ace_text-layer {\ + cursor: text;\ + color: #F8F8F8;\ +}\ +\ +.ace-twilight .ace_cursor {\ + border-left: 2px solid #A7A7A7;\ +}\ +\ +.ace-twilight .ace_cursor.ace_overwrite {\ + border-left: 0px;\ + border-bottom: 1px solid #A7A7A7;\ +}\ +\ +.ace-twilight .ace_marker-layer .ace_selection {\ + background: rgba(221, 240, 255, 0.20);\ +}\ +\ +.ace-twilight.multiselect .ace_selection.start {\ + box-shadow: 0 0 3px 0px #141414;\ + border-radius: 2px;\ +}\ +\ +.ace-twilight .ace_marker-layer .ace_step {\ + background: rgb(198, 219, 174);\ +}\ +\ +.ace-twilight .ace_marker-layer .ace_bracket {\ + margin: -1px 0 0 -1px;\ + border: 1px solid rgba(255, 255, 255, 0.25);\ +}\ +\ +.ace-twilight .ace_marker-layer .ace_active_line {\ + background: rgba(255, 255, 255, 0.031);\ +}\ +\ +.ace-twilight .ace_marker-layer .ace_selected_word {\ + border: 1px solid rgba(221, 240, 255, 0.20);\ +}\ +\ +.ace-twilight .ace_invisible {\ + color: rgba(255, 255, 255, 0.25);\ +}\ +\ +.ace-twilight .ace_keyword, .ace-twilight .ace_meta {\ + color:#CDA869;\ +}\ +\ +.ace-twilight .ace_constant, .ace-twilight .ace_constant.ace_other {\ + color:#CF6A4C;\ +}\ +\ +.ace-twilight .ace_constant.ace_character, {\ + color:#CF6A4C;\ +}\ +\ +.ace-twilight .ace_constant.ace_character.ace_escape, {\ + color:#CF6A4C;\ +}\ +\ +.ace-twilight .ace_invalid.ace_illegal {\ + color:#F8F8F8;\ +background-color:rgba(86, 45, 86, 0.75);\ +}\ +\ +.ace-twilight .ace_invalid.ace_deprecated {\ + text-decoration:underline;\ +font-style:italic;\ +color:#D2A8A1;\ +}\ +\ +.ace-twilight .ace_support {\ + color:#9B859D;\ +}\ +\ +.ace-twilight .ace_support.ace_constant {\ + color:#CF6A4C;\ +}\ +\ +.ace-twilight .ace_fold {\ + background-color: #AC885B;\ + border-color: #F8F8F8;\ +}\ +\ +.ace-twilight .ace_support.ace_function {\ + color:#DAD085;\ +}\ +\ +.ace-twilight .ace_storage {\ + color:#F9EE98;\ +}\ +\ +.ace-twilight .ace_variable {\ + color:#AC885B;\ +}\ +\ +.ace-twilight .ace_string {\ + color:#8F9D6A;\ +}\ +\ +.ace-twilight .ace_string.ace_regexp {\ + color:#E9C062;\ +}\ +\ +.ace-twilight .ace_comment {\ + font-style:italic;\ +color:#5F5A60;\ +}\ +\ +.ace-twilight .ace_variable {\ + color:#7587A6;\ +}\ +\ +.ace-twilight .ace_xml_pe {\ + color:#494949;\ +}\ +\ +.ace-twilight .ace_meta.ace_tag {\ + color:#AC885B;\ +}\ +\ +.ace-twilight .ace_entity.ace_name.ace_function {\ + color:#AC885B;\ +}\ +\ +.ace-twilight .ace_markup.ace_underline {\ + text-decoration:underline;\ +}\ +\ +.ace-twilight .ace_markup.ace_heading {\ + color:#CF6A4C;\ +}\ +\ +.ace-twilight .ace_markup.ace_list {\ + color:#F9EE98;\ +}"; + + var dom = require("../lib/dom"); + dom.importCssString(exports.cssText, exports.cssClass); +}); diff --git a/graphterm/www/theme-vibrant_ink.js b/graphterm/www/theme-vibrant_ink.js new file mode 100644 index 0000000..0b9eb79 --- /dev/null +++ b/graphterm/www/theme-vibrant_ink.js @@ -0,0 +1,183 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +ace.define('ace/theme/vibrant_ink', ['require', 'exports', 'module' , 'ace/lib/dom'], function(require, exports, module) { + +exports.isDark = true; +exports.cssClass = "ace-vibrant-ink"; +exports.cssText = "\ +.ace-vibrant-ink .ace_editor {\ + border: 2px solid rgb(159, 159, 159);\ +}\ +\ +.ace-vibrant-ink .ace_editor.ace_focus {\ + border: 2px solid #327fbd;\ +}\ +\ +.ace-vibrant-ink .ace_gutter {\ + background: #e8e8e8;\ + color: #333;\ +}\ +\ +.ace-vibrant-ink .ace_print_margin {\ + width: 1px;\ + background: #e8e8e8;\ +}\ +\ +.ace-vibrant-ink .ace_scroller {\ + background-color: #0F0F0F;\ +}\ +\ +.ace-vibrant-ink .ace_text-layer {\ + cursor: text;\ + color: #FFFFFF;\ +}\ +\ +.ace-vibrant-ink .ace_cursor {\ + border-left: 2px solid #FFFFFF;\ +}\ +\ +.ace-vibrant-ink .ace_cursor.ace_overwrite {\ + border-left: 0px;\ + border-bottom: 1px solid #FFFFFF;\ +}\ +\ +.ace-vibrant-ink .ace_marker-layer .ace_selection {\ + background: #6699CC;\ +}\ +\ +.ace-vibrant-ink.multiselect .ace_selection.start {\ + box-shadow: 0 0 3px 0px #0F0F0F;\ + border-radius: 2px;\ +}\ +\ +.ace-vibrant-ink .ace_marker-layer .ace_step {\ + background: rgb(198, 219, 174);\ +}\ +\ +.ace-vibrant-ink .ace_marker-layer .ace_bracket {\ + margin: -1px 0 0 -1px;\ + border: 1px solid #404040;\ +}\ +\ +.ace-vibrant-ink .ace_marker-layer .ace_active_line {\ + background: #333333;\ +}\ +\ +.ace-vibrant-ink .ace_marker-layer .ace_selected_word {\ + border: 1px solid #6699CC;\ +}\ +\ +.ace-vibrant-ink .ace_invisible {\ + color: #404040;\ +}\ +\ +.ace-vibrant-ink .ace_keyword, .ace-vibrant-ink .ace_meta {\ + color:#FF6600;\ +}\ +\ +.ace-vibrant-ink .ace_constant, .ace-vibrant-ink .ace_constant.ace_other {\ + color:#339999;\ +}\ +\ +.ace-vibrant-ink .ace_constant.ace_character, {\ + color:#339999;\ +}\ +\ +.ace-vibrant-ink .ace_constant.ace_character.ace_escape, {\ + color:#339999;\ +}\ +\ +.ace-vibrant-ink .ace_constant.ace_numeric {\ + color:#99CC99;\ +}\ +\ +.ace-vibrant-ink .ace_invalid {\ + color:#CCFF33;\ +background-color:#000000;\ +}\ +\ +.ace-vibrant-ink .ace_invalid.ace_deprecated {\ + color:#CCFF33;\ +background-color:#000000;\ +}\ +\ +.ace-vibrant-ink .ace_fold {\ + background-color: #FFCC00;\ + border-color: #FFFFFF;\ +}\ +\ +.ace-vibrant-ink .ace_support.ace_function {\ + color:#FFCC00;\ +}\ +\ +.ace-vibrant-ink .ace_variable {\ + color:#FFCC00;\ +}\ +\ +.ace-vibrant-ink .ace_variable.ace_parameter {\ + font-style:italic;\ +}\ +\ +.ace-vibrant-ink .ace_string {\ + color:#66FF00;\ +}\ +\ +.ace-vibrant-ink .ace_string.ace_regexp {\ + color:#44B4CC;\ +}\ +\ +.ace-vibrant-ink .ace_comment {\ + color:#9933CC;\ +}\ +\ +.ace-vibrant-ink .ace_entity.ace_other.ace_attribute-name {\ + font-style:italic;\ +color:#99CC99;\ +}\ +\ +.ace-vibrant-ink .ace_entity.ace_name.ace_function {\ + color:#FFCC00;\ +}\ +\ +.ace-vibrant-ink .ace_markup.ace_underline {\ + text-decoration:underline;\ +}"; + + var dom = require("../lib/dom"); + dom.importCssString(exports.cssText, exports.cssClass); +}); diff --git a/graphterm/www/themes/stars-images/starry-night-PublicDomain-VeraKratochvil.jpg b/graphterm/www/themes/stars-images/starry-night-PublicDomain-VeraKratochvil.jpg new file mode 100644 index 0000000..d21dc09 Binary files /dev/null and b/graphterm/www/themes/stars-images/starry-night-PublicDomain-VeraKratochvil.jpg differ diff --git a/graphterm/www/themes/stars.css b/graphterm/www/themes/stars.css new file mode 100644 index 0000000..e1747bf --- /dev/null +++ b/graphterm/www/themes/stars.css @@ -0,0 +1,13 @@ +/* stars.css: Stars theme (3d capable) */ + +body.stars { + background: url(stars-images/starry-night-PublicDomain-VeraKratochvil.jpg); + background-repeat: repeat; +} + +/* Need font styling for both body and pre; otherwise pre styling will be overridden by useragent default */ +.stars, .stars pre { + font-family: lucida sans typweriter, lucida console, monospace; + color: yellow; +} + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2957e93 --- /dev/null +++ b/setup.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python + +import os, sys +from setuptools import setup +from setuptools.command.install import install as _install + +class install(_install): + def run(self): + _install.run(self) + dirname = os.getcwd() + try: + try: + sys.path.remove(dirname) + except Exception: + dirname = None + import graphterm.gterm_setup + graphterm.gterm_setup.main() + except Exception, excp: + print >> sys.stderr, "Failed to configure executables", excp + finally: + if dirname: + sys.path.insert(0, dirname) + +requires = ["tornado"] + +setup(name="graphterm", + cmdclass={'install': install}, + packages=["graphterm"], + entry_points={"console_scripts":["gterm = graphterm.gterm:main", + "gtermserver = graphterm.gtermserver:main", + "gtermhost = graphterm.gtermhost:main", + "gterm_setup = graphterm.gterm_setup:main"]}, + install_requires=requires, + include_package_data=True, + version="0.30", + description="GraphTerm: A Graphical Terminal Interface", + author="Ramalingam Saravanan", + author_email="sarava@sarava.net", + url="http://info.mindmeldr.com/code/graphterm", + download_url="https://github.com/mitotic/graphterm/tags", + license="BSD License", + keywords=["console", "screen", "shell", "terminal", "terminal emulator", "vt100", "xterm"], + classifiers=[ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: End Users/Desktop", + "Intended Audience :: Information Technology", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Topic :: System :: Shells", + "Topic :: Terminals :: Terminal Emulators/X Terminals", + "Topic :: Utilities", + ], + long_description="""\ +GraphTerm: A Graphical Terminal Interface +--------------------------------------------------------------------------- + +*GraphTerm* is graphical command line interface, combining features +from XMLTerm and AjaxTerm. + """ + )